Strategy pattern in Symfony
Tags: php, symfony
Published: 2023-09-29 14:37:45

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:

```php
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 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. 
```php
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:

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

So our image resize class could be changed now:

```php
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 services](https://symfony.com/doc/current/service_container/tags.html#reference-tagged-services). 

Put in your `services.yml` these lines
```yaml
_instanceof:
    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: 

```php
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:
```php
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:
```php
public static function getImageType(): string
{
    return 'JPG'; // Or something else
}
```

Now we can rewrite our resize method using getImageType function: 

```php
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`
```yaml
  - !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:

```php
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!