PHP Magic

Understanding PHP Magic Methods with Practical Examples

Erland Muchasaj
9 min readOct 17, 2023
Understanding PHP Magic Methods with Practical Examples
Understanding PHP Magic Methods with Practical Examples

In object-oriented programming (OOP), PHP provides a set of special methods known as “magic methods” that begin with a double underscore (__). These methods are automatically called by PHP in response to certain events, allowing you to implement custom behaviors within your classes.

There are several magic methods in PHP, each serving a unique purpose. I will go through each of them and explain what they do and how we can use them in our projects.

You should avoid prefixing your own function with double underscore __ in order to avoid any confusion or conflict with php magic methods, and also all magic methods should be declared public.

The name “magic” was coined because these methods automatically get invoked at specific times or under specific circumstances, without being explicitly called by the programmer. The behavior they provide can seem like magic, as the PHP interpreter handles it behind the scenes.

__construct()

This is the constructor method, invoked automatically when an object of a class is created using the new keyword. It initializes object properties or performs setup actions when the object is instantiated.

class Address
{
public function __construct(
public string $street,
protected string $city,
private readonly string $state,
private ?int $zip = null,
private readonly string $type = 'address',
) {
}

// ...
}

# usage
$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

var_dump($address);

object(App\Utils\Address)[2060]
private string 'street' => string 'Blv. Bajram Curri' (length=17)
protected string 'city' => string 'Tirana' (length=6)
public string 'state' => string 'Albania' (length=7)
public int 'zip' => int 1040
private readonly string 'type' => string 'address' (length=7)

As you can see I used php 8 property promotion.

__destruct()

The destructor method is automatically called when the object is no longer referenced or is explicitly destroyed using unset() or at the end of the script execution. It is typically used to release resources or perform cleanup operations.

public function __destruct() {
$connection->close();
}

__construct() and __destruct() are not called when methods are called statically.

__invoke()

This method is called when you try to call an object as if it were a function. A use case for this is to use it as a single method class or Invokable Controllers in Laravel.

