Strategy Pattern in Symfony using attributes
Tags: tech, symfony, strategy, php
Published: 2023-12-04 12:00:00

---
title: Strategy Pattern in Symfony using attributes
published: true
description: 
tags: tech, symfony, strategy, php
published_at: 2023-12-04 12:00
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oicazwdtj1u6hqmve4kj.png
canonical_url: https://alexandrunastase.com/posts/the-joy-of-implementing-strategy-pattern-in-symfony
---

One of the design patterns that I find myself most often implementing is the Strategy Design pattern. Oftentimes it helps decouple logic and allows better separation of concerns.

In previous versions of Symfony, it was of course possible to implement the pattern, but there was a bit more friction involved, like the need to fiddle with yaml files. This has dissapeared meanwhile thanks to the introduction of the attributes in PHP8. Now that we have the `TaggedIterator` and `AutoconfigureTag` attributes at our disposal, it has never been so effortless to implement the strategy pattern using Symfony framework.

In this post, we'll go through a concrete example of how to do this, in which we'll highlight the main components.


## Case Study

Let's suppose we're building a Payment Gateway Software that supports most major credit/debit card providers (Visa, Mastercard, and American Express).

To keep things simple we will ignore lots of important elements like the credit card number, CVV, and so on and of course, there won't be any request to external entities.

**Acceptance criteria:**
  - Create a POST endpoint called `/card-payments` that accepts the following JSON payload:
```json
{
  "amount": "12.22",
  "currency": "EUR",
  "provider": "visa|mastercard|amex"
}
```
- The endpoint should return a JSON response containing a `transactionId` property that has as a prefix the name of the payment provider passed in the input and is followed by some "random" string similar to the example below:
 ```json
 {
  "transactionId": "visa_asd22kjlkoiposd"
 }
 ```
- The assumption is that other logic should be dealt for each provider so generating the transactionId should be separated for each card payment provider

## An example implementation

Note, that a lot of important aspects like validation, exception handling, and code structuring have not been considered in detail as the focus is on the strategy pattern itself.

First we'll start out by building the `CardPaymentController`, where we get the payload, deserialize it to a `CardPaymentDto`, and pass it further to a `CardPaymentService` which is responsible for finding the correct strategy and retrieving the transactionId from the strategy.


```php
...
class CardPaymentController
{
   ...
    #[Route('/card-payments', methods: ['POST'])]
    public function index(Request $request): Response
    {
        $paymentDto = $this->serializer->deserialize($request->getContent(), CardPaymentDto::class, 'json');

        $paymentResponse = $this->cardPaymentService->handleCardPayment($paymentDto);

        return new JsonResponse($this->serializer->serialize($paymentResponse, 'json'), json: true);
    }
}
```

After the controller is created we will implement the strategy interface:

```php
#[AutoconfigureTag(CardPaymentStrategyInterface::class)]
interface CardPaymentStrategyInterface
{
    public function isPaymentProviderSupported(string $paymentProvider);

    public function handlePayment(CardPaymentDto $payment): CardPaymentResponseDto;
}
```
Note the use of the `AutoconfigureTag` here. Usually, it's more common to use a string here as the service tag, but I prefer using the name of the interface to minimize the chance of typos.

As the interface is defined we can start creating the strategy implementations:

```php
class VisaCardPaymentStrategy implements CardPaymentStrategyInterface
{
    public function isPaymentProviderSupported(string $paymentProvider): bool
    {
        return $paymentProvider === PaymentProvider::VISA;
    }

    public function handlePayment(CardPaymentDto $payment): CardPaymentResponseDto
    {
        // Call Visa payment provider
        // More logic

        $transactionId = PaymentProvider::VISA . uniqid();
        return new CardPaymentResponseDto($transactionId);
    }
}
```
and similar for the other payment providers:

```php
class MastercardCardPaymentStrategy implements CardPaymentStrategyInterface
{
    public function isPaymentProviderSupported(string $paymentProvider): bool
    {
        return $paymentProvider === PaymentProvider::MASTERCARD;
    }

    public function handlePayment(CardPaymentDto $payment): CardPaymentResponseDto
    {
        // Call Mastercard payment provider
        // More logic

        $transactionId = PaymentProvider::MASTERCARD . uniqid();
        return new CardPaymentResponseDto($transactionId);
    }
}
```
When the supported strategies are defined we can start implementing the CardPaymentService:

```php
class CardPaymentService implements CardPaymentServiceInterface
{
    /**
     * @param iterable<CardPaymentStrategyInterface> $cardPaymentStrategies
     */
    public function __construct(
        #[TaggedIterator(CardPaymentStrategyInterface::class)]
        private readonly iterable $cardPaymentStrategies,
    ) {
    }

    public function handleCardPayment(CardPaymentDto $paymentDto): CardPaymentResponseDto
    {
        $paymentStrategy = $this->getStrategy($paymentDto->getProvider());

        return $paymentStrategy->handlePayment($paymentDto);
    }

    public function getStrategy(string $paymentProvider): CardPaymentStrategyInterface
    {
        $pickedPaymentStrategy = null;
        foreach ($this->cardPaymentStrategies as $paymentStrategy) {
            if ($paymentStrategy->isPaymentProviderSupported($paymentProvider)) {
                $pickedPaymentStrategy = $paymentStrategy;
            }
        }

        if (is_null($pickedPaymentStrategy)) {
            throw UnsupportedPaymentStrategyException::createForPaymentProvider($paymentProvider);
        }

        return $pickedPaymentStrategy;
    }
}
```
Note the `TaggedIterator` attribute here. It allows us to collect all the classes that were auto-configured using the `CardPaymentStrategyInterface` and have them available as an iterable.
Here we simply find the correct strategy and call it using the `PaymentDto` and return the result of the picked strategy, which should contain the transactionId.


Make sure that service autoconfiguration is enabled in order for this setup work properly.

## Conclusion

Applying the strategy pattern is really straightforward using the latest versions of Symfony and PHP (Symfony 6.4 and PHP 8.2 in the given example). 

Apart from keeping the code much more decoupled, it offers the benefit of keeping the code open to extension but closed to modifications. So for example, if we were to add other payment providers, we wouldn't need to touch any of the existing logic thus avoiding the risk of breaking anything there.

To see the entire implementation feel free to check out the [git repository](https://github.com/alexandrunastase/blog-appendix/tree/main/strategy-pattern). 
Additionally from the code shown above, the repository contains a docker-compose setup and some basic tests that you can play around with.

 ---

### References:
- https://symfony.com/doc/6.4/service_container/tags.html
- https://symfony.com/doc/current/service_container.html#the-autoconfigure-option

> Cross-posted from my blog: [The joy of implementing strategy pattern in Symfony](https://alexandrunastase.com/posts/the-joy-of-implementing-strategy-pattern-in-symfony)