At the time Symfony 2 reached the public, The Symfony Service Container was THE Symfony 2 feature and yet it is one of the most powerful and central component for the framework. The Service Container is the place where dependency injection and configuration happens, making life really easy for developers who love decoupled and reusable code. And design patterns too. Infact, as you probably know, the Service Container is not only a service locator (ContainerAwareInterface is your enemy, isn't it?): it is a switchboard, where each line(object) is properly connected (dependency injection) to another in order to communicate (through a common interface). Even more: the Service Container is the place where some well known design patterns can be implemented with a few lines of configuration. 

One of the design patterns that we can use to understand how to do it is The Chain of Responsibility.

The Chain of Responsibility is a behavioural pattern and can be summarized like this:

In object-oriented design, the chain-of-responsibility pattern is a design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain.

— Wikipedia

In a few words: a processing object arrives to the chain and the buck is passed until some of the configured objects are able to process it. This way a complex, multi-format processing system can be easily implemented and extended. Our example will be based on a multi-format parser: we will implement a parser that through a chain of responsibility will handle json and xml parsing and see how it will be easy to add another format later.

First, we need to create the main Chain class:

<?php
 
namespace SpaghettiDashboardBundleParser;
 
class ParserChain 
{
    protected $first = null;
 
    public function __construct(array $parsers)
    {
        $current = null;
 
        foreach ($parsers as $parser) {
            if (is_null($this->first)) {
                $this->first = $parser;
            }
 
            if (!is_null($current)) {
                $current->setNext($parser);
            }
            $current = $parser;
        }
    }
 
    public function parse(SplFileObject $file)
    {
        if ($this->first) {
            return $this->first->parse($file);
        }
        throw new RuntimeException("Empty chain!");
    }
}

this class will keep a pointer to the first element of the chain and will be injected with an array of instanced parsers with which the full chain will be created. This will be the only class we'll need to inject in other classes.

Let's define the classes that will be used to create the chain. Since they are going to have a common interface and some common logic, we will create an AbstractParser class:

<?php
 
namespace SpaghettiDashboardBundleParser;
 
abstract class Parser
{
    protected static $format;
    protected $next;
 
    public function setNext(Parser $next)
    {
        $this->next = $next;
    }
 
    protected function canHandle(SplFileObject $file)
    {
        return $file->getExtension() == static::$format;
    }
 
    public function parse(SplFileObject $file)
    {
        if ($this->canHandle($file)) {
            return $this->doParse($file);
        } else {
            if (is_null($this->next)) {
                throw new InvalidArgumentException($file->getExtension(). " extension is not supported");
            }
            return $this->next->parse($file);
        }
    }
 
    abstract public function doParse(SplFileObject $file);
}

Notice that here, for simplicity, we will not overcomplicate the canHandle method, it will just check for a format static attribute, overwritten by each of the concrete classes.

And now the parsers, quite self-explaing:

<?php
 
namespace SpaghettiDashboardBundleParser;
 
class JsonParser extends Parser
{
    public static $format = "json";
 
    public function doParse(SplFileObject $file)
    {
        //parse the file
    }
}
<?php
 
namespace SpaghettiDashboardBundleParser;
 
class XmlParser extends Parser
{
    public static $format = "xml";
 
    public function doParse(SplFileObject $file)
    {
        //parse the file
    }
}

We need to put things together now, using the symfony DIC:

The parsers have been declared as private services, so that their instances cannot be taken from the code, and they have been injected has an array in the ParserChain constructor.

That's it, now we can use our chain in our code getting the chain service from the container (or, better, injecting it where needed) and calling its parse method:

services:
    spaghetti_dashboard.parser.json:
        class: SpaghettiDashboardBundleParserJsonParser
        public: false
 
    spaghetti_dashboard.parser.xml:
        class: SpaghettiDashboardBundleParserXmlParser
        public: false
 
    spaghetti_dashboard.parser_chain:
        class: SpaghettiDashboardBundleParserParserChain
        arguments:
            - [@spaghetti_dashboard.parser.json, @spaghetti_dashboard.parser.xml, @spaghetti_dashboard.parser.csv]

Adding a new parser is quite easy. All we need to do is defining the new class:

<?php
 
namespace SpaghettiDashboardBundleController;
 
use SymfonyBundleFrameworkBundleControllerController;
 
class ParserController extends Controller
{
    public function indexAction()
    {
        //[...]
        $file = new SplFileObject(__DIR__ . "/assets/test.xml");
        $chain = $this->get("spaghetti_dashboard.parser_chain");
        $parsed = $chain->parse($file);
 
        //[...]
    }
}

and updating out services.yml file:

<?php
 
namespace SpaghettiDashboardBundleParser;
 
class CsvParser extends Parser
{
    public static $format = "csv";
 
    public function doParse(SplFileObject $file)
    {
        //parse the file
    }
}

Possible improvements:

Add an append function to the ParserChain to add parsers at runtime.

Use a DIC tag to identify a Parser service and automatically inject it in the chain in a CompilerPass

Giorgio Cefaro

Freelance Software Engineer , Website , Git home page , @giorrrgio , Linkedin profile
Software Engineer, PHP specialist, coach, public speaker, open source developer and enthusiast, works in the IT field since 1999. TDD addicted, DDD worshipper

All articles by Giorgio Cefaro

Comments

comments powered by Disqus

cloudparty

Follow Us