Magic methods: __get, __set, __isset, __unset (property overloading)
Concept
Property overloading magic methods allow classes to intercept reads and writes to inaccessible properties — properties that don't exist, or exist but are private/protected and accessed from outside. They are called "overloading" because they override the default "property not found" behavior.
__get($name): Called when accessing an inaccessible property. $obj->name where name is undefined or inaccessible triggers __get('name'). Return the value you want the expression to resolve to.
__set($name, $value): Called when writing to an inaccessible property. $obj->name = 'Alice' where name is inaccessible triggers __set('name', 'Alice').
__isset($name): Called when isset() or empty() is used on an inaccessible property. Must return bool.
__unset($name): Called when unset() is used on an inaccessible property.
Laravel's Eloquent: Uses __get/__set to proxy attribute access to the internal $attributes array, apply mutators/accessors, and load relationships lazily. $user->name calls __get('name') which looks up $this->attributes['name'] and passes it through any registered accessor.
Pitfalls: These methods are slow (function call per property access), confusing to IDE autocompletion (editors can't see dynamically provided properties), and can hide bugs (typos in property names silently invoke __get instead of throwing an error). Use @property PHPDoc annotations to help IDEs.
Code Example
<?php
declare(strict_types=1);
// Dynamic property store
class DynamicObject
{
private array $data = [];
public function __get(string $name): mixed
{
if (!array_key_exists($name, $this->data)) {
throw new \RuntimeException("Property $name does not exist");
}
return $this->data[$name];
}
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
public function __unset(string $name): void
{
unset($this->data[$name]);
}
}
$obj = new DynamicObject();
$obj->name = 'Alice'; // __set
echo $obj->name; // __get → 'Alice'
var_dump(isset($obj->name)); // __isset → true
unset($obj->name); // __unset
// echo $obj->name; // throws RuntimeException
// Eloquent-like attribute proxy
class Model
{
protected array $attributes = [];
protected array $mutators = [];
public function __get(string $key): mixed
{
// Check for accessor (getNameAttribute)
$method = 'get' . ucfirst($key) . 'Attribute';
if (method_exists($this, $method)) {
return $this->$method();
}
return $this->attributes[$key] ?? null;
}
public function __set(string $key, mixed $value): void
{
// Check for mutator (setNameAttribute)
$method = 'set' . ucfirst($key) . 'Attribute';
if (method_exists($this, $method)) {
$this->$method($value);
return;
}
$this->attributes[$key] = $value;
}
}
class User extends Model
{
public function getNameAttribute(): string
{
return ucfirst($this->attributes['name'] ?? '');
}
public function setNameAttribute(string $value): void
{
$this->attributes['name'] = strtolower($value);
}
}
$user = new User();
$user->name = 'ALICE'; // mutator lowercases it
echo $user->name; // "Alice" (accessor ucfirsts it)