DEV Community

Marat Latypov
Marat Latypov

Posted on • Edited on

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');
        }
    }
Enter fullscreen mode Exit fullscreen mode

This code will work, but it's not perfect. Everything is written in the same class in 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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

Put in your services.yml these lines

_instanceof:
    App\SomePathHere\ImageResizeStrategyInterface:
        tags: [ 'image_resize_strategy' ]

App\SomePathHere\ImageResizer:
    arguments:
        - !tagged_iterator { tag: 'image_resize_strategy' }
Enter fullscreen mode Exit fullscreen mode

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;
    }
   ...
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

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');
}
Enter fullscreen mode Exit fullscreen mode

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' }
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Easy reading, clear and beautiful code!

Top comments (1)

Collapse
 
christophelouis profile image
christophe-spiteri

Salut

Sur symfony 6 dans services.yaml il faut mettre _instanceof:

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