ZetCode

PHP Magic Methods

last modified November 14, 2026

In this article, we describe PHP magic methods: special methods whose names start with two underscores. PHP calls them automatically when a certain event happens, such as reading an inaccessible property, calling a missing method, or converting an object to a string.

We never call magic methods directly. We define them in a class, and the engine invokes them at the right moment. They allow us to intercept operations on objects and customize the behaviour of our classes.

All examples were run with PHP 8.5. The magic methods are part of the object-oriented model; if the terms are new to you, read the PHP OOP tutorial first.

Overview of magic methods

The following list shows the magic methods, grouped by the event that triggers them.

PHP reserves all names that start with __ for its own use, so avoid inventing your own methods with such names.

Constructor and destructor

The __construct method initializes an object right after it is created. Its counterpart __destruct runs when the last reference to the object disappears, or when the script ends.

construct_destruct.php
<?php

class Connection
{
    public function __construct(private string $name)
    {
        echo "Opening {$this->name}\n";
    }

    public function __destruct()
    {
        echo "Closing {$this->name}\n";
    }
}

function work(): void
{
    $c = new Connection('local');
    echo "Working in function\n";
}

$a = new Connection('A');
$b = new Connection('B');

work();

$a = null;
echo "A was released\n";

echo "End of script\n";

The destructor is useful for releasing resources, such as closing a file or a connection. The script creates objects in several ways to show when the destructor runs.

The object created inside work is destroyed as soon as the function returns, because the local variable goes out of scope. Setting $a to null removes the last reference to the first object. The object B lives until the end of the script, so it is destroyed after the last echo.

$ php construct_destruct.php
Opening A
Opening B
Opening local
Working in function
Closing local
Closing A
A was released
End of script
Closing B

Property overloading: __get, __set, __isset and __unset

These four methods are called when code touches a property that is not accessible: it does not exist, or it is private or protected. They are often used to store properties in an array.

get_set.php
<?php

class Settings
{
    private array $data = [];

    public function __get(string $name): mixed
    {
        echo "get $name\n";

        return $this->data[$name] ?? null;
    }

    public function __set(string $name, mixed $value): void
    {
        echo "set $name\n";
        $this->data[$name] = $value;
    }

    public function __isset(string $name): bool
    {
        echo "isset $name\n";

        return isset($this->data[$name]);
    }

    public function __unset(string $name): void
    {
        echo "unset $name\n";
        unset($this->data[$name]);
    }
}

$s = new Settings();

$s->theme = 'dark';
echo $s->theme, "\n";

var_dump(isset($s->theme));
var_dump(isset($s->font));
var_dump(empty($s->theme));

unset($s->theme);
var_dump(isset($s->theme));

The class keeps its values in a private array. Each operation on a missing property is routed to the matching method: __set for assignment, __get for reading, __isset for isset and empty, and __unset for unset.

Note that empty($s->theme) first calls __isset and, only when that returns true, __get to check the value. This is why the output shows both calls. If we implement __get without __isset, then isset always returns false.

$ php get_set.php
set theme
get theme
dark
isset theme
bool(true)
isset font
bool(false)
isset theme
get theme
bool(false)
unset theme
isset theme
bool(false)

The magic methods can also validate what is stored. The following class allows only two attributes and raises an exception for anything else.

typed_props.php
<?php

class User
{
    private array $attributes = [];

    public function __set(string $name, mixed $value): void
    {
        $allowed = ['name', 'email'];

        if (!in_array($name, $allowed, true)) {
            throw new InvalidArgumentException("Unknown property: $name");
        }

        $this->attributes[$name] = $value;
    }

    public function __get(string $name): mixed
    {
        return $this->attributes[$name]
            ?? throw new OutOfBoundsException("Property $name is not set");
    }
}

$u = new User();
$u->name = 'Alice';
echo $u->name, "\n";

try {
    $u->age = 30;
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";
}

try {
    echo $u->email;
} catch (OutOfBoundsException $e) {
    echo $e->getMessage(), "\n";
}

The __set method checks the property name against a whitelist. The __get method throws an exception for a missing value, using the throw expression after ??. This catches typing mistakes that PHP would otherwise ignore by creating a new property.

$ php typed_props.php
Alice
Unknown property: age
Property email is not set

The array pitfall of __get

A well-known trap is modifying an array property that is returned by __get.

get_array_pitfall.php
<?php

class Bag
{
    private array $data = ['items' => [1, 2]];

    public function __get(string $name): mixed
    {
        return $this->data[$name];
    }
}

$bag = new Bag();

$bag->items[] = 3;

