5 votes

Meilleure façon d'implémenter un modèle de décorateur pour la mise en cache des résultats des méthodes en PHP

J'ai un ensemble de classes qui ont l'habitude d'être appelées à plusieurs reprises avec les mêmes arguments. Ces méthodes exécutent généralement des requêtes de base de données et construisent des tableaux d'objets et autres, et donc pour éviter cette duplication, j'ai construit quelques méthodes de mise en cache pour optimiser. Elles sont utilisées comme suit :

Avant l'application de la mise en cache :

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

Après la mise en cache appliquée :

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

Malheureusement, je me retrouve maintenant avec la tâche un peu laborieuse d'ajouter manuellement ceci à toutes les méthodes - je crois que c'est un cas d'utilisation du motif décorateur mais je n'arrive pas à trouver comment l'implémenter de manière plus simple en PHP pour ce cas.

Quelle est la meilleure façon de procéder, en espérant que soit todo Les méthodes de l'une de ces classes le font automatiquement, ou je dois simplement ajouter une ligne dans la méthode, etc.

J'ai cherché des moyens de remplacer l'instruction de retour et autres, mais je n'ai rien trouvé.

Gracias.

12voto

Gordon Points 156415

Si vous n'avez pas besoin de la sécurité de type vous pouvez utiliser un décorateur de cache générique :

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

Vous pouvez alors envelopper une instance qui a besoin d'être mise en cache dans ce décorateur comme ceci :

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

De toute évidence, le $cachedInstance n'a pas de foo() méthode. L'astuce consiste ici à utiliser la magie __call pour intercepter tous les appels à des méthodes inaccessibles ou inexistantes et les déléguer à l'instance décorée. De cette façon, nous exposons l'API publique entière de l'instance décorée à travers le décorateur.

Comme vous pouvez le voir, le __call contient également le code permettant de vérifier si une mise en cache est définie pour cette méthode. Si c'est le cas, elle renvoie l'appel de la méthode mise en cache. Sinon, elle appellera l'instance et renverra le retour en cache.

Vous pouvez également transmettre un CacheBackend dédié au décorateur au lieu d'implémenter la mise en cache dans le décorateur lui-même. Le décorateur fonctionnerait alors uniquement comme un médiateur entre l'instance décorée et le backend.

El inconvénient de cette approche générique est que votre décorateur de cache n'aura pas le type de l'instance décorée. Lorsque votre code de consommation attendra des instances de type Instance, vous obtiendrez des erreurs.


Si vous avez besoin de décorateurs sûrs pour les types vous devez utiliser l'approche "classique" :

  1. Créer une interface de l'API publique de l'instance décorée. Vous pouvez le faire manuellement ou, si c'est beaucoup de travail, utiliser ma méthode de création d'une interface publique. Distillateur d'interface )
  2. Changez les TypeHints de chaque méthode attendant l'instance décorée pour l'Interface
  3. Faites en sorte que l'instance Decorated l'implémente.
  4. Demandez au décorateur de l'implémenter et de déléguer toutes les méthodes à l'instance décorée.
  5. Modifier toutes les méthodes qui nécessitent une mise en cache
  6. Répétez pour toutes les classes qui veulent utiliser le décorateur

En bref

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
    }
}

El inconvénient c'est qu'il y a plus de travail. Vous devez le faire pour chaque classe censée utiliser la mise en cache. Vous devrez aussi mettre la vérification du backend du cache dans chaque fonction (duplication de code) et déléguer tous les appels qui ne nécessitent pas de cache à l'instance décorée (fastidieux et source d'erreurs).

1voto

CodeAngry Points 5672

Utilisez le __call méthode magique.

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()

Ceci est utilisé en utilisant le stockage interne mais vous pouvez utiliser la BD pour cela et créer votre propre stockage transitoire. Il suffit d'ajouter _Cached o Cached à toute méthode existante. Évidemment, vous pouvez modifier la durée de vie et plus encore.

C'est juste une preuve de concept. Il est possible de l'améliorer considérablement :)

0voto

decebal Points 558

Voici un extrait d'un article sur le sujet de la mise en cache en 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;
   }
}

Cela me semble plus logique car il s'agit d'une mise en œuvre SOLIDE. Je ne suis pas un grand fan de la mise en œuvre de la même chose avec des annotations, je préférerais quelque chose de plus simple.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X