PHP supports Traits, and like in many other programming languages they can be stacked and composed. In a scenario where one class uses two different traits with the same method signatures, conflicts may arise.
In the domain of our main application here at Sharesquare, we had a working class for one kind of model, let's call it Blue, and another one for a different kind, let's call it Red. When it was time to introduce Magenta, which was a combination of Red and Blue with the Red nature prevailing whenever a single choice had to be made, we thought about making two traits out of these classes and defining three separate color classes. However, when Magenta had to use
both the Red and Blue traits, that's when hell started to break loose.
Let's consider the following traits:
Trait 1: Blue.php
namespace App\Traits;
trait Blue
{
public function greeting()
{
echo "Hello Github.\nBlue Here.";
}
}
Trait 2: Red.php
namespace App\Traits;
trait Red
{
public function greeting()
{
echo "Hello Github.\nRed Here.";
}
}
Now let's combine these two traits and introduce the new Magenta class. At the same time, we will try and solve the collision problem, a problem that arises due the fact that the same method signature name is shared from both Blue and Red:
class Magenta
{
use Red, Blue {
Red::greeting as protected redGreeting;
Blue::greeting as protected blueGreeting;
}
public function greetingFromBlue()
{
$this->blueGreeting();
// override here...
}
public function greetingFromRed()
{
$this->redGreeting();
// override here...
}
public function greeting()
{
$this->greetingFromBlue();
echo "\n";
$this->greetingFromRed();
}
}
Here, we have used different aliases for both traits, which will allow us to differentiate which trait method to call:
use Red, Blue {
Red::greeting as protected redGreeting;
Blue::greeting as protected blueGreeting;
}
We can use the local methods of the Magenta::class
class to override trait functionality or use combined functionality by defining local methods.
$magenta = new Magenta();
$magenta->greetingFromBlue();
$magenta->greetingFromRed();
$magenta->greeting();
Let's explore a better approach, this time pushing forward a little theory with a couple of services.
Naming overrides as a solution just works, but defining aliases can be a bit messy, especially if we have a long list of methods in traits. Let's try to make it a bit shorter in the second part.
We'll continue with the same sample problem.
Our first service, just to keep the naming simple (at the cost of a little less fantasy) will be: RedService.php
namespace App\Http\Services;
class RedService
{
public function greeting()
{
echo "Hello Github.\nRed Here.";
}
}
And, of course: BlueService.php
namespace App\Http\Services;
class BlueService
{
public function greeting()
{
echo "Hello Github.\nBlue Here.";
}
}
Now, let's introduce the new Magenta::class
class, which combines the flavors of the blue and red pill services:
class Magenta
{
protected $red;
protected $blue;
public function __construct()
{
$this->red = new RedService();
$this->blue = new BlueService();
}
public function __call($functionName, $arg = [])
{
$pillType = $arg[0] ?? null;
switch ($pillType) {
case 'red':
$this->red->{$functionName}();
break;
case 'blue':
$this->blue->{$functionName}();
break;
default:
// combine - use both
$this->red->{$functionName}();
$this->blue->{$functionName}();
break;
}
}
}
Let's test it live:
$new = new Magenta();
$new->greeting('red');
$new->greeting('blue');
$new->greeting();
In this approach, we have used the magic function __call($functionName, $arg = [])
inside the Magenta::class
class. It accepts the first parameter as the function name and the second parameter can be any arguments that we want to pass to this function.
These magic methods provide flexibility and control over the behavior of objects in PHP. By implementing these methods in your classes, you can define custom actions for various object interactions and events. Here, we are using __call($method, $arguments)
, which is invoked when an inaccessible or non-existent method is called on an object. It allows us to handle and respond to undefined method calls. For more information, refer to the official PHP documentation on __call magic method in PHP ref
Using the above magic methods can make our class much cleaner, and you can call any methods on Magenta
from the red or blue service just like you call methods on original class methods. You can also override functions by declaring a local method inside the Magenta
class for future calls to the relevant pill method.
This approach provides much cleaner code, but adding proper validation or exception handling could make it even better. For example, let's assume some of the methods are not common between both pills. Here's how we can improve it:
- Have two additional properties:
protected $redMethods;
protected $blueMethods;
- Create a list of available methods in an array by adding a method in the
Magenta::class
class. You can call this method in the__construct
function:
public function initials()
{
$this->redMethods = get_class_methods($this->red);
$this->blueMethods = get_class_methods($this->blue);
}
- Check if the methods exist for both pill services:
if (!in_array($this->redMethods, array_keys($this->functions))) {
throw new BadMethodCallException();
}
With PHP 8.3, an #[\Override]
modifier is introduced for functions: would you improve the snippets above, thanks to this classic override statement? And if yes, how?
No worry if you have no answer yet, perhaps we will give you ours in a future blog post!
Blog by Riccardo Vincelli and Usama Liaquat brought to you by the engineering team at Sharesquare.