Decorating service classes
Published by Matthew Daly at 6th December 2018 6:34 pm
I've written before about using decorators to extend the functionality of existing classes, in the context of the repository pattern when working with Eloquent. However, the same practice is applicable in many other contexts.
Recently, I was asked to add RSS feeds to the home page of the legacy project that is my main focus these days. The resulting service class looked something like this:
1<?php23namespace App\Services;45use Rss\Feed\Reader;6use App\Contracts\Services\FeedFetcher;78class RssFetcher implements FeedFetcher9{10 public function fetch($url)11 {12 return Reader::import($url);13 }14}
In accordance with the principle of loose coupling, I also created an interface for it:
1<?php23namespace App\Contracts\Services;45interface FeedFetcher6{7 public function fetch($url);8}
I was recently able to add dependency injection to the project using PHP-DI, so now I can inject an instance of the feed fetcher into the controller by typehinting the interface and having it resolve to the RssFetcher
class.
However, there was an issue. I didn't want the application to make multiple HTTP requests to fetch those feeds every time the page loads. At the same time, it was also a bit much to have a scheduled task running to fetch those feeds and store them in the database, since many times that would be unnecessary. The obvious solution was to cache the feed content for a specified length of time, in this case five minutes.
I could have integrated the caching into the service class itself, but that wasn't the best practice, because it would be tied to that implementation. If in future we needed to switch to a different feed handler, we'd have to re-implement the caching functionality. So I decided it made sense to decorate the service class.
The decorator class implemented the same interface as the feed fetcher, and accepted another instance of that interface in the constructor, along with a PSR6-compliant caching library. It looked something like this:
1<?php23namespace App\Services;45use App\Contracts\Services\FeedFetcher;6use Psr\Cache\CacheItemPoolInterface;78class FetcherCachingDecorator implements FeedFetcher9{10 protected $fetcher;1112 protected $cache;1314 public function __construct(FeedFetcher $fetcher, CacheItemPoolInterface $cache)15 {16 $this->fetcher = $fetcher;17 $this->cache = $cache;18 }1920 public function fetch($url)21 {22 $item = $this->cache->getItem('feed_'.$url);23 if (!$item->isHit()) {24 $item->set($this->fetcher->fetch($url));25 $this->cache->save($item);26 }27 return $item->get();28 }29}
Now, when you instantiate the feed fetcher, you wrap it in the decorator as follows:
1<?php23$fetcher = new FetcherCachingDecorator(4 new App\Services\RssFetcher,5 $cache6);
As you can see, this solves our problem quite nicely. By wrapping our feed fetcher in this decorator, we keep the caching layer completely separate from any one implementation of the fetcher, so in the event we need to swap the current one out for another implementation, we don't have to touch the caching layer at all. As long as we're using dependency injection to resolve this interface, we're only looking at a little more code to instantiate it.
In addition, this same approach can be applied for other purposes, and you can wrap the service class as many times as necessary. For instance, if we wanted to log all the responses we got, we could write a logging decorator something like this:
1<?php23namespace App\Services;45use App\Contracts\Services\FeedFetcher;6use Psr\Log\LoggerInterface;78class FeedLoggingDecorator implements FeedFetcher9{10 protected $fetcher;1112 protected $logger;1314 public function __construct(FeedFetcher $fetcher, LoggerInterface $logger)15 {16 $this->fetcher = $fetcher;17 $this->logger = $logger;18 }1920 public function fetch($url)21 {22 $response = $this->fetcher->fetch($url);23 $this->logger->info($response);24 return $response;25 }26}
The same idea can be applied to an API client. For instance, say we have the following interface for an API client:
1<?php23namespace Foo\Bar\Contracts;45use Foo\Bar\Objects\Item;6use Foo\Bar\Objects\ItemCollection;78interface Client9{10 public function getAll(): ItemCollection;1112 public function find(int $id): Item;1314 public function create(array $data): Item;1516 public function update(int $id, array $data): Item;1718 public function delete(int $id);19}
Now, of course any good API client should respect HTTP headers and use those to do some caching itself, but depending on the use case, you may also want to cache these requests yourself. For instance, if the only changes to the entities stored by the third party API will be ones you've made, or they don't need to be 100% up to date, you may be better off caching those responses before they reach the actual API client. Under those circumstances, you might write a decorator like this to do the caching:
1<?php23namespace Foo\Bar\Services;45use Foo\Bar\Contracts\Client;6use Psr\Cache\CacheItemPoolInterface;78class CachingDecorator implements Client9{10 protected $client;1112 protected $cache;1314 public function __construct(Client $client, CacheItemPoolInterface $cache)15 {16 $this->client = $client;17 $this->cache = $cache;18 }1920 public function getAll(): ItemCollection21 {22 $item = $this->cache->getItem('item_all');23 if (!$item->isHit()) {24 $item->set($this->client->getAll());25 $this->cache->save($item);26 }27 return $item->get();28 }2930 public function find(int $id): Item31 {32 $item = $this->cache->getItem('item_'.$id);33 if (!$item->isHit()) {34 $item->set($this->client->find($id));35 $this->cache->save($item);36 }37 return $item->get();3839 }4041 public function create(array $data): Item42 {43 $this->cache->clear();44 return $this->client->create($data);45 }4647 public function update(int $id, array $data): Item48 {49 $this->cache->clear();50 return $this->client->update($id, $data);51 }5253 public function delete(int $id)54 {55 $this->cache->clear();56 return $this->client->delete($id);57 }58}
Any methods that change the state of the data on the remote API will clear the cache, while any that fetch data will first check the cache, only explicitly fetching data from the API when the cache is empty, and caching it again. I won't go into how you might write a logging decorator for this, but it should be straightforward to figure out for yourself.
The decorator pattern is a very powerful way of adding functionality to a class without tying it to a specific implementation. If you're familiar with how middleware works, decorators work in a very similar fashion in that you can wrap your service in as many layers as you wish in order to accomplish specific tasks, and they adhere to the single responsibility principle by allowing you to use different decorators for different tasks.