The power of magic methods and late static binding

giuliano1993

Giuliano1993

Posted on June 21, 2023

The power of magic methods and late static binding

Table of contents

Introduction

As with every programming language, PHP has its tricks and traps, which can be valuable to acknowledge and master. Lately, while researching with Donato ( author of the second part of this article ) about Laravel's inner mechanics, we found some patterns that really caught our interest, so we decided to go into more detail, and here's the result.

In this article, we will talk about the PHP's __call() and __callStatic() magic methods and about the late static binding feature. We will see how to use them together to make an Object invoke a method from a different class Class, making our OOP coding smoother and without breaking the SOLID principles .

To explain this, we will walk through building some classes, methods, and traits, getting some gaming inspiration from the old-but-gold Minecraft, to have our friendly Steve be able to make some damages thanks to our code ;)

Building the skeleton

Before diving into the hard part, let's first write the skeleton of our application.
Let's start with the Models; first of all, we will need a class called Entity, which is the basis both for our character Steve and for the enemies, and a Weapon class, so we can give our hero some help in fighting the foes ;)

Entity.php

class Entity{

    protected $weapon;
    
    public function setWeapon(string $weaponName){
        $this->weapon  = $this->equipWeapon($weaponName);
        return $this;
    }
    
    public function getWeapon() : Weapon
    {
        return $this->weapon ?? $this->equipWeapon();
    }
    
    private function equipWeapon($weaponName = null): Weapon
    {
        return  new Weapon($weaponName);
    }
    
   

}

Enter fullscreen mode Exit fullscreen mode

Weapon.php


class Weapon
{
    protected $name;

    public function __construct($name = null)
    {
        $this->name = $name ?? 'Bare Hands';
    }

