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 |
|---|---|
|
the object is created with |
|
the last reference to the object goes away |
|
reading or writing an inaccessible or undefined property |
|
|
|
calling an inaccessible or undefined method |
|
the object is used as a string |
|
the object is called like a function |
|
code produced by |
|
|
|
|
|
the older serialisation hooks (soft-deprecated in 8.5) |
|
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.
Source: Property overloading
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.
Source: Method overloading
__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
Source: __toString() and The Stringable interface
__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)
Source: __set_state() and var_export
__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.
Source: __serialize() and __unserialize(), Object Serialization and RFC: Soft-deprecate __sleep() and __wakeup()
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. Usejson_decode(). -
When unserialising trusted data, restrict classes with
['allowed_classes' => [Allowed::class]](orfalsefor none: objects become__PHP_Incomplete_Class), and limit nesting withmax_depth. -
Sign data you must round-trip through untrusted storage with
hash_hmac()and verify it withhash_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"
Source: unserialize and OWASP — PHP Object Injection
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, andStringable(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()withoutallowed_classeson user input appears in several older tutorials; it is a known remote code execution vector.