Intro
In this short article I would like to demonstrate a way to inject dependencies that are not known until runtime. There are many use cases for this and in essence it is about choosing between concrete implementations of some common interface. In object oriented design this is known at the Strategy pattern. The choice itself can be made in various ways, for example via a configuration option or a command line parameter in case of a console command, and I think the dynamic nature of the choice is the most interesting part of the pattern.
A practical example
Let's say we have to process 3rd party data feeds periodically. Some of them will return data in JSON format and some in XML. Let's start by defining the interface for reading the feeds:
interface FeedReaderInterface
{
public function read($url);
}
We have a read() method that requires a $url parameter, which is the url of the remote feed we will read from. Now let's implement (well, simulate) the concrete readers for JSON and XML based feeds:
class JsonFeedReader implements FeedReaderInterface
{
public function read($url)
{
return sprintf('reading JSON data from %s ...', $url);
}
}
class XmlFeedReader implements FeedReaderInterface
{
public function read($url)
{
return sprintf('reading XML data from %s ...', $url);
}
}
Great, now we have our concrete reader strategies. Next, let's create the command itself. For this I will use the Symfony Command component. If you're not familiar with it you can read about it here:
class FeedReaderCommand extends Command
{
private $feedReader;
public function __construct(FeedReaderInterface $feedReader)
{
parent::__construct();
$this->feedReader = $feedReader;
}
protected function configure()
{
$this
->setName('feed-reader')
->setDescription('This command reads remote feeds. Well, it simulates it anyway.')
->addArgument('feedType', InputArgument::REQUIRED, 'The type of the feed.')
->addArgument('feedUrl', InputArgument::REQUIRED, 'The url of the feed.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = $this->feedReader->read($input->getArgument('feedUrl'));
$output->writeln($data);
}
}
Here we have 2 command line arguments, namely "feedType" and "feedUrl". The former is the type of the feed, ie. JSON or XML, the latter is the url. The point is that it is the user who will define these at runtime.
Finally let's create a simple runnable script, namely console.php, that bootstraps this whole thing:
<?php
$console = new Console();
$console->add(new FeedReader\FeedReaderCommand(new FeedReader\Strategies\JsonFeedReader()));
$console->run();
Great, it is time to give it a go:
[pwm@mbp ~]$ app/console feed-reader json "http://foobar.com/feed"
reading JSON data from http://foobar.com/feed ...
All is well! Hm, except there's a small issue:
[pwm@mbp ~]$ app/console feed-reader xml "http://foobar.com/feed"
reading JSON data from http://foobar.com/feed ...
[pwm@mbp ~]$ app/console feed-reader foobar "http://foobar.com/feed"
reading JSON data from http://foobar.com/feed ...
It ignores the "feedType" parameter. Well, not surprising really, given we injected JsonFeedReader, a concrete implementation instead of whatever the user selected. So at the moment we have our strategies, but we don't yet have a way to dynamically choose between them. The problem we face is that we can only access the "feedType" parameter from within the FeedReaderCommand's execute() function hence we cannot use it to choose a reader strategy when instantiating the class. A bit of a catch 22.
Now what if somehow we could capture the computation required to choose a strategy and inject that into FeedReaderCommand? Then, once we possess the value of "feedType" we can execute said computation and the result will be the strategy selected. Sounds a bit cryptic? Let's edit our console.php script:
$readerStrategyResolver = function ($feedType) {
$readerStrategies = [
'json' => FeedReader\Strategies\JsonFeedReader::class,
'xml' => FeedReader\Strategies\XmlFeedReader::class,
];
if (! array_key_exists($feedType, $readerStrategies)) {
throw new \RuntimeException('Not a feed type I can read ...');
}
return new $readerStrategies[$feedType]();
};
$console = new Console();
$console->add(new FeedReader\FeedReaderCommand($readerStrategyResolver));
$console->run();
In PHP, just like in many other languages, functions are first-class citizens, which means, amongst other things, that we can pass then around as values. Here we injected the function $readerStrategyResolver, that requires the parameter $feedType and returns a reader strategy based on the value of $feedType. Eagle-eyed readers will also notice that $readerStrategyResolver is a simple factory function.
For this to work we also need to change FeedReaderCommand (I left out configure() for brevity, as that doesn't change):
class FeedReaderCommand extends Command
{
private $readerStrategyResolver;
private $feedReader;
public function __construct(\Closure $readerStrategyResolver)
{
parent::__construct();
$this->readerStrategyResolver = $readerStrategyResolver;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setFeedReader(call_user_func(
$this->readerStrategyResolver,
$input->getArgument('feedType')
));
$data = $this->feedReader->read($input->getArgument('feedUrl'))
$output->writeln($data);
}
private function setFeedReader(FeedReaderInterface $feedReader)
{
$this->feedReader = $feedReader;
}
}
Our new $readerStrategyResolver function is injected upon instantiation and it encapsulates the computation required to choose a concrete reader strategy. Then in execute(), where we have access to the value of "feedType", we can execute $readerStrategyResolver and pass its return value into setFeedReader(). Notice that using a setter also allows us to type hint and thus ensure that our closure returns an implementation of FeedReaderInterface. Neat!
[pwm@mbp ~]$ app/console feed-reader json "http://foobar.com/feed"
reading JSON data from http://foobar.com/feed ...
[pwm@mbp ~]$ app/console feed-reader xml "http://foobar.com/feed"
reading XML data from http://foobar.com/feed ...
[pwm@mbp ~]$ app/console feed-reader foobar "http://foobar.com/feed"
[RuntimeException]
Not a feed type I can read ...
Click here for an extended version of this example, which used Pimple, a simple dependency injection container.
Conclusion
We managed to create a simple yet robust way to choose between concrete implementations of a common interface at runtime. The above technique can be used anywhere, where we need to resolve dependencies dynamically, based on user input.