Skip to content

Instantly share code, notes, and snippets.

@rvvincelli
Created April 16, 2024 12:42
Show Gist options
  • Save rvvincelli/074262ddbcfb506e08fd11b1918a2b77 to your computer and use it in GitHub Desktop.
Save rvvincelli/074262ddbcfb506e08fd11b1918a2b77 to your computer and use it in GitHub Desktop.

Traits and conflicts

Conflicting conflicts

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.

Image by <a href="https://pixabay.com/users/digipictures-2242655/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1350560">digipictures</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1350560">Pixabay</a>

Let's consider the following traits:

How

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:

Image by <a href="https://pixabay.com/users/clker-free-vector-images-3736/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=311237">Clker-Free-Vector-Images</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=311237">Pixabay</a>

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.

Usage

$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.

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 traits. Let's try to make it a bit shorter in the second part.

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

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;
        }
    }
}

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::classclass. 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

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:

  • 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();
		}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment