Magic Caching Decorator
Now I’m working on finalizing / rewriting a project that was written, well, let's say, “not quite correctly.” Along the way, there is a task to optimize the work, because the code was originally written extremely non-optimal. Among the optimization work, a cache is screwed.
There are several different data sources in the project, the results of which would be nice to cache, the main one is of course the database. I wanted a transparent solution with minimal blood. At one point, tired of writing constructions of the form
And I want something else. Of course, you can put the code into a separate function or method, but it’s kind of 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 your own function /method.
On the other hand, adding caching code directly to data sources is also not very correct - in the end, they should not do this (which is why Traits are also not suitable). Creating a separate cache class is also not very convenient.
In general, I wanted a single, universal solution that would be suitable for different data sources, with different interfaces, but at the same time it would be uniform. It was decided to make a "magic" decorator.
If you are not aware of what a decorator is, then in general terms: A decorator is a design pattern whose purpose is to dynamically connect a new behavior to an object. Thus, in our case, the data access object will remain the same for the system, with exactly the same interface and behavior, but it will have some new (caching) behavior.
What exactly I want: for additional methods of the cached * type to 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:
The initialization of the decorator will look something like this:
But so far, our decorator is not a decorator at all and does not behave at all like a decorated object. Fix this by adding magic (add getters / setters, call forwarding):
Well, now the behavior of the object is identical to natural (well, almost, but in our situation this 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 we do this:
Actually everything. Now, having decorated the necessary data source, we can write instead
Simply:
That's so simple, and no more gardens are required to work with the cache.
Update: Here in a personal email they write that flexibility is lost, there is no way to specify the cache lifetime. In my case, this is simply not relevant, the time specified when initializing the cache object is used. 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 lifetimes, for example, fastCached * and slowCached * (for frequently and rarely updated data, respectively).
There are several different data sources in the project, the results of which would be nice to cache, the main one is of course the database. I wanted a transparent solution with minimal blood. At one point, tired of writing constructions of the form
$query = "Select something";
$result = $cache->get($query, $tag);
if (!$result) {
$result = $db->queryAll($query);
$cache->set($query, $tag);
}
And I want something else. Of course, you can put the code into a separate function or method, but it’s kind of 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 your own function /method.
On the other hand, adding caching code directly to data sources is also not very correct - in the end, they should not do this (which is why Traits are also not suitable). Creating a separate cache class is also not very convenient.
In general, I wanted a single, universal solution that would be suitable for different data sources, with different interfaces, but at the same time it would be uniform. It was decided to make a "magic" decorator.
If you are not aware of what a decorator is, then in general terms: A decorator is a design pattern whose purpose is to dynamically connect a new behavior to an object. Thus, in our case, the data access object will remain the same for the system, with exactly the same interface and behavior, but it will have some new (caching) behavior.
What exactly I want: for additional methods of the cached * type to 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 {
/**
* @var object Ссылка на декорируемый объект.
*/
protected $obj;
/**
* @var object Ссылка на объект кэша.
*/
protected $cache;
/**
* @var string Дополнительный параметр для кэша - тэг.
*/
protected $cacheTag;
/**
* @param type $object Декорируемый объект
* @param type $cache Объект кэша
* @param type $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 at all like a decorated object. Fix this by adding magic (add getters / setters, call forwarding):
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);
}
Well, now the behavior of the object is identical to natural (well, almost, but in our situation this 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 we do 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, having decorated the necessary data source, we can write instead
$result = $data->getDataById($id);
Simply:
$result = $data->cachedGetDataById($id);
That's so simple, and no more gardens are required to work with the cache.
Update: Here in a personal email they write that flexibility is lost, there is no way to specify the cache lifetime. In my case, this is simply not relevant, the time specified when initializing the cache object is used. 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 lifetimes, for example, fastCached * and slowCached * (for frequently and rarely updated data, respectively).