Table of contents
- Introduction
- Building the skeleton
__call()
and__callStatic()
magic methods- PHP's late static binding
- Let's connect the dots
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);
}
}
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';
}
}
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
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';
}
}
}
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
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);
}
}
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);
}
}
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);
}
}
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
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 {
}
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));
}
//...
}
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';
}
}
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
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
Top comments (2)
I understand you want to show how some of the functionality of PHP.
The main problem I see with the example is that the story will never have a good flow, because you are relying on the name of the class for the characters. In your code there has to be an Entity sub class for every character if you want a story to be like;
Steve punched
Wait... an enemy is approaching
Skeleton attacked with Diamond Sword
The solution I would use in this case is to use constructor dependencies.
Weapon.php
The weapon does not need to know who carries it, which means you can have one instance for multiple characters.
It also does not need to know how to do the output.
The Entity is in control of the name and the attack output.
Entity.php
By making the Weapon creation not a part of the Entity, I have an automatic code reduction.
If I want to add a spell as an attack. I could create Power interface with an attack method, and implement the method in Weapon and Spell.
The story code is now;
In the second part of the article, the example is better. But in this case there is a better way.
Hi David! First of all, thank you for your feedback! I've actually to agree with you, probably while coding i would have also structured the thing slightly differently.
This was more kind of an example to show a series of mechanics, and it came to hand to work it out like this.
Next time I'll think on better examples! :)
Thanks again, really appreciated it :D