cult3

What are PHP Magic Methods?

Apr 16, 2014

Table of contents:

  1. Working within a context
  2. Construct and Destruct
  3. Getting and Setting
  4. Checking to see if a property is set
  5. Unsetting a property
  6. To String
  7. Sleep and Wakeup
  8. Call
  9. Cloning
  10. Invoke
  11. Conclusion

If you have ever looked at the source code of open source PHP projects, you might have noticed object methods that begin with a double underscore.

These are Magic Methods that allow you to react to certain events when using these particular objects. This means when certain things happen to your object, you can define how it should react in that instance.

You’ve probably already come across a number of PHP’s magic methods already because some of them are really quite common. However, I believe in order to be a competent PHP developer, you do need a firm grasp of the tools you have at your disposal when working with Object Oriented PHP.

With that in mind, this week’s tutorial is all about PHP’s Magic Methods.

Working within a context

When I was learning this stuff, I used to find tutorials that either gave stupid examples, or no example at all not much help. I think in order to really understand something you need to learn about it in the context of the real world.

So with that in mind, here is the context for this tutorial.

Imagine that we’re pulling tweets from Twitter’s API. We pull a JSON payload of the current user’s tweets and we want to turn each tweet into an entity object to work with.

Here is the basic Tweet class:

class Tweet
{
}

Now that we have this context set up, I’ll walk through each of PHP magic methods to show you how they could be applied to this object within our system.

Construct and Destruct

The most common of PHP’s magic method is the __construct() method. If you’ve been following along with the Building Cribbb series I’m sure you are well aware of this method already.

The __construct() method is automatically called when the object is first created. This means you can inject parameters and dependancies to set up the object.

For example:

public function __construct($id, $text)
{
    $this->id = $id;
    $this->text = $text;
}

$tweet = new Tweet(123, 'Hello world');

When you create a new instance of the Tweet object, you can pass in parameters that will be injected into the __construct() method. As you can see, you don’t actually have to call the method as it will be automatically called for you.

When you extend an object, the parent object will sometimes also have a __construct() method that is expecting something to be injected into the object. You satisfy this requirement by calling the parent’s __construct() method:

class Entity
{
    protected $meta;

    public function __construct(array $meta)
    {
        $this->meta = $meta;
    }
}

class Tweet extends Entity
{
    protected $id;
    protected $text;

    public function __construct($id, $text, array $meta)
    {
        $this->id = $id;
        $this->text = $text;

        parent::__construct($meta);
    }
}

When an object is destroyed the __destruct() method is fired. Again, as with the __construct() method, this is not something you have to trigger as PHP will take care of it for you.

The destruct method will allow you to cleanup anything that shouldn’t be around once the object has been destroyed. For example this could be a connection to an external service or database.

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

To be honest you don’t really see much of the __destruct() method. PHP is not really the type of language that you would be using for long running processes so I think for the most part you don’t really need to ever clean up anything using the __destruct() method. The lifecycle of a PHP request is so short, actually implementing the __destruct() method is probably more hassle than it’s worth.

Getting and Setting

When working with objects in PHP, you will often want to access the properties of the object like this:

$tweet = new Tweet(123, "hello world");
echo $tweet->text; // 'hello world'

However, generally speaking, the properties on an object will be set to protected and so attempting to access a property in this way will cause an error.

The __get() method will listen for requests for properties that are not public properties:

public function __get($property)
{
    if (property_exists($this, $property)) {
        return $this->$property;
    }
}

The __get() method accepts the name of the property you were looking for as an argument. In the code above, first I check to see if the property exists on the current object. If it does, I can return it dynamically from the object.

You don’t have to actually call the __get() method as PHP will automatically call it for you when you try to access a property that is not one of the object’s public properties.

If you try to set a property that is not accessible, the __set() magic method will be triggered. This method takes the property you were attempting to access and the value you were trying to set as two arguments.

If you wanted to allow this functionality on your object, you could do something like this:

public function __set($property, $value)
{
    if (property_exists($this, $property)) {
        $this->$property = $value;
    }
}

$tweet->text = 'Setting up my twttr';
echo $tweet->text; // 'Setting up my twttr'

In these two examples I’ve shown how you can get or set properties on an object that are not set to public. Accessing properties like this on an object isn’t always a great idea. It’s much better to have defined getter and setter methods that form a consistent API. This means if your properties change, any code that uses this object won’t be immediately broken.

