Traits and conflicts
Conflicting traits
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.
Sample problem
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.
How
Let’s consider the following traits: Blue.php
, Red.php
.
namespace App\Traits;
trait Blue
{
public function greeting()
{
echo "Hello Github.\nBlue Here.";
}
}
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 to override trait functionality or use combined functionality by defining local methods.
Usage
$magenta = new Magenta();
$magenta->greetingFromBlue();
$magenta->greetingFromRed();
$magenta->greeting();
Sample 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 the traits. Let’s explore a better approach, this time pushing forward a little theory with a couple of services.
We’ll continue with the same sample problem.
How
Our first service, just to keep the naming simple (at the cost of a little less fantasy) will be: RedService.php
php
namespace App\Http\Services;
class RedService
{
public function greeting()
{
echo "Hello Github.\nRed Here.";
}
}
And, of course: BlueService.php
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;
}
}
}
Usage
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. 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.
Thoughts?
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.
Room for improvement
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:
public function initials()
{
$this->redMethods = get_class_methods($this->red);
$this->blueMethods = get_class_methods($this->blue);
}
Then, check if the methods exist for both pills:
if (!in_array($this->redMethods, array_keys($this->functions))) {
throw new BadMethodCallException();
}
Some more elegance
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.