Magic Methods
Magic methods are special methods, all starting with two underscores, that PHP calls automatically in certain situations: when an object is converted to a string, called like a function, cloned, or when code accesses a property or method that doesn't exist. You've already met __construct; this lesson covers the others and when (sparingly) to use them.
__toString
Called when an object is used as a string — in echo, interpolation or concatenation:
<?php final class Money { public function __construct(private int $cents, private string $currency = 'EUR') {} public function __toString(): string { return sprintf("%.2f %s", $this->cents / 100, $this->currency); } } $price = new Money(1999); echo $price, "\n"; echo "Total: $price\n"; var_dump($price instanceof Stringable);
19.99 EUR Total: 19.99 EUR bool(true)
Any class with __toString automatically implements the Stringable interface, so you can type-hint string|Stringable.
__invoke
Makes an object callable like a function — useful for small configurable operations you want to pass to array_map, usort and similar:
<?php final class Multiplier { public function __construct(private float $factor) {} public function __invoke(float $value): float { return $value * $this->factor; } } $double = new Multiplier(2); echo $double(21), "\n"; echo implode(", ", array_map(new Multiplier(1.2), [10, 20, 30])), "\n"; var_dump(is_callable($double));
42 12, 24, 36 bool(true)
Many frameworks use invokable classes for single-action controllers and middleware.
__get, __set, __isset and __unset
These run when code reads, writes, checks or unsets a property that isn't accessible (undeclared or private). They let an object expose a dynamic set of fields:
<?php final class Config { private array $values = []; public function __get(string $name): mixed { if (!array_key_exists($name, $this->values)) { throw new OutOfBoundsException("No config value '$name'"); } return $this->values[$name]; } public function __set(string $name, mixed $value): void { $this->values[$name] = $value; } public function __isset(string $name): bool { return isset($this->values[$name]); } } $config = new Config(); $config->debug = true; $config->timeout = 30; var_dump($config->timeout, isset($config->debug), isset($config->cache)); echo $config->cache;
int(30)
bool(true)
bool(false)
Fatal error: Uncaught OutOfBoundsException: No config value 'cache' in main.php:10
Stack trace:
#0 main.php(31): Config->__get('cache')
#1 {main}
thrown in main.php on line 10Use these with restraint: editors and static analysers can't see "magic" properties, typos aren't caught, and the code is slower than real properties. Since PHP 8.4, property hooks (lesson 35) handle most of the validation and computed-value cases that used to need __get and __set.
__call and __callStatic
These catch calls to inaccessible methods. They power "fluent" APIs and proxies — for instance, Laravel's query builder methods such as whereEmail(...):
<?php final class QueryBuilder { private array $wheres = []; public function __call(string $method, array $args): static { if (str_starts_with($method, 'where')) { $column = strtolower(substr($method, 5)); $this->wheres[] = "$column = '" . addslashes($args[0]) . "'"; return $this; } throw new BadMethodCallException("Unknown method $method()"); } public static function __callStatic(string $method, array $args): static { return new static()->$method(...$args); } public function toSql(): string { return "SELECT * FROM users WHERE " . implode(" AND ", $this->wheres); } } echo QueryBuilder::whereCity("Leeds")->whereRole("admin")->toSql(), "\n";
SELECT * FROM users WHERE city = 'Leeds' AND role = 'admin'
(This builds SQL by hand only to demonstrate __call; real database code must use prepared statements — lesson 58.)
__clone
clone makes a shallow copy: properties holding objects still point to the same objects. __clone runs on the copy, so you can clone those inner objects too (a "deep copy"):
<?php final class Address { public function __construct(public string $city) {} } final class Customer { public function __construct(public string $name, public Address $address) {} public function __clone(): void { $this->address = clone $this->address; } } $a = new Customer("Ana", new Address("Lisbon")); $b = clone $a; $b->name = "Ben"; $b->address->city = "Porto"; echo "{$a->name} lives in {$a->address->city}\n"; echo "{$b->name} lives in {$b->address->city}\n";
Ana lives in Lisbon Ben lives in Porto
Without __clone, changing Ben's city would also have moved Ana to Porto.
Serialisation hooks
__serialize and __unserialize control what serialize() stores — for example to leave out a database connection or a cache. (The older __sleep and __wakeup do a similar job.) __debugInfo controls what var_dump shows, which is handy for hiding secrets:
<?php final class ApiClient { public function __construct(private string $baseUrl, private string $apiKey) {} public function __debugInfo(): array { return ['baseUrl' => $this->baseUrl, 'apiKey' => '***']; } } var_dump(new ApiClient('https://api.example.com', 'sk-live-123456'));
object(ApiClient)#1 (2) {
["baseUrl"]=>
string(23) "https://api.example.com"
["apiKey"]=>
string(3) "***"
}For hiding sensitive function arguments from stack traces, PHP also has the #[\SensitiveParameter] attribute.
All the magic methods
| Method | Called when |
|---|---|
__construct, __destruct | Object created / destroyed |
__toString | Object used as a string |
__invoke | Object called like a function |
__get, __set, __isset, __unset | Inaccessible property used |
__call, __callStatic | Inaccessible method called |
__clone | Object cloned (runs on the copy) |
__serialize, __unserialize | serialize() / unserialize() |
__debugInfo | var_dump() |
__set_state | var_export() output is evaluated |
Try it yourself
Give Temperature a __toString that formats it nicely, and make it invokable to convert to Fahrenheit.
It's 21.5 °C today That's 70.7 °F