Magic Methods and Overloading

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. Magic methods are methods with reserved names starting with __ that PHP calls automatically in specific situations: when an object is created or destroyed, when an inaccessible property or method is used, when an object is converted to a string, called, exported, dumped or serialised. __clone() is covered in Cloning, Late Static Binding and Object Lifecycle. All magic methods except __construct(), __destruct() and __clone() must be public, and since PHP 8.0 their signatures are checked when they declare types.

Method Called when

__construct(…)

the object is created with new

__destruct()

the last reference to the object goes away

__get($name), __set($name, $value)

reading or writing an inaccessible or undefined property

__isset($name), __unset($name)

isset()/empty() or unset() on such a property

__call($name, $args), __callStatic($name, $args)

calling an inaccessible or undefined method

__toString()

the object is used as a string

__invoke(…)

the object is called like a function

__set_state(array $properties)

code produced by var_export() is evaluated

__debugInfo()

var_dump() dumps the object

__serialize(), __unserialize(array $data)

serialize() / unserialize()

__sleep(), __wakeup()

the older serialisation hooks (soft-deprecated in 8.5)

__clone()

the object is cloned

Source: Magic Methods

Property overloading: __get, __set, __isset and __unset

"Overloading" in PHP means handling access to members that do not exist or are not visible. __get() and __set() run when code reads or writes such a property; __isset() and __unset() run for isset()/empty() and unset(). Without __isset(), isset($obj->virtual) returns false even when __get() would return a value.

<?php

declare(strict_types=1);

/** A record whose fields come from a database row (as ORMs do). */
final class Record
{
    public function __construct(private array $attributes = []) {}

    public function __get(string $name): mixed
    {
        if (!array_key_exists($name, $this->attributes)) {
            throw new OutOfRangeException("No attribute {$name}");
        }
        return $this->attributes[$name];
    }

    public function __set(string $name, mixed $value): void
    {
        $this->attributes[$name] = $value;
    }

    public function __isset(string $name): bool
    {
        return isset($this->attributes[$name]);
    }

    public function __unset(string $name): void
    {
        unset($this->attributes[$name]);
    }
}

$book = new Record(['title' => 'Kindred']);
$book->year = 1979;                         // __set
echo $book->title, ' ', $book->year, "\n";  // __get: Kindred 1979
var_dump(isset($book->year), empty($book->subtitle));   // __isset: bool(true) bool(true)
unset($book->year);                         // __unset
var_dump(isset($book->year));               // bool(false)

Since PHP 8.4, property hooks cover most uses of __get/__set for declared properties, with typing and static analysis; keep the magic methods for genuinely dynamic attribute bags (as Laravel’s Eloquent models use them). Magic property access is slower than declared properties and invisible to tools unless documented with @property PHPDoc.

Method overloading: __call and __callStatic

__call() receives the name and arguments of an inaccessible instance method, __callStatic() those of a static one. Frameworks use them for dynamic finders and proxies: Laravel’s facades forward static calls to service instances with __callStatic(), and Eloquent’s Book::where(...) reaches the query builder this way.

<?php

declare(strict_types=1);

final class BookQuery
{
    private array $wheres = [];

    public function __call(string $method, array $args): static
    {
        if (str_starts_with($method, 'where')) {                    // whereTitle('Kindred') -> title = Kindred
            $column = lcfirst(substr($method, 5));
            $this->wheres[] = sprintf("%s = '%s'", $column, $args[0]);
            return $this;
        }
        throw new BadMethodCallException("Method {$method} does not exist");
    }

    public static function __callStatic(string $method, array $args): static
    {
        return (new static())->$method(...$args);                  // BookQuery::whereYear(1979)
    }

    public function toSql(): string
    {
        return 'SELECT * FROM books WHERE ' . implode(' AND ', $this->wheres);
    }
}

echo BookQuery::whereAuthor('Butler')->whereYear(1979)->toSql(), "\n";
// SELECT * FROM books WHERE author = 'Butler' AND year = '1979'

The example interpolates values into SQL only to show the generated text; real query builders bind parameters (see Databases with PDO). Document magic methods with @method PHPDoc tags so IDEs and static analysers know about them.

__toString and Stringable

__toString() defines the object’s string conversion, used by echo, concatenation, interpolation, string parameters in coercive mode and string comparisons. Since PHP 8.0 every class with __toString() automatically implements the Stringable interface, and __toString() may throw exceptions (it could not before 7.4).

<?php

declare(strict_types=1);

final readonly class Isbn
{
    public function __construct(private string $digits) {}

    public function __toString(): string
    {
        return sprintf('%s-%s-%s-%s-%s', substr($this->digits, 0, 3), $this->digits[3],
            substr($this->digits, 4, 2), substr($this->digits, 6, 6), $this->digits[12]);
    }
}

function label(string|Stringable $value): string { return "[{$value}]"; }

$isbn = new Isbn('9780000000011');
echo $isbn, "\n";                              // 978-0-00-000001-1
echo label($isbn), "\n";                       // [978-0-00-000001-1]
var_dump($isbn instanceof Stringable);         // bool(true) -- implemented automatically

__invoke

An object with __invoke() is callable: $obj(…) calls it, is_callable($obj) is true, and it satisfies the callable type. Invokable objects are a natural shape for single-action services (validators, handlers, Laravel’s single-action controllers and invokable jobs), because they can hold configuration in their constructor:

