0

Magic method — PHP's __ prefix hooks: what 'magic' actually means

Beginner5 min read·eng-12-020
interview

Concept

Magic methods — PHP's __ (double underscore) prefix methods that PHP calls automatically in response to specific actions. "Magic" because you don't call them directly — PHP invokes them behind the scenes.

Why the name "magic": These methods are not part of any interface. PHP calls them implicitly when certain operations occur on objects. The behavior is convention-based, not enforced by the type system.

The most important magic methods:

  • __construct(): Called when new ClassName() is executed. Initialize the object.
  • __destruct(): Called when the object is garbage collected or explicitly unset.
  • __get($name): Called when reading an inaccessible/undefined property.
  • __set($name, $value): Called when writing to an inaccessible/undefined property.
  • __isset($name): Called when isset() or empty() is used on inaccessible property.
  • __unset($name): Called when unset() is used on inaccessible property.
  • __call($method, $args): Called when an inaccessible/undefined instance method is called.
  • __callStatic($method, $args): Called when an inaccessible/undefined static method is called.
  • __toString(): Called when the object is cast to string (e.g., echo $object).
  • __invoke($args...): Called when the object is called as a function ($obj()).
  • __clone(): Called after clone $object. Customize how the clone is initialized.
  • __serialize() / __unserialize(): Custom serialization (PHP 7.4+).
  • __debugInfo(): Controls what var_dump() shows.

PHP Attributes are NOT magic methods: #[Route] is a different mechanism — metadata, not behavior.

Code Example

php
<?php
class DynamicObject
{
    private array $data = [];

    // __get / __set — dynamic properties
    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? null;
    }

    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 called
echo $obj->name;         // __get called → 'Alice'
echo isset($obj->name);  // __isset called → true

// __toString — echo-able objects
class Money
{
    public function __construct(private readonly int $cents, private readonly string $currency) {}
    public function __toString(): string { return number_format($this->cents / 100, 2) . ' ' . $this->currency; }
}
echo new Money(1999, 'USD'); // '19.99 USD'

// __invoke — callable objects
class Multiplier
{
    public function __construct(private readonly int $factor) {}
    public function __invoke(int $n): int { return $n * $this->factor; }
}
$double = new Multiplier(2);
echo $double(5);        // 10 — __invoke called
echo $double(10);       // 20
array_map($double, [1, 2, 3]); // works as a callable

// __call / __callStatic — method overloading (proxy pattern)
class ApiClient
{
    private string $baseUrl;

    public function __call(string $method, array $args): mixed
    {
        // Dynamically generate API methods: get(), post(), delete()
        $httpMethod = strtoupper($method);
        return $this->request($httpMethod, $args[0], $args[1] ?? []);
    }

    public static function __callStatic(string $method, array $args): static
    {
        return new static($args[0] ?? '');
    }

    private function request(string $method, string $url, array $data): array { return []; }
}

$api = new ApiClient();
$api->get('/users');     // __call: method='get', args=['/users']
$api->post('/users', ['name' => 'Alice']); // __call: method='post'
ApiClient::create('https://api.example.com'); // __callStatic

// __clone — deep copy
class Connection
{
    public \PDO $pdo;
    public function __clone(): void
    {
        $this->pdo = clone $this->pdo; // ensure deep copy of the PDO connection
    }
}