Лучший способ реализовать шаблон декоратора для кэширования результатов метода в PHP

У меня есть набор классов, которые имеют привычку многократно вызываться с теми же аргументами. Эти методы обычно запускают запросы к базе данных и строят массивы объектов и т. Д., И поэтому, чтобы вырезать это дублирование, я разработал несколько методов кэширования для оптимизации. Они используются так:

Перед применением кеширования:

public function method($arg1, $arg2) { $result = doWork(); return $result; } 

После применения кэширования:

 public function method($arg1, $arg2, $useCached=true) { if ($useCached) {return $this->tryCache();} $result = doWork(); return $this->cache($result); } 

К сожалению, теперь я оставил немного кропотливую задачу вручную добавить это ко всем методам – ​​я считаю, что это пример использования шаблона декоратора, но я не могу понять, как его проще реализовать в PHP для Это дело.

Каков наилучший способ сделать это, мы надеемся, что либо все методы в любом из этих классов автоматически сделают это, либо мне просто нужно добавить одну строку в метод и т. Д.?

Я посмотрел, как переопределить оператор return и т. Д., Но ничего не вижу.

Благодаря!

Если вам не нужна Type Safety , вы можете использовать общий кэш-декоратор:

 class Cached { public function __construct($instance, $cacheDir = null) { $this->instance = $instance; $this->cacheDir = $cacheDir === null ? sys_get_temp_dir() : $cacheDir; } public function defineCachingForMethod($method, $timeToLive) { $this->methods[$method] = $timeToLive; } public function __call($method, $args) { if ($this->hasActiveCacheForMethod($method, $args)) { return $this->getCachedMethodCall($method, $args); } else { return $this->cacheAndReturnMethodCall($method, $args); } } // … followed by private methods implementing the caching 

Затем вы оберните экземпляр, который нужно кэшировать в этот Decorator следующим образом:

 $cachedInstance = new Cached(new Instance); $cachedInstance->defineCachingForMethod('foo', 3600); 

Очевидно, что $cachedInstance не имеет метода foo() . Трюк здесь заключается в использовании метода магии __call для перехвата всех вызовов недоступным или несуществующим методам и делегирования их декорированному экземпляру. Таким образом, мы обнародуем весь открытый API украшенного экземпляра через Decorator.

Как вы можете видеть, метод __call также содержит код для проверки наличия кеширования, определенного для этого метода. Если это так, он вернет вызов метода кэширования. В противном случае он вызовет экземпляр и кеширует возврат.

Кроме того, вы передаете выделенную CacheBackend для Decorator вместо реализации кэширования в самом декораторе. Декоратор тогда будет работать только как посредник между украшенным экземпляром и бэкэндом.

Недостатком этого общего подхода является то, что у вашего Cache Decorator не будет типа Decorated Instance. Когда ваш потребительский код ожидает экземпляры типа Instance, вы получите ошибки.


Если вам нужны декораторы с типом , вам необходимо использовать «классический» подход:

  1. Создайте интерфейс открытого API открытого экземпляра. Вы можете сделать это вручную или, если это большая работа, используйте мой интерфейс Distiller )
  2. Измените TypeHints для каждого метода, ожидающего декорированный экземпляр интерфейса
  3. Попросите экземпляр Decorated реализовать его.
  4. Попросите Decorator реализовать его и делегировать любые методы украшенному экземпляру
  5. Измените все методы, требующие кеширования
  6. Повторите для всех классов, которые хотят использовать декоратор

В двух словах

 class CachedInstance implements InstanceInterface { public function __construct($instance, $cachingBackend) { // assign to properties } public function foo() { // check cachingBackend whether we need to delegate call to $instance } } 

Недостатком является то, что это больше работает. Вам нужно сделать это для каждого класса, который должен использовать кеширование. Вам также необходимо поместить проверку в бэкэнд кэша в каждую функцию (дублирование кода), а также делегировать любые вызовы, которые не требуют кэширования для украшенного экземпляра (утомительный и подверженный ошибкам).

Используйте метод __call magic.

 class Cachable { private $Cache = array(); public function Method1(){ return gmstrftime('%Y-%m-%d %H:%M:%S GMT'); } public function __call($Method, array $Arguments){ // Only 'Cached' or '_Cached' trailing methods are accepted if(!preg_match('~^(.+)_?Cached?$~i', $Method, $Matches)){ trigger_error('Illegal Cached method.', E_USER_WARNING); return null; } // The non 'Cached' or '_Cached' trailing method must exist $NotCachedMethod = $Matches[1]; if(!method_exists($this, $NotCachedMethod)){ trigger_error('Cached method not found.', E_USER_WARNING); return null; } // Rebuild if cache does not exist or is too old (5+ minutes) $ArgumentsHash = md5(serialize($Arguments)); // Each Arguments product different output if( !isset($this->Cache[$NotCachedMethod]) or !isset($this->Cache[$NotCachedMethod][$ArgumentsHash]) or ((time() - $this->Cache[$NotCachedMethod][$ArgumentsHash]['Updated']) > (5 * 60)) ){ // Rebuild the Cached Result $NotCachedResult = call_user_func_array(array($this, $NotCachedMethod), $Arguments); // Store the Cache again $this->Cache[$NotCachedMethod][$ArgumentsHash] = array( 'Method' => $NotCachedMethod, 'Result' => $NotCachedResult, 'Updated' => time(), ); } // Deliver the Cached result return $this->Cache[$NotCachedMethod][$ArgumentsHash]['Result']; } } $Cache = new Cachable(); var_dump($Cache->Method1()); var_dump($Cache->Method1Cached()); // or $Cache->Method1_Cached() sleep(5); var_dump($Cache->Method1()); var_dump($Cache->Method1Cached()); // or $Cache->Method1_Cached() 

Это используется с использованием внутреннего хранилища, но вы можете использовать БД для этого и создать свое собственное хранилище переходных процессов. Просто добавьте _Cached или _Cached в любой существующий метод. Очевидно, вы можете изменить срок службы и многое другое.

Это просто доказательство концепции. Там есть много улучшений 🙂

Вот выдержка из статьи, посвященной кешированию в php

 /** * Caching aspect */ class CachingAspect implements Aspect { private $cache = null; public function __construct(Memcache $cache) { $this->cache = $cache; } /** * This advice intercepts the execution of cacheable methods * * The logic is pretty simple: we look for the value in the cache and if we have a cache miss * we then invoke original method and store its result in the cache. * * @param MethodInvocation $invocation Invocation * * @Around("@annotation(Annotation\Cacheable)") */ public function aroundCacheable(MethodInvocation $invocation) { $obj = $invocation->getThis(); $class = is_object($obj) ? get_class($obj) : $obj; $key = $class . ':' . $invocation->getMethod()->name; $result = $this->cache->get($key); if ($result === false) { $result = $invocation->proceed(); $this->cache->set($key, $result); } return $result; } } 

Это имеет для меня больше смысла, поскольку он реализуется с помощью SOLID-реализации. Я не большой поклонник реализации того же самого с аннотациями, предпочел бы что-то более простое.