📜 ⬆️ ⬇️

Using the proxy pattern to organize caching in PHP

Formulation of the problem. There is a working debugged PHP project containing a dozen models, each of which has 5 data sampling methods. The project is growing, everything is fine, but at a certain point, under the weight of the load, there is a need to add caching of calls to the models in some way.

Possible solutions.

The first way is "on the forehead": we add caching according to the standard scheme to each model method: check the cache, if there is actual data, return it, if not - perform the method as it was before and plus at the end we also write the data obtained from the database in the cache. To say that this is a terrible way means to say nothing, so I’ll just say why this is bad:
')
  1. One of the SOLID principles is violated, “the code should be open for expansion, but closed for changes”, i.e. we take and break the already debugged production-released code in order to add new functionality, and this always causes a flurry of errors and, as a result, user and customer dissatisfaction.
  2. In the same code, the logic of data acquisition and caching is mixed, which leads to the swelling of classes and the ruthless repetition of the code.
  3. By doing this, we lose the ability to get live data to bypass the cache (the next step is to add the $ nocache flag).
  4. Very high complexity of caching in this way and even greater complexity of cutting it out later.

The second way, “expanding the classes of models”: add to the model methods-doublers that wrap calls to existing methods in caching, for example, findById_Cached ().
It seems to be better, we don’t touch existing methods, instead we add new ones. But the remaining cons on the spot:
  1. Mixing logic.
  2. Class sizes grow even more than in the previous method.
  3. Very high labor intensity (add 50 new methods, in our example) + replace the calls of the old methods everywhere in the application with new ones, and if in the future you have to cut the caching, then repeat all the actions back.


The third way is “caching proxy” , a very simple and fast solution, striking with its grace and speed of implementation. How to do it - look at the code.

First we have a model (sample):
<?php namespace Storage { Class News { public function getTodayNews() { return "today news"; } public function searchNews( array $filter ) { $key = \http_build_query($filter); return "search news where $key"; } } } ?> 

And calling it in the application before caching:
 <?php $news = new \Storage\News; $todayNews = $news->getTodayNews(); $searchNews = $news->searchNews( array('tag' =>'sport') ); ?> 

Then we have a cache (sample):
 <?php namespace Cache { Class Cache { protected $data = array(); public function get($key) { if ( isset($this->data[$key]) ) { return $this->data[$key]; } else { return null; } } public function set($key, $val, $ttl = 60) { $this->data[$key] = $val; } } ?> 

And here is the star of this topic - a caching proxy:
 <?php namespace Cache { Class Proxy { protected $realObject = null; protected $cache = null; protected $ttl = 0; public function __construct( $object, $ttl = 60 ) { $this->realObject = $object; //  ,  - //    $this->cache = new Cache; $this->ttl = $ttl; } //       //      //    __call() public function __call( $method, $args ) { $cacheKey = $method . '(' . \serialize($args) . ')'; $data = $this->cache->get( $cacheKey ); if ( null === $data ) { $call = array( $this->realObject, $method ); $data = \call_user_func_array( $call, $args ); $this->cache->set( $cacheKey, $data, $this->ttl ); } return $data; } } } ?> 


We use:
 <?php // $news = new \Storage\News; //   : $realNews = new \Storage\News; /** *    IDE  PhpDoc, *     ,      * * @var \Storage\News $news; */ $news = new \Cache\Proxy( $realNews , 600 ); $todayNews = $news->getTodayNews(); $searchNews = $news->searchNews( array('tag' =>'sport') ); ?> 


As you can see, the proxy does not matter at all what object and method to cache, it is not at all necessary that this will be a class working with the database. If necessary, we have kept the possibility of obtaining live data. A huge layer of the application responsible for caching, eventually reduced to a small class, the implementation of which does not cause difficulties.

UPD: There is a nuance, in our example $ news has become an object of a different type, and if somewhere in the code there are type checks (for example instanceof or data type in method parameters), then these checks will break. To avoid this, you need to inherit \ Cache \ Proxy from \ Storage, of course, the universality of the caching class in this case will decrease.

Source: https://habr.com/ru/post/240557/


All Articles