You’ll often also see __get() and __set() methods that dynamically call a getter or setter method on the object. This is a good solution if you wanted to mutate or add some kind of logic to the process.

Checking to see if a property is set

If you are familiar with PHP, you’ve probably already encountered the isset() function when working with arrays. You can also use this function on objects to see if a publicly accessible property has been set.

If you attempt to use this function to check the presence of a property that isn’t publicly accessible, you can use the __isset() magic method to respond:

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

isset($tweet->text); // true

As you can see above, the __isset() method will catch the call to the inaccessible property and accept it as an argument. You can then run the isset() function from inside the object to return the correct boolean value.

Unsetting a property

Similar to the isset() function, the unset() function is commonly used when working with arrays. Again, as with the isset() function, you can also run it on an object to unset public properties. If the property you are trying to unset is not public, the __unset() method will catch the request and allow you to implement it from within the class:

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

To String

The __toString() method allows you to return the object as a string representation.

For example:

public function __toString()
{
    return $this->text;
}

$tweet = new Tweet(1, 'hello world');
echo $tweet; // 'hello world'

This means whenever you try to cast the object as a string, such as trying to use echo, the object will be returned however you define it in the __toString() method.

A good example of this is whenever you return one of Laravel’s Eloquent Models in a controller, you will be returned the object as json. This works by defining the __toString() method to return a json representation of the object. To see how Laravel implements it, take a look at the source code.

Sleep and Wakeup

The serialize() function is a pretty common way to store a representation of an object. For example, if you wanted to store an object in the database, first you would serialise it, store it, and then when you wanted it again you would unserialise it.

The __sleep() method allows you to define which properties of the object should be serialised as you probably don’t want to serialise any kind of external object that might not be relevant when you unserialise the object.

For example, imagine if when we create a new entity, we also need to provide a storage mechanism:

$tweet = new Tweet(
    123,
    "Hello world",
    new PDO("mysql:host=localhost;dbname=twttr", "root")
);

When we serialise this tweet, we don’t want to also serialise the database connection because it won’t be relevant in the future.

The __sleep() method is therefore simply an array of properties that should be serialised:

public function __sleep()
{
    return array('id', 'text');
}

When it comes to unserialising the object, we need to establish the object into it’s correct state. In this example I need to re-establish the database connection, but in reality this could mean setting up anything that the object should be aware of. You can do this using the __wakeup() magic method:

public function __wakeup()
{
    $this->storage->connect();
}

Call

The __call() method will pick up when you try to call a method that is not publicly accessible on the object. For example you might have an array of data on the object that you want to mutate before returning:

class Tweet extends {

    protected $id;
    protected $text;
    protected $meta;

    public function __construct($id, $text, array $meta)
    {
        $this->id = $id;
        $this->text = $text;
        $this->meta = $meta;
    }

    protected function retweet()
    {
        $this->meta['retweets']++;
    }

    protected function favourite()
    {
        $this->meta['favourites']++;
    }

    public function __get($property)
    {
        var_dump($this->$property);
    }

    public function __call($method, $parameters)
    {
        if (in_array($method, array('retweet', 'favourite'))) {
            return call_user_func_array(array($this, $method), $parameters);
        }
    }
}

$tweet = new Tweet(123, 'hello world', array('retweets' => 23, 'favourites' => 17));

$tweet->retweet();
$tweet->meta; // array(2) { ["retweets"]=> int(24) ["favourites"]=> int(17) }

Another common scenario is when you have an injected object that forms part of the publicly accessible api:

class Location
{
    protected $latitude;
    protected $longitutde;

    public function __construct($latitude, $longitude)
    {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
    }

    public function getLocation()
    {
        return [
            "latitude" => $this->latitude,
            "longitude" => $this->longitude,
        ];
    }
}

class Tweet
{
    protected $id;
    protected $text;
    protected $location;

    public function __construct($id, $text, Location $location)
    {
        $this->id = $id;
        $this->text = $text;
        $this->location = $location;
    }

    public function __call($method, $parameters)
    {
        if (method_exists($this->location, $method)) {
            return call_user_func_array(
                [$this->location, $method],
                $parameters
            );
        }
    }
}

$tweet = new Tweet(
    123,
    "Hello world",
    new Location("37.7821120598956", "-122.400612831116")
);

