The Strategy Pattern with Symfony

Typically, the Strategy Pattern lends itself well to situations where we have a number of processes or algorithms (or strategies) that we can select from at runtime based on some criteria.

Strategy Pattern UML Diagram

As with almost all software engineering problems, there are a number of ways in which we can implement this pattern with the tools available. I'll describe a method that uses Symfony's DependencyInjection component here.

The Strategy Interface

We start by defining an interface for our strategy classes to implement. The interface will have two methods:

  • canProcess, which will return true if the strategy is able to process the provided data, or false if not.
  • process, which will process the provided data.
interface StrategyInterface  
{
    public function canProcess($data);
    public function process($data);
}

The Context Class

The context class is responsible for choosing a suitable strategy from the available ones. In my example, the individual strategies decide if they are suitable. I like this approach because it keeps the context class isolated from any complex decision logic; though if your suitability criteria is simple enough, that logic could be made to live in the context class instead.

class Context  
{
    private $strategies = array();

    public function addStrategy(StrategyInterface $strategy)
    {
        $this->strategies[] = $strategy;
    }

    public function handle($data)
    {
        foreach ($this->strategies as $strategy) {
            if ($strategy->canProcess($data)) {
                return $strategy->process($data);
            }
        }
        return $data;
    }
}

The context class is quite simple, we have private field strategies, containing the instances of our strategies that we will use at runtime; then two methods:

  • addStrategy - a simple adder for providing new strategies to our context.
  • handle - our entry point for the strategy pattern which finds a suitable strategy and uses it to process then return the provided data.

The Strategies

We can define as many strategies as we like.

class ShortStrategy implements StrategyInterface  
{
    public function canProcess($data)
    {
        return strlen($data) < 10;
    }

    public function process($data)
    {
        return $data;
    }
}

And another one...

class LongStrategy implements StrategyInterface  
{
    public function canProcess($data)
    {
        return strlen($data) >= 10;
    }

    public function process($data)
    {
        return substr($data, 0, 10) . '...';
    }
}

About DependencyInjection

If you're familiar with Symfony's DependencyInjection component already, this section might be old news to you, so feel free to skip on ahead.

Symfony's DependencyInjection component provides some great tooling to do this with minimal effort and in a flexible, configurable way.

The DependencyInjection component works by defining services within configuration files. These services are just objects - instances of classes - that each have a name assigned to them which can be used to request the service from within your application.

When a service is first requested, the arguments for that service are provided to the constructor to obtain the service instance. Arguments can be literal values, variable parameters, references to other services or even expressions now. In addition, you can specify calls - method calls to make on the service instance after it is constructed. Services can also be tagged to group related services together.

When the application initially starts up (I.e. with cold caches), the configuration files are parsed into definitions the container is compiled by a number of built-in passes - one resolves the arguments for each service, another detects any circular dependencies, and at the end a PHP file containing the complete Container class is dumped into the application cache. This container class is then used on each page load of the application rather than recompiling the entire configuration into a new container on each page load.

Services

I mentioned above how the container compilation process is made up of built-in passes. Well, we're actually able to write our own passes that provide us with an opportunity to manipulate the container before it is dumped too.

We'll use this capability, combined with the tagging and automatic method calling functionality of the Symfony DependencyInjection component to automatically register each of our strategy instances with the context instance at initialisation time.

First we need to register our context class as a service:

services:  
  context:
    class: Context

Then each of our strategies:

services:  
  long_strategy:
    class: LongStrategy
    tags:
      - { name: strategy }
  short_strategy:
    class: ShortStrategy
    tags:
      - { name: strategy }

Note that we tagged the strategies with the strategy service tag.

Adding a Compiler Pass

A Compiler Pass is simply a class that implements the CompilerPassInterface interface from the Symfony\Component\DependencyInjection\Compiler namespace. The interface requires just one method implementation:

  • process, which accepts a ContainerBuilder instance. This method is run when the Compiler Pass is executed during the compilation of the container.

Our compiler pass needs to register each of our strategies with the context service:

class StrategyCompilerPass implements CompilerPassInterface  
{
    public function process(ContainerBuilder $container)
    {
        // Find the definition of our context service
        $contextDefinition = $container->findDefinition('context');

        // Find the definitions of all the strategy services
        $strategyServiceIds = array_keys(
            $container->findTaggedServiceIds('strategy')
        );

        // Add an addStrategy call on the context for each strategy
        foreach ($strategyServiceIds as $strategyServiceId) {
            $contextDefinition->addMethodCall(
                'addStrategy',
                array(new Reference($strategyServiceId))
            );
        }
    }
}

If we're using bundles in the full-stack Symfony framework, we can register our Compiler Pass with the container in our Bundle class by adding a method call to our Bundle class' build method:

$container->addCompilerPass(new StrategyCompilerPass());

Now when our bundle is registered with the kernel, our compiler pass will be added to the compile process for the container automatically.

The Result

We can test our implementation from any controller, typically by injecting the context service into the controller's service as an argument; or if we're just doing a quick test, we can retrieve it from the container by calling $this->get('context').

$context = $this->get('context');
print $context->handle('hello');  // hello  
print $context->handle('hello world');  // hello worl...  

Adding another strategy is really easy too - just write the class, implementing the StrategyInterface interface and create a service for it, tagged with the strategy tag and next time the container is compiled, it'll be made available to the context class automatically each time the context service is retrieved from the container.

If you'd like to have a play with the code, I've set up a GitHub repository containing a Symfony Standard Edition installation with the exact code seen here running.

Variations

The pattern can be adapted and modified depending on the specific requirements that we're working with.

For example, the logic of the context class could be altered to select multiple acceptable strategies and try all of them, or try a number of acceptable strategies until a successful outcome is obtained. A variation of this pattern is often applied in situations where we need to have multiple parties vote on the state of an entity or value - for instance in Symfony's Security component, Voters can be used to vote on a user's ability to access a resource.


comments powered by Disqus