Strategy pattern in Symfony

Strategy pattern in Symfony

In general, we use the Strategy Pattern when we can do something in several ways depending on some condition. Suppose we have to create some image resize class using GD library.

We could create some code like this:

class ImageResizer
{
    public function resize(string $filename, string extension, int $newWidth, int $newHeight) 
    {
        switch ($extension) {
            case 'PNG':
                $source = imagecreatefrompng($filename);
                $dest = imagescale($source, $newWidth, $newHeight);
                imagepng($dest, $filename);
                return;
            case 'JPG':
                $source = imagecreatefromjpeg($filename);
                $dest = imagescale($source, $newWidth, $newHeight);
                imagejpeg($dest, $filename);
                return;
            case 'BMP':
                $source = imagecreatefrombmp($filename);
                $dest = imagescale($source, $newWidth, $newHeight);
                imagebmp($dest, $filename);
                return;
            default:
                throw new \Exception('Unhandled extension');
        }
    }

This code will work, but it’s not perfect. Everything is written in the same class a single method. This code does too many things.

First of all let’s create separate classes for each resize strategy.

Here is an easy and clean example for JPG resize strategy.

class ImageJpgResizeStrategy implements ImageResizeStrategyInterface
{
    public function resize(string $filename, int $newWidth, int $newHeight)
    {
        $source = imagecreatefromjpeg($filename);
        $dest = imagescale($source, $newWidth, $newHeight);
        imagejpeg($dest, $filename);
    }
}

The rest strategies can look similar. Because of this, it’s a good moment to create common interface for all these strategies:

interface ImageResizeStrategyInterface
{
    public function resize(string $filename, int $newWidth, int $newHeight);
}

So our image resize class could be changed now:

class ImageResizer
{

    private ImageResizeStrategyInterface $strategy;

    public function resize(string $filename, string $extension, int $newWidth, int $newHeight) 
    {
        switch ($extension) {
            case 'PNG':
                $strategy = new ImagePngResizeStrategy();
                break 
            case 'JPG':
                $strategy = new ImageJpgResizeStrategy();
                break 
            case 'BMP':
                $strategy = new ImageBmpResizeStrategy();
                break 
            default:
                throw new \Exception('Unhandled extension');
        }

        $strategy->resize($filename, $newWidth, $newHeight)
    }

And that’s Strategy Pattern for! We moved our resize logic for each image type somewhere else and made our code clearer!

What about Symfony

But could we get rid of this ugly switch-case construction?

Yes! In Symfony we can use reference tagged servises.

Put in your services.yml these lines

App\SomePathHere\ImageResizeStrategyInterface:
    tags: [ 'image_resize_strategy' ]
App\SomePathHere\ImageResizer:
    arguments:
        - !tagged_iterator { tag: 'image_resize_strategy' }

This will cause that our ImageResizer class will get an array or strategies as a parameter in constructor:

class ImageResizer
{
    private array $strategies;

    public function __construct(iterable $strategies)
    {
        $this->strategies = $strategies instanceof \Traversable ? iterator_to_array($strategies) : $strategies;
    }
   ...

But now all strategies are in a plain array and there is no sign what is each strategy for. To separate them we need some method in each strategy which will give us some information about it

Let’s change our interface and add getImageType method there:

interface ImageResizeStrategyInterface
{
    public function resize(string $filename, int $newWidth, int $newHeight);
    public function getImageType(): string;
}

Then we have to add this method in each strategy. Something like this:

public static function getImageType(): string
{
    return 'JPG'; // Or something else
}

Now we can rewrite our resize method using getImageType function:

public function resize(string $filename, string $extension, int $newWidth, int $newHeight) 
{
    foreach($this->strategies as $strategy) {
        if($strategy->getImageType() === $extension) {
            $strategy->resize($filename, $newWidth, $newHeight);
            return;
        }
    }

    throw new \Exception('Unhandled extension');
}

Or we can add index to the strategies array, just add default_index_method option in your services.yml

  - !tagged_iterator { tag: 'image_resize_strategy', default_index_method: 'getImageType' }

This will make your strategies array indexed. So the resize method of the class can look this way:

public function resize(string $filename, string $extension, int $newWidth, int $newHeight) 
{
    if(!isset($this->stategies[$extension])) {
        throw new \Exception('Unhandled extension');
    }

    $strategy = $this->stategies[$extension];    
    $strategy->resize($filename, $newWidth, $newHeight)
}

Easy reading, clear and beautiful code!

Udostępnij

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

© copyright 2024. All Rights Reserved.