print_r($bag->items);

The __get method returns the array by value, so the expression $bag->items[] = 3 would modify a temporary copy. PHP warns about it with a notice, and the original array is not changed.

To make this work, declare __get to return by reference (public function &__get(...)), or better, provide explicit methods such as addItem.

$ php get_array_pitfall.php

Notice: Indirect modification of overloaded property Bag::$items has no effect in /home/user/project/get_array_pitfall.php on line 15
Array
(
    [0] => 1
    [1] => 2
)

Method overloading: __call and __callStatic

The __call method is invoked when code calls an inaccessible or undefined instance method. __callStatic does the same for static calls. Both receive the method name and an array of arguments.

call.php
<?php

class Logger
{
    public function __call(string $method, array $args): void
    {
        $levels = ['debug', 'info', 'warning', 'error'];

        if (!in_array($method, $levels, true)) {
            throw new BadMethodCallException("Method $method does not exist");
        }

        printf("[%s] %s\n", strtoupper($method), $args[0]);
    }

    public static function __callStatic(string $method, array $args): void
    {
        echo "static call: $method(", implode(', ', $args), ")\n";
    }
}

$log = new Logger();
$log->info('Server started');
$log->error('Disk full');

Logger::create('a', 'b');

try {
    $log->fatal('boom');
} catch (BadMethodCallException $e) {
    echo $e->getMessage(), "\n";
}

The Logger class accepts four method names and rejects everything else with a BadMethodCallException. Thus we can write $log->info() and $log->error() without declaring each method. The __callStatic method handles the call Logger::create().

$ php call.php
[INFO] Server started
[ERROR] Disk full
static call: create(a, b)
Method fatal does not exist

A typical use of __call is a proxy, which forwards every call to another object and adds behaviour, such as logging, caching, or access control.

proxy.php
<?php

class Cart
{
    private array $items = [];

    public function add(string $item): static
    {
        $this->items[] = $item;

        return $this;
    }

    public function count(): int
    {
        return count($this->items);
    }
}

class LoggingProxy
{
    public function __construct(private object $target)
    {
    }

    public function __call(string $method, array $args): mixed
    {
        $result = $this->target->$method(...$args);

        echo "$method(" . implode(', ', $args) . ") was called\n";

        return $result;
    }
}

$cart = new LoggingProxy(new Cart());
$cart->add('apple');
$cart->add('pear');

echo $cart->count(), "\n";

The proxy stores the target object. Its __call method invokes the same method on the target with the argument unpacking operator ...$args, prints a message, and returns the result. The client code does not know that it talks to a proxy.

$ php proxy.php
add(apple) was called
add(pear) was called
count() was called
2

__toString

The __toString method tells PHP how to convert an object to a string. It must return a string.

tostring.php
<?php

class Money
{
    public function __construct(
        private int $cents,
        private string $currency = 'EUR'
    ) {
    }

    public function __toString(): string
    {
        return sprintf('%s %.2f', $this->currency, $this->cents / 100);
    }
}

$price = new Money(1999);

echo $price, "\n";
echo 'Total: ' . $price . "\n";
echo "Interpolated: $price\n";
echo strlen($price), "\n";

var_dump($price instanceof Stringable);
var_dump((string) $price);
echo str_pad($price, 15, '.'), "\n";

The object can now be printed with echo, concatenated, embedded in a double-quoted string, and passed to functions that expect a string. A class that defines __toString automatically implements the Stringable interface (PHP 8.0), which is what the instanceof check shows.

Note that strlen gives 9, the length of the string EUR 19.99, not of the object.

$ php tostring.php
EUR 19.99
Total: EUR 19.99
Interpolated: EUR 19.99
9
bool(true)
string(9) "EUR 19.99"
EUR 19.99......

__invoke

The __invoke method makes an object callable, so it can be used wherever PHP expects a callback.

invoke.php
<?php

class Multiplier
{
    public function __construct(private int $factor)
    {
    }

    public function __invoke(int $n): int
    {
        return $n * $this->factor;
    }
}

$triple = new Multiplier(3);

echo $triple(5), "\n";
var_dump(is_callable($triple));
print_r(array_map($triple, [1, 2, 3]));

The Multiplier object keeps a factor as state and multiplies its argument. We call it as $triple(5), check it with is_callable, and pass it to array_map. See the __invoke method tutorial for more.

$ php invoke.php
15
bool(true)
Array
(
    [0] => 3
    [1] => 6
    [2] => 9
)

__clone

The clone operator makes a shallow copy: properties that hold objects are shared between the original and the copy. The __clone method runs on the new object and lets us make a deep copy.