class Address
{
// ...
public function __invoke(): void
{
dump('Address object is called as a function!');
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);
$address(); // Address object is called as a function!

__toString()

This method is invoked when an object is treated as a string using echo or print for example, what echo $address; will print out. It allows you to define how an object should be represented as a string.

class Address
{
// ...
public function __toString() {
return "{$this->type}: {$this->street}, {$this->city}, {$this->state}, {$this->zip}.";
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

// return object as string using __toString
echo "Full address: {$address}"; // Blv. Bajram Curri, Tirana, Albania, 1040

__clone()

Creating a copy of an object with fully replicated properties is not always the wanted behavior, sometimes you might want to prevent the cloning of an object at all or you might want to make some modification to some properties before you copy them, or even when you have a singleton class that you want to prevent people from cloning it.
This method is automatically invoked when an object is cloned using the clone keyword.
Note that when you clone an object the __destruct() will be called again on the newly cloned object, but __construct() won't.

Once the cloning is complete, if a __clone() method is defined, then the newly created object’s __clone() method will be called, to allow any necessary properties that need to be changed.

class Address
{
// ...
public function __clone() {
trigger_error('Cloning forbidden.', E_USER_ERROR);
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

$newAddress = clone $address; // throw e_error: Cloning forbiden.

Another way to prevent the cloning of an object of a class is to make the method private like so:

/**
* Prevent cloning of the instance of the object.
*
* @return void
*/
private function __clone() {}

One other way we can utilize clone is to change the attribute value on the newly created object. For example, if we want to make all clones flagged we can do that like so:

/**
* Increment Id.
*
* @return void
*/
public function __clone() {
$this->is_cloned = true;
}

// now each new object cloned will have the propertys is_cloned set to true.

The __sleep() and __wakeup() methods.

These methods are called when we call serialize() and unserialize() functions on an object respectively.

The __sleep() method is useful if a very large object doesn’t need to be saved completely. so you pass only the attribute that you want to be serialized.

class Address
{
// ...
public function __sleep()
{
return array('street', 'zip');
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

// Set object properties using __sleep
$serialized = serialize($address);

The __wakeup() method is useful when you want to perform an action after a deserialization of an object. For example, if you need to connect to DB or if you need to connect to an external service.

class Address
{
// ...
public function __wakeup()
{
$this->connect();
}
}

Property Overloading

The overloading methods are invoked when interacting with properties or methods that have not been declared or are not visible in the current scope.

So when you try to access a property that is protected or private from outside the class, these methods are called.

__set($name, $value)

Invoked when an attempt is made to assign a value to an inaccessible (private or protected) or undefined property. It enables you to handle setting values for properties that are not accessible in a typical manner.

class Address
{
// ...
public function __set($name, $value) {
if (property_exists($this, $name)) {
$this->$name = $value;
}

dump("Address `$name` property was set to: `$value`",);
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

# "Address `city` property was set to: `London`"
$address->city = 'London'; // this WILL trigger __set() method since is `protected`.
$address->state = 'England'; // this will NOT since is `public`.

var_dump($address);

__get($name)

We discussed how to set values for non-existent or non-accessible (private or protected) properties. The __get($name) method is exactly the opposite of __set($name, $value), it is utilized for reading data from inaccessible (protected or private) or non-existing properties.

class Address
{
// ...

/**
* @throws Exception
*/
public function __get($name) {
if (property_exists($this, $name)) {
return $this->$name;
}
return throw new Exception('Cannot access property or does not exists.');
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

var_dump($address->state); // 'Albania' - This will `NOT` trigger __get() method.
var_dump($address->city); // 'Tirana' - This `WILL` trigger __get() metdhod.

__isset($name)

Called when the isset() or empty() functions are used to check the existence of an inaccessible (private or protected) or undefined property.

class Address
{
// ...

public function __isset($name) {
return isset($this->$name);
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

// Check if a property is set using __isset
echo isset($address->city) ? 'City is set' : 'City is not set';
echo empty($address->city) ? 'City is empty' : 'City is not empty';

__unset($name)

The same as __isset() is called when you are trying to unset() an inaccessible (private or protected) or undefined property.

class Address
{
// ...

public function __unset($name) {
unset($this->{$name});
}
}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

// Unset a property using __unset
unset($address->city); // Unsetting 'city'.

Method overloading

Method overloading is a concept that allows you to have a method that can perform differently based on its number of parameters. It allows you to have multiple definitions for the same method in the same class.

Unlike other programming languages, PHP doesn’t really let you redeclare a method multiple times, it will throw a PHP Fatal error, but PHP supports method overloading using a magic keyword, __call and __callStatic for static methods.

If an object of that class is called with a method that doesn’t exist then __call($name, $arguments) is called instead of that method.

On the other hand __callStatic($name, $arguments) is triggered when invoking inaccessible methods in a static context.

Definition of a method overloading:

public function __call(string $name, array $arguments);

public static function __callStatic(string $name, array $arguments);

The $name argument is the name of the method being called.
The $arguments argument is an enumerated array containing the parameters passed to the method being called.

NOTE: the value of $name (function name) is case-sensitive.

class Address
{
// ...
public function __call($name, $arguments) {
if ($name === 'format') {
return $this->displayAddress();
}
}

public static function __callStatic($name, $arguments) {
if ($name === 'getDefaultAddress') {
return new Address('123 Main St', 'Meriland', 'US', 1000);
}
}

public function displayAddress(): string
{
return "{$this->street}, {$this->city}, {$this->state}";
}

}

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

// Call a method using __call
echo $address->format();

// Call a static method using __callStatic
$defaultAddress = Address::getDefaultAddress();

NOTE: When using __get, __set, and __call as a way to access the data in your class, keep in mind that this will prevent any sort of autocomplete, highlighting, or documentation that your IDE might do.

Below is the full Address.php class with some simple usage examples.

class Address
{

protected $link;

public function __construct(
public string $street,
protected string $city,
private readonly string $state,
private ?int $zip = null,
private readonly string $type = 'address'
)
{
$this->connect();
dump('Open connection');
}

public function __invoke()
{
// dump('Address object is called as a function!');
}

public function __get($name)
{
dump("Address `$name` property was retrieve using __get");
if (property_exists($this, $name)) {
return $this->$name;
}
return null;
}

public function __set($name, $value)
{
dump("Address `$name` property was __set to: `$value`");
if (property_exists($this, $name)) {
$this->$name = $value;
}
}

public function __isset($name)
{
dump("Address `$name` property checked if __isset");
return isset($this->$name);
}

public function __unset($name)
{
dump("Address `$name` property was __unset");
$this->{$name} = null;
}

public function __toString()
{
dump("Address properties were returned as string using __toString");
return "{$this->type}: {$this->street}, {$this->city}, {$this->state}, {$this->zip}.";
}

public function __clone()
{
dump('Address object called __clone');
trigger_error('Cloning forbidden.', E_USER_ERROR);
}

public function __sleep()
{
dump('Address attributes to serialize, called __sleep');
return array('street', 'zip');
}

public function __wakeup()
{
dump('Reconnect to DB __wakeup');
$this->connect();
}

public function __destruct() {
dump('Close connection to DB and do other cleanups');
$this->closeConnection();
}

public function __call($name, $arguments) {
dump("Calling object method '$name' " . implode(', ', $arguments));
if ($name === 'format') {
return $this->displayAddress();
}
}

public static function __callStatic($name, $arguments) {
dump("Calling static method '$name' " . implode(', ', $arguments));
if ($name === 'getDefaultAddress') {
return new Address('123 Main St', 'Meriland', 'US', 1000);
}
}

private function connect(): void
{
// $this->link = new mysqli('$servername', '$username', '$password');
}

private function closeConnection(): void
{
// $this->link->close();
}

public function displayAddress(): string
{
return "{$this->street}, {$this->city}, {$this->state}";
}

}

Usage examples:

$address = new Address('Blv. Bajram Curri', 'Tirana', 'Albania', 1040);

echo "Full address as string: {$address}\n";
echo "\n <br>";

// No magic method called.
$address->street = 'Blv. Zogu 1';

// Access properties using __set since is protected
$address->city = 'London';

// Access properties using __get
echo "City: {$address->city} \n";
echo "\n <br>";

// Check if a property is set using __isset
echo isset($address->zip) ? 'ZIP is set' : 'ZIP is not set';
echo "\n <br>";

// Convert the object to a string using __toString
echo "Full address as string: {$address}\n";
echo "\n <br>";

// Unset a property using __unset
unset($address->zip);
echo isset($address->zip) ? 'ZIP is set' : 'ZIP is not set';
echo "\n <br>";

// Set object properties using __sleep
$serialized = serialize($address);
echo "Object address serialized: $serialized";
echo "\n <br>";

// Call a method using __call
echo "Formatted address:" . $address->format();
echo "\n <br>";

// Call a static method using __callStatic
$defaultAddress = Address::getDefaultAddress(1);
echo "\nDefault Address: {$defaultAddress}\n";
echo "\n <br>";

// Call __clone method of object.
$newAddress = clone $address;

PS: there are also a couple of other magic methods that are not mentioned in this article which you can check out: __debugInfo(), __set_state(), __serialize(), __unserialize().

Feel free to Subscribe for more content like this 🔔, clap 👏🏻 , comment 💬, and share the article with anyone you’d like

And as it always has been, I appreciate your support, and thanks for reading.

--

--