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.
- __construct, __destruct: object creation and destruction.
- __get, __set, __isset, __unset: access to inaccessible or missing properties.
- __call, __callStatic: calls to inaccessible or missing methods.
- __toString: conversion of an object to a string.
- __invoke: calling an object like a function.
- __clone: the object has just been cloned.
- __debugInfo: output of
var_dump. - __serialize, __unserialize: conversion to and from a serialized form. The older
__sleepand__wakeupdo a similar job. - __set_state: re-creation of an object exported with
var_export.
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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
- They hide the real interface of a class, so IDEs and static analysers cannot help without extra annotations such as
@propertyand@method. - They are slower than normal property and method access.
- They can hide bugs, such as typos in property names.
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
In this article, we have covered the PHP magic methods. See also the PHP OOP, PHP __invoke and PHP property hooks tutorials.
Author
List all PHP tutorials.