Decorating service classes

Published by 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<?php
2
3namespace App\Services;
4
5use Rss\Feed\Reader;
6use App\Contracts\Services\FeedFetcher;
7
8class RssFetcher implements FeedFetcher
9{
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<?php
2
3namespace App\Contracts\Services;
4
5interface FeedFetcher
6{
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<?php
2
3namespace App\Services;
4
5use App\Contracts\Services\FeedFetcher;
6use Psr\Cache\CacheItemPoolInterface;
7
8class FetcherCachingDecorator implements FeedFetcher
9{
10 protected $fetcher;
11
12 protected $cache;
13
14 public function __construct(FeedFetcher $fetcher, CacheItemPoolInterface $cache)
15 {
16 $this->fetcher = $fetcher;
17 $this->cache = $cache;
18 }
19
20 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<?php
2
3$fetcher = new FetcherCachingDecorator(
4 new App\Services\RssFetcher,
5 $cache
6);

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<?php
2
3namespace App\Services;
4
5use App\Contracts\Services\FeedFetcher;
6use Psr\Log\LoggerInterface;
7
8class FeedLoggingDecorator implements FeedFetcher
9{
10 protected $fetcher;
11
12 protected $logger;
13
14 public function __construct(FeedFetcher $fetcher, LoggerInterface $logger)
15 {
16 $this->fetcher = $fetcher;
17 $this->logger = $logger;
18 }
19
20 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<?php
2
3namespace Foo\Bar\Contracts;
4
5use Foo\Bar\Objects\Item;
6use Foo\Bar\Objects\ItemCollection;
7
8interface Client
9{
10 public function getAll(): ItemCollection;
11
12 public function find(int $id): Item;
13
14 public function create(array $data): Item;
15
16 public function update(int $id, array $data): Item;
17
18 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<?php
2
3namespace Foo\Bar\Services;
4
5use Foo\Bar\Contracts\Client;
6use Psr\Cache\CacheItemPoolInterface;
7
8class CachingDecorator implements Client
9{
10 protected $client;
11
12 protected $cache;
13
14 public function __construct(Client $client, CacheItemPoolInterface $cache)
15 {
16 $this->client = $client;
17 $this->cache = $cache;
18 }
19
20 public function getAll(): ItemCollection
21 {
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 }
29
30 public function find(int $id): Item
31 {
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();
38
39 }
40
41 public function create(array $data): Item
42 {
43 $this->cache->clear();
44 return $this->client->create($data);
45 }
46
47 public function update(int $id, array $data): Item
48 {
49 $this->cache->clear();
50 return $this->client->update($id, $data);
51 }
52
53 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.