Now I am working on revising / rewriting a project that was written, well, let's say, “not quite competently”. Along the way, there is a task to optimize the work, since The code was originally written extremely non-optimal. Among the work on optimizing cache is screwed.
There are several different data sources in the project, the results of which would be good to cache, the main one - of course, the database. I wanted a clear solution, with minimal blood. At one point, tired of writing constructs of the form
$query = "Select something"; $result = $cache->get($query, $tag); if (!$result) { $result = $db->queryAll($query); $cache->set($query, $tag); }
And you want something else. Of course, the code can be put into a separate function or method, but it is somehow boring and, besides, for each different call (and there is not only $ db-> queryAll, but several different options) you will need your own code and its own /method.
')
On the other hand, adding caching code directly to data sources is also not very correct - after all, they should not do this (which is why
Traits also do not fit). Creating a separate cache class is also not very convenient.
In general, we wanted a single, universal solution that would be suitable for different data sources, with different interfaces, but at the same time was uniform. It was decided to make a "magic" decorator.
If you do not know what a decorator is, then in general terms: A decorator is a design pattern that aims to dynamically connect a new behavior to an object. Thus, in our case, the data access object for the system will seem to be the same, with exactly the same interface and behavior, but it will have some new (caching) behavior.
What exactly you want: so that additional methods like cached * appear in the data source object. For example, there was a getData () method, in addition to it, a cachedGetData () method will appear, with the same interface as getData (). The decorator will do on the "magic" methods.
So, we write:
class CachingDecorator { protected $obj; protected $cache; protected $cacheTag; public function __construct($object, $cache, $cacheTag = 'query') { $this->obj = $object; $this->cache = $cache; $this->cacheTag = $cacheTag; } }
The initialization of the decorator will look something like this:
$data = new CachingDecorator($data, $cache, 'remote');
But so far our decorator is not a decorator at all and does not behave like a decorated object at all. Fix this by adding magic (add getters / setters, forwarding calls):
public function __get($name) { return $this->obj->$name; } public function __set($name, $value) { return $this->obj->$name = $value; } public function __call($name, $args) { return call_user_func_array(array($this->obj, $name), $args); }
Great, now the object's behavior is identical to the natural (well, almost, but in our situation it is enough, if you are missing something, add the necessary magic methods).
Usually, simple methods are added to the decorator. But we want magic, so let's do it like this:
public function __call($name, $args) { if (strtolower(substr($name, 0, 6)) == 'cached') { $name = substr($name, 6); $cacheName = md5(serialize($args)); $result = $this->cache->get($cacheName, $this->cacheTag); if ($result === false) { $result = call_user_func_array(array($this->obj, $name), $args); $this->cache->save($result, $cacheName, $this->cacheTag); } return $result; } else { return call_user_func_array(array($this->obj, $name), $args); } }
Actually everything. Now, by decorating the desired data source, we can write instead
$result = $data->getDataById($id);
Simply:
$result = $data->cachedGetDataById($id);
So simple, and no longer need to fence any gardens to work with the cache.
Update: Here they write in a personal note that flexibility is being lost, there is no way to specify the cache lifetime. In my case, this is simply not relevant, using the time specified during the initialization of the cache object. But if you need it, you can simply expand the decorator. You can, for example, change the interface for cached * functions by adding the cache lifetime to the first parameter. Or add more magic methods that will use different cache lifetime, for example, fastCached * and slowCached * (for often and rarely updated data, respectively).