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
<?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);
Output
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
<?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));
Output
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
<?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;
Output
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 10

Use 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
<?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";
Output
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
<?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";
Output
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
<?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'));
Output
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

MethodCalled when
__construct, __destructObject created / destroyed
__toStringObject used as a string
__invokeObject called like a function
__get, __set, __isset, __unsetInaccessible property used
__call, __callStaticInaccessible method called
__cloneObject cloned (runs on the copy)
__serialize, __unserializeserialize() / unserialize()
__debugInfovar_dump()
__set_statevar_export() output is evaluated

Try it yourself

Give Temperature a __toString that formats it nicely, and make it invokable to convert to Fahrenheit.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
It's 21.5 °C today
That's 70.7 °F