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 whennew 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 whenisset()orempty()is used on inaccessible property.__unset($name): Called whenunset()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 afterclone $object. Customize how the clone is initialized.__serialize()/__unserialize(): Custom serialization (PHP 7.4+).__debugInfo(): Controls whatvar_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
}
}