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);
}
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);
}
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
}
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);
}
}
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;
}
}
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>
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.
Top comments (0)