0

Magic methods: __get, __set, __isset, __unset (property overloading)

Advanced5 min read·php-07-012
interviewlaravel-src

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
<?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)