<?php

declare(strict_types=1);

final class LateFee
{
    public function __construct(private int $centsPerDay, private int $cap) {}

    public function __invoke(int $daysLate): int
    {
        return min($this->cap, max(0, $daysLate) * $this->centsPerDay);
    }
}

$fee = new LateFee(25, 1000);
echo $fee(10), ' ', $fee(100), "\n";                      // 250 1000
print_r(array_map($fee, [0, 3, 7]));                      // usable wherever a callable is expected

Source: __invoke()

__set_state and var_export

var_export() produces valid PHP code for a value. For objects it emits a call to ClassName::__set_state([...]) with the properties; the class must implement this static method to rebuild the object. Since 7.3, stdClass is exported as an (object) array(...) cast instead. This is how some tools generate cached configuration files that OPcache can load quickly.

<?php

declare(strict_types=1);

final class LoanPolicy
{
    public int $days = 21;
    public int $maxRenewals = 2;

    public static function __set_state(array $properties): self
    {
        $policy = new self();
        $policy->days = $properties['days'];
        $policy->maxRenewals = $properties['maxRenewals'];
        return $policy;
    }
}

$code = var_export(new LoanPolicy(), true);
echo $code, "\n";

$file = sys_get_temp_dir() . '/loan-policy.cache.php';
file_put_contents($file, "<?php\nreturn {$code};\n");
$restored = require $file;
var_dump($restored->days);                                // int(21)
\LoanPolicy::__set_state(array(
   'days' => 21,
   'maxRenewals' => 2,
))
int(21)

__debugInfo

__debugInfo() returns the array that var_dump() shows for the object, which is useful to hide secrets (passwords, tokens, connection objects) or to show a computed summary. Since PHP 8.5 it must return an array; returning null is deprecated.

<?php

declare(strict_types=1);

final class Member
{
    public function __construct(private string $email, private string $passwordHash) {}

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

var_dump(new Member('reader@library.example', password_hash('secret', PASSWORD_DEFAULT)));

For function arguments in stack traces, the #[\SensitiveParameter] attribute (8.2) hides values instead; see Attributes.

Source: __debugInfo()

Serialisation: __serialize and __unserialize

serialize() turns a value, including objects, into a string that unserialize() turns back into an equal value. By default all properties are written. Classes control this with __serialize(), which returns an array of what to store, and __unserialize(array $data), which restores the object from it (both 7.4). Resources, closures, connections and other non-serialisable members must be excluded and rebuilt.

<?php

declare(strict_types=1);

final class CatalogueCache
{
    private ?PDO $connection = null;                          // must not be serialised

    public function __construct(private array $titles, private string $dsn = 'sqlite::memory:') {}

    public function __serialize(): array
    {
        return ['titles' => $this->titles, 'dsn' => $this->dsn, 'version' => 2];
    }

    public function __unserialize(array $data): void
    {
        $this->titles = $data['titles'];
        $this->dsn = $data['dsn'];                            // the connection is reopened lazily when needed
    }

    public function titles(): array { return $this->titles; }
}

$data = serialize(new CatalogueCache(['Kindred', 'Solaris']));
echo $data, "\n";
print_r(unserialize($data, ['allowed_classes' => [CatalogueCache::class]])->titles());

__sleep() (return the list of property names to keep) and __wakeup() (re-initialise after unserialising) are the older pair; if __serialize() exists, __sleep() is ignored. PHP 8.5 soft-deprecates __sleep() and __wakeup(): they still work without a notice, but new code should use __serialize()/__unserialize(). The Serializable interface has been deprecated since 8.1.

For data exchanged with other systems, prefer JSON (JsonSerializable) over PHP serialisation, which is PHP-specific and tied to class names.

unserialize() is dangerous with untrusted input

unserialize() instantiates objects of any class named in the string and calls their magic methods (__unserialize(), __wakeup(), __destruct() and others). Attackers who control the input can chain existing classes ("gadgets") into file writes or remote code execution: PHP object injection. Rules:

  • Never call unserialize() on data from users, cookies, URLs or other untrusted sources. Use json_decode().

  • When unserialising trusted data, restrict classes with ['allowed_classes' => [Allowed::class]] (or false for none: objects become __PHP_Incomplete_Class), and limit nesting with max_depth.

  • Sign data you must round-trip through untrusted storage with hash_hmac() and verify it with hash_equals() before unserialising.

<?php

declare(strict_types=1);

final class Session { public function __construct(public string $user = 'guest') {} }

$payload = serialize(new Session('ada'));

$safe = unserialize($payload, ['allowed_classes' => false]);
var_dump(get_class($safe));          // string(22) "__PHP_Incomplete_Class"

$allowed = unserialize($payload, ['allowed_classes' => [Session::class], 'max_depth' => 4]);
var_dump($allowed->user);            // string(3) "ada"

Where older books and tutorials are outdated

  • Serializable (Beak ch. 5) is deprecated since 8.1; use __serialize()/__unserialize() (7.4).

  • __sleep()/__wakeup() are soft-deprecated since 8.5.

  • __toString() could not throw before 7.4, and Stringable (8.0) did not exist.

  • Magic accessors for declared properties are now better expressed with property hooks (8.4).

  • __autoload() was a magic function, not a method, and was removed in 8.0.

  • unserialize() without allowed_classes on user input appears in several older tutorials; it is a known remote code execution vector.