var_dump($tweet->getLocation());
// array(2) { ["latitude"]=> string(16) "37.7821120598956" ["longitude"]=> string(17) "-122.400612831116" }

In the example above, we can call the getLocation method on the Tweet object, but actually delegate the method call to the injected Location object.

If the method you are trying to call is static, you can use the __callStatic() magic method instead. This method is exactly the same as the __call() method but it will be invoked on static method calls instead of regular method calls.

Cloning

When you make a copy of an object in PHP, it is still linked to the original object. This means if you make a change to the original object, your copied object will also be changed:

$sheep1 = new stdClass();
$sheep2 = $sheep1;

$sheep2->name = "Polly";
$sheep1->name = "Dolly";

echo $sheep1->name; // Dolly
echo $sheep2->name; // Dolly

This is because when you copy an object in PHP, it is “passed as a reference”, meaning, it maintains a link to the original object.

In order to create a clean copy of the object, we must use the clone keyword;

$sheep1 = new stdClass();
$sheep2 = clone $sheep1;

$sheep2->name = "Polly";
$sheep1->name = "Dolly";

echo $sheep1->name; // Dolly
echo $sheep2->name; // Polly

However, if we have objects that are injected into the object, those dependencies will be passed as a reference:

class Notification
{
    protected $read = false;

    public function markAsRead()
    {
        $this->read = true;
    }

    public function isRead()
    {
        return $this->read == true;
    }
}

class Tweet
{
    protected $id;
    protected $text;
    protected $notification;

    public function __construct($id, $text, Notification $notification)
    {
        $this->id = $id;
        $this->text = $text;
        $this->notification = $notification;
    }

    public function __call($method, $parameters)
    {
        if (method_exists($this->notification, $method)) {
            return call_user_func_array(
                [$this->notification, $method],
                $parameters
            );
        }
    }
}

$tweet1 = new Tweet(123, "Hello world", new Notification());
$tweet2 = clone $tweet1;

$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // true

To fix this problem we can use the __clone() method to clone any injected objects whenever the clone event happens on the parent object:

class Tweet
{
    protected $id;
    protected $text;
    protected $notification;

    public function __construct($id, $text, Notification $notification)
    {
        $this->id = $id;
        $this->text = $text;
        $this->notification = $notification;
    }

    public function __call($method, $parameters)
    {
        if (method_exists($this->notification, $method)) {
            return call_user_func_array(
                [$this->notification, $method],
                $parameters
            );
        }
    }

    public function __clone()
    {
        $this->notification = clone $this->notification;
    }
}

$tweet1 = new Tweet(123, "Hello world", new Notification());
$tweet2 = clone $tweet1;

$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // false

Invoke

The __invoke() magic method allows you to use an object as if it were a function:

class User
{
    protected $name;
    protected $timeline = [];

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addTweet(Tweet $tweet)
    {
        $this->timeline[] = $tweet;
    }
}

class Tweet
{
    protected $id;
    protected $text;
    protected $read;

    public function __construct($id, $text)
    {
        $this->id = $id;
        $this->text = $text;
        $this->read = false;
    }

    public function __invoke($user)
    {
        $user->addTweet($this);
        return $user;
    }
}

$users = [new User("Ev"), new User("Jack"), new User("Biz")];
$tweet = new Tweet(123, "Hello world");
$users = array_map($tweet, $users);

var_dump($users);

In this example I’m able to pass the $tweet object as a callback into the array_map function. The array_map will then iterate over the array of users and use the $tweet object as a function. In this case each Tweet will be added to the user’s timeline.

This is a bit of a contrived example, but hopefully you’ll see the opportunities that being able to pass an object as a function would create.

Conclusion

As you can see from each method description above, the PHP magic methods are used to react to different events and scenarios that your object might find itself in. Each of these magic methods are triggered automatically, so in essence, you are just defining what should happen under those circumstances.

For a long time I didn’t realise that PHP magic methods worked in this way. I thought they were just handing methods for doing interesting things with PHP objects. I think when you realise what the magic methods are intended for, it enables you to write objects that are much more powerful as part of a bigger application.

Hopefully each of the examples I’ve presented in this tutorial are realistic and show how you would use the methods in your day to day coding. I’ll be the first to admit it is annoying when someone explains something to you, but does not explain what are the practical implications in the real world.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.