DEV Community

Graham Crocker
Graham Crocker

Posted on • Updated on

Implement a strategy pattern in Magento2

The problem

Do you ever end up creating massive switch statements in functions in order to handle different behavior resulting in lots of logic in one place thats difficult to test or understand?

Take for example a type handler for an entity comprising of $X and $Y with these initial requirements:

If its of Type A then return [$X=>$Y], if its of Type B then return [ $X => [ $Y->getId(), $Y->getContent() ] ].

One could use a simple if statement:

if ($type instanceof A) {
    return [
        $X => $Y
    ]; 
} elseif ($type instanceof B) {
    return [
        $X => [
            $Y->getId(), $Y->getContent()
    ];
} else {
    throw new \InvalidArgumentException(__('Type: %1 is not supported', $type);
}
Enter fullscreen mode Exit fullscreen mode

The above is perfectly acceptable, but what if the requirements change and you had to add several cases, handle errors with 3rd party modules for specific types or handle edge cases? You'd end up with something unmanagable and difficult to read and maintain. Potentially hundreds of lines of If statements, but then again you could improve and use a switch statement like so:

$typeClassName = get_class($type);
switch($typeClassName) {
    case A::class: 
        return [
            $X => $Y
        ]; 
    case B::class:
        return [
            $X => [
                $Y->getId(), $Y->getContent()
        ];
    case C::class:
        if ($X instanceof int) {
            // Do something
        } 
        if (is_array($Y)) {
             //Convert $Y into DataObject
        } 
        return [
            $X => [
                $Y->getId(), $Y->getContent()
        ];

    default:
        throw new \InvalidArgumentException(__('Type: %1 is not supported', $type);
}
Enter fullscreen mode Exit fullscreen mode

Neater, but new requests keep coming in for further logic to add and eventually you'll spend time looking for the type to modify and worse still increase chances of making a mistake and adding a bug. A long convoluted switch statement will dramatically increase that chance, and as such this is a code smell and evidence of poor quality code.

The Solution

We know that data wise, its the same function (give us an entry based on the type of it), but the $type variable requires a different behavior. We can extend these behaviorial differences out into isolated classes limiting the scope of bugs on modification and increasing testability. This is a common design pattern known as the Strategy pattern, but how do we wire it up in something like Magento?

1. Create the Interface

These types all have something in common, a get() function, but we also need a way to discern from other types and one way to do it is test if it is applicable

<?php
namespace \Your\Vendor\Model\TypeMapper;

interface TypeInterface 
{
    public function get($X, $Y): array;

    public function is($type): bool
}
Enter fullscreen mode Exit fullscreen mode

2. Create the Type Mapper

This class sets the context of the behavior, by looping over classes that implements TypeInterface until it gets the one which matches the type and implements the required behavior

namespace \Your\Vendor\Model;

use \Your\Vendor\Model\TypeMapper\TypeInterface;

class TypeMapper
{
    private $arrayOfTypeMappers;

    public function __construct(
        array $arrayOfTypeMappers
    )
    {
        $this->arrayOfTypeMappers = $arrayOfTypeMappers;
    }

    public function get($type, $X, $Y): array
    {
        $type = array_filter($this->arrayOfTypeMappers,
            function ($mapper) use ($fieldType) {
                if ($mapper instanceof TypeInterface) {
                    return $mapper->is($fieldType);
                }
         });
        if (count($type) !== 1) {
            throw new \InvalidArgumentException(__("Type %1 is not supported by %2", $type, self::class));
        }
        return current($type)->get($X, $Y);
    }
}
Enter fullscreen mode Exit fullscreen mode

Wouldn't it be great if you could avoid this and get the classes implementing the Interface like you can do in .NET? get_declared_classes() unfortunetely won't work as PHP lazy loads classes. There is a way to preload specific folders in composer, but its rarely worth doing unless you're building something specific (like a microservice outside of Magento). We'll get to solving this in step 4.

3. Extend the Interface to create a concrete class/s

To keep this short, we're just going to implement one concrete class and add the declared classes in the di component

namespace \Your\Vendor\Model\TypeMapper;
use \Your\Vendor\Model\Types\A as TypeA;
class A implements TypeInterface
{
    public function get($X, $Y): array
    {
        return [
            $X => $Y
        ]; 
    }

    public function is(string $type): bool
    {
       return $type instanceof TypeA;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Add the types to your etc/di.xml

See https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/di-xml-file.html#constructor-arguments for further info

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Your\Vendor\Model\TypeMapper">
        <arguments>
<argument name="arrayOfTypeMappers" xsi:type="array">
                <item name="a" xsi:type="object">
                    Your\Vendor\Model\TypeMapper\A
                </item>
                <item name="b" xsi:type="object">
                    Your\Vendor\Model\TypeMapper\B
                </item>
                <item name="c" xsi:type="object">
                    Your\Vendor\Model\TypeMapper\C
                </item>
                <item name="d" xsi:type="object">
                    Your\Vendor\Model\TypeMapper\D
                </item>
            </argument>
        </arguments>
    </type>
</config>
Enter fullscreen mode Exit fullscreen mode

Step 5. Usage

Add \Your\Vendor\Model\TypeMapper as a property $typeMapper in the client class constructor and use $this->typeMapper>get($field, $X, $Y) to implement this functionality.

If you ever need to add a new type, simply create the class in the relavant directory, make it implement the interface and declare it in the di.xml as per Step 4. Modifying is simple and limited to the types you modify increasing quality of your code.

Give me a shout out if you found this interesting or helpful.

Oldest comments (0)