Metaprogramming
Magic methods: how $user->name works when there is no name
The mistake
An Eloquent model feels like it has a property for every column and a method for
every query. $user->name reads the name, User::where(...) builds a query, and it
all looks like ordinary object code. So when people meet __get and __call, the
guess is that these magic methods sit in front of every access and intercept it.
They do not. PHP tries the ordinary thing first: it looks for a real, accessible property or method, and only when there is none does it fall back to a magic method. Magic is the last resort, not the front door. Once you see the order, the model stops being mysterious: it has almost no real properties or methods for its columns and queries at all. It is a small shell that catches the misses and dispatches them.
The machine
The resolution order, what resolves each access and the result run on the tested reducer, matching how PHP falls back to magic methods only when ordinary lookup fails.
Drive it
The panel boots on $user->name. The model declares no $name property, so watch
where the access goes.
- Resolve it. PHP looks for a declared
$name, finds none, and falls through to__get('name'), which reads the attributes bag. The column was never a property. - Try
where(), thensave().where()is not a method either: it falls through to__calland forwards to a query builder. Butsave()is a real method onModel, so it runs directly and no magic happens. Real members win. - Declare a real
public $name, then read the column again. Now the declared property is found first,__getnever runs, and the row’s value is never read. One stray property silently breaks the attribute.
The mechanism
For a method call, PHP resolves $obj->method(...) like this. It searches the class
and its parents for a method that is defined and accessible from the calling scope.
If it finds one, it calls it, and that is the end of the story. Only if there is no
such method does PHP call __call('method', $args), if the class defines it. With
neither, you get Error: Call to undefined method. Static calls work the same way
through __callStatic.
Property access is the same shape. $obj->prop returns a declared, accessible
property if there is one. An undefined or inaccessible property falls through to
__get('prop'); writing one falls through to __set('prop', $value). The word
“inaccessible” matters: reading a private property from outside the class also
triggers __get, because from that scope the real property cannot be seen.
That fallback is the entire machinery of an Eloquent model. Its columns are not
declared properties, they live in a $attributes array, so every $user->name misses
the property lookup and lands in __get, which calls getAttribute and reads the
array (or runs an accessor, or loads a relation). Its query methods are not declared
either, so $user->where(...) misses and lands in __call, which forwards to a fresh
query builder. But save, delete, fresh and the rest are real methods on the
Model base class, so they resolve directly and never touch __call. The model looks
like it has hundreds of members. It has a handful, plus two magic methods that fake
the rest.
Which is exactly why shadowing causes trouble. Declare a real property whose name matches a
column, and the property lookup now succeeds, so __get is never reached and the
column value never loads.
In your code
The pattern on its own is small. A class catches unknown reads with __get and
unknown calls with __call, while its real methods stay real:
class Config
{
private array $items = ['driver' => 'mysql'];
public function __get(string $key): mixed
{
return $this->items[$key] ?? null;
}
public function reload(): void { /* a real, declared method */ }
}
$c = new Config();
$c->driver; // 'mysql' via __get: there is no declared $driver
$c->reload(); // runs directly: __call is never consulted
Eloquent is this pattern at scale, and the shadowing bug is a real one:
class User extends Model
{
public $name; // don't: this declares a real property that shadows the column
}
$user = User::find(1);
$user->name; // null, not the row value: the real property shadowed __get
The fix is to not declare it. A model’s columns are meant to live in the attributes
bag and be reached through __get, so the property you were about to add is the bug.
The fine print
- This page shows
__get,__setand__call. The family is larger:__callStaticfor static calls,__issetand__unsetforisset()andunset(),__invokefor calling an object like a function, and__toString. - Magic dispatch is slower than a real member and invisible to IDEs and static
analysis, which cannot see a property that does not exist in the source. Laravel
leans on
@propertyand@methoddocblocks, generated model helpers, so your tools know what a model appears to have. - PHP walks the whole inheritance chain for a real member before it ever considers a
magic method.
__callreally is the last thing tried. __getreturns by value. Trying to modify what it returns in place, like$model->items[] = 'x'whenitemscomes from__get, raises an “indirect modification of overloaded property” notice, because there is no real property to modify.
Further reading
- PHP: overloading is
the reference for
__get,__set,__call,__callStaticand the rest, including exactly when each fires. - Laravel: accessors, mutators and casting
is
getAttributeandsetAttribute, the methods__getand__setdispatch to. - Laravel: the base Model
lists the real methods, like
save, that resolve without any magic at all.
Spotted a problem, or have a way to make this clearer? Suggest an improvement.