clone.php
<?php

class Address
{
    public function __construct(public string $city)
    {
    }
}

class Person
{
    public function __construct(
        public string $name,
        public Address $address
    ) {
    }

    public function __clone(): void
    {
        $this->address = clone $this->address;
    }
}

$a = new Person('Alice', new Address('Bratislava'));
$b = clone $a;

$b->name = 'Bob';
$b->address->city = 'Kosice';

echo $a->name, ' lives in ', $a->address->city, "\n";
echo $b->name, ' lives in ', $b->address->city, "\n";

Without __clone, both persons would point to the same Address, and changing the city of Bob would change the city of Alice. With the method, the address is cloned as well, so the two objects are independent.

$ php clone.php
Alice lives in Bratislava
Bob lives in Kosice

__debugInfo

The __debugInfo method controls what var_dump and print_r show. It lets us hide secrets or noise.

debuginfo.php
<?php

class Account
{
    public function __construct(
        private string $owner,
        private string $password
    ) {
    }

    public function __debugInfo(): array
    {
        return ['owner' => $this->owner, 'password' => '***'];
    }
}

$acc = new Account('alice', 'secret');

var_dump($acc);
print_r($acc);
echo "\n";
var_export($acc);
echo "\n";

We return an array with a masked password. Both var_dump and print_r use it. The var_export function does not; it exports the real properties, so never rely on __debugInfo to protect secrets in other contexts.

$ php debuginfo.php
object(Account)#1 (2) {
  ["owner"]=>
  string(5) "alice"
  ["password"]=>
  string(3) "***"
}
Account Object
(
    [owner] => alice
    [password] => ***
)

\Account::__set_state(array(
   'owner' => 'alice',
   'password' => 'secret',
))

__serialize and __unserialize

When an object is serialized, PHP calls __serialize to get an array of the data to store. During unserialization __unserialize receives that array. We can leave out values that must not be stored and restore resources.

serialize.php
<?php

class Session
{
    private $handle;

    public function __construct(
        public string $user,
        private string $token
    ) {
        $this->handle = fopen('php://memory', 'r+');
    }

    public function isConnected(): bool
    {
        return is_resource($this->handle);
    }

    public function __serialize(): array
    {
        return ['user' => $this->user];
    }

    public function __unserialize(array $data): void
    {
        $this->user = $data['user'];
        $this->token = '';
        $this->handle = fopen('php://memory', 'r+');
    }
}

$s = new Session('alice', 'abc123');

$str = serialize($s);
echo $str, "\n";

$copy = unserialize($str);
echo $copy->user, "\n";
var_dump($copy->isConnected());

The class holds a token and a stream resource. A resource cannot be serialized and the token should not be stored, so __serialize returns only the user name. After unserialize, the __unserialize method sets the token to an empty string and reopens the stream.

The serialized string contains only the public data of the object. The older __sleep and __wakeup methods do a similar job, but the new pair is preferred.

$ php serialize.php
O:7:"Session":1:{s:4:"user";s:5:"alice";}
alice
bool(true)

__set_state

The var_export function outputs valid PHP code for a value. For an object, the code contains a call to the static __set_state method, which we implement to re-create the object.

set_state.php
<?php

class Point
{
    public function __construct(public int $x = 0, public int $y = 0)
    {
    }

    public static function __set_state(array $props): static
    {
        return new static($props['x'], $props['y']);
    }
}

$p = new Point(3, 4);

$file = tempnam(sys_get_temp_dir(), 'point');
file_put_contents($file, '<?php return ' . var_export($p, true) . ';');

echo file_get_contents($file), "\n";

$q = include $file;
unlink($file);

var_dump($q == $p);
var_dump($q === $p);

We export the object into a PHP file and load it with include. The __set_state method receives an array of properties and returns a new object. The new object is equal (==) to the original, but not identical (===), because it is a different instance.

This is how some frameworks generate cache files that load fast, without parsing JSON or unserializing.

$ php set_state.php
<?php return \Point::__set_state(array(
   'x' => 3,
   'y' => 4,
));
bool(true)
bool(false)

When to use magic methods

Magic methods are powerful, but they have costs.

Use them where they pay off: proxies, fluent builders, value objects with __toString, and cleanup in __destruct. For plain properties with extra logic, prefer property hooks (PHP 8.4), which are explicit and visible to tools.

Source

PHP magic methods

PHP overloading

In this article, we have covered the PHP magic methods. See also the PHP OOP, PHP __invoke and PHP property hooks tutorials.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all PHP tutorials.