    public function attack():void
    {
        echo $this->name != 'Bare Hands' ? 'Attacked with '.$this->name : 'Punched';
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have these two classes, we come to the point. We want them to communicate, but we also know they have different tasks, roots, and responsibilities, so we don't want to endanger our structure stability by mixing different things up.
At this point we would simply write:

index.php

$entity = new Entity();
$entity->setWeapon('branch'):
$entity->getWeapon()->attack();

//Output: Attacked with branch
Enter fullscreen mode Exit fullscreen mode

Easy peasy, but we can do better!

__call() and __callStatic() magic methods

So, what's so magic in these two methods?

They are triggered when an undefined method is called on the class they're defined. As you can imagine, __callStatic() manage undefined static methods, while __call() is invoked for non-static ones. This means that by using them, we can better handle wrong method calls or, what we're going to do, redirect calls to the correct class.
This can be useful when you're developing, and you want to make classes communicate easily, having a much cleaner code without mixing their functionalities. If you're building a library or a framework (like Laravel ), this helps to make the code style outstanding. Let's see how! Let's add some code to our Entity.php file:

Entity.php

class Entity{

// ...
    public function __call($name, $arguments):void
    {
        if($name == 'attack')
        {               
                echo 'Entity attacked';
        }
    } 
    
}

Enter fullscreen mode Exit fullscreen mode

What's happening here? We defined the __call magic method that, as previously said, will be invoked instead of a not defined method. The $name parameter is the name of the function called, and the $arguments is an array containing the parameters passed to it. So, inside of __call(), we can add some logic to control what happens depending on the method name.
In this first simple step, we just added an echo, so now our index.php can evolve this way.

index.php

$entity = new Entity();
$entity->setWeapon('branch'):
$entity->attack();

//Output: Entity attacked
Enter fullscreen mode Exit fullscreen mode

Though this works nicely, we probably want to call the attack() method from our Weapon class. So, let's introduce an incredibly helpful Trait which we'll see in a more detailed way in the second part of this article, the ForwardCallTrait:

trait ForwardCallTrait
{
    public function forwardCallTo($object, $method, $params)
    {
        return $object->$method($params);
    }
}
Enter fullscreen mode Exit fullscreen mode

The trait is a significantly simplified version of a Laravel Trait, which seamlessly allows calling methods from different classes. As I said, you will explore this trait more deeply in the next part; for now, we focus on the fact that it takes both an object and a method as parameters and lets you perform the call.
So let's make a small arrangement to our Entity.php.

Entity.php

public function __call($name, $arguments):void
    {

        use ForwardCallTrait;

        if($name == 'attack')
        {
               $this->forwardCallTo($this->getWeapon(),$name, $arguments);    
        }
    } 
    
Enter fullscreen mode Exit fullscreen mode

Now, with forwardCallTo(), we are redirecting the call to the Weapon class, so the output from our index.php is now the expected "Attacked with branch".

So, what the __callStatic() method can do to improve even more our code? Let's add it to our Entity.php and check out what happens:

class Entity{

//...

    public static function __callStatic($name, $arguments): mixed
    {
        return (new static)->$name($arguments);
    }
}
Enter fullscreen mode Exit fullscreen mode

When an unknown static method is invoked on an Entity instance, the __callStatic() method is triggered. As for __call(), __callStatic() has the $name and $arguments parameters available. So what we're doing here is creating a new instance of the Entity class (don't worry about the (new static) syntax; we'll talk about it in a minute ;) ), that will be able to call a non-static method: and if it does not belong to the class, we'll end up triggering the __call() method, once again.
So what's the advantage of these twists and turns?
If you go back again to our index.php file, you can actually replace everything you wrote before with this:

Entity::attack();

//Output: Attacked with bare hands
Enter fullscreen mode Exit fullscreen mode

Pretty cool, right? As you will see in the second part, Laravel is filled up with this pattern to improve the Dev Experience and code cleaning.

So there's only one thing left to explain if you're still wondering ;)

PHP's late static binding

What's this (new static)->$name($arguments); about?
Let's first make one step back. We created the Entity Class, representing a Player, a Foe, or anything that can perform an action in our ideal Minecraft-like game.
So we will probably extend it. Let's finally make a simple empty class named after our cubey friend Steve:

class Steve extends Entity {

}
Enter fullscreen mode Exit fullscreen mode

Now a Steve instance can call all methods from the Entity class. The problem is that no instance is created in a static context, so you can't use $this variable either to call non-static methods. That's why we need to create a new instance, and here comes the tricky part.
If you create a new instance with (new self), which could be the first thing coming to mind, the later calls will result as made from an Entity instance instead of Steve because, at the time of invoking the method, there's no instance created yet. Using the (new static) allows this thing to be changed and compute the method using runtime information. So, the result would be that when __call() is invoked, the class of $this would be Steve and not Entity.

To make this more apparent, let's make the latest few adjustments to our classes:
Entity.php


class Entity{

//...
     private function equipWeapon($weaponName = null): Weapon
     {
        return  new Weapon($weaponName, get_class($this));
     }
//...
}

Enter fullscreen mode Exit fullscreen mode

Weapon.php

class Weapon
{
    protected $name;
    protected $bearer;
    public function __construct($name = null, $bearer = null)
    {
        $this->name = $name ?? 'Bare Hands';
        $this->bearer = $bearer;
    }
    public function attack():void
    {
        echo $this->name != 'Bare Hands' ? $this->bearer. 'Attacked with '.$this->name : $this->bearer . 'Punched';
    }
}
Enter fullscreen mode Exit fullscreen mode

index.php


Steve::attack();

echo '<br>';
echo 'Wait... an enemy is approaching';
echo '<br>';

$skeleton = new Entity();
$skeleton->setWeapon('Diamond Sword');
$skeleton->attack();


//output:

//Steve Punched  
//Wait... an enemy is approaching  
//Entity Attacked with Diamond Sword

Enter fullscreen mode Exit fullscreen mode

If you play around with the code ( fully available on github) you can see by yourself that changing (new static) with (new self) would result in both cases above having an Entity instance calling the methods. A pretty peculiar pattern, isn't it?

Let's connect the dots

So, we had a short but intense trip in these few lines of code. Knowing PHP magic methods and patterns can be surprisingly helpful, and even with an easy example, we showed the power of using __call() and __callStatic() methods to handle undefined calls and to address them to other classes. We also learned about PHP's late static binding, which is really worth knowing when coding a lot in OOP, to handle classes and inheritance correctly. In the second part of the article, you'll see how these patterns are applied in Laravel to ease Developer Experience and to create a fully extensible structure, allowing it to be the versatile framework it is.

I hope you enjoyed this first part. Here you can find the second half. ;)

If this article was helpful or want to start a conversation, feel free to reach out in the comments or here @gosty93 and @donato-riccio-wda
We'll be happy for any feedback and ideas.
Happy Coding | _ 0

💖 💪 🙅 🚩
giuliano1993
Giuliano1993

Posted on June 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related