Zend Framework 2 : le cache

Adaptateur, plugins et performances

Blanchon Vincent

Entièrement réécrit pour la version 2 du Zend Framework, l'apparition des plugins et de composants capables de répondre à des problématiques précises (cache d'objets ou méthodes) font de ce composant un outil indispensable pour répondre aux besoins de performances. Commentez

- Titre : Zend Framework 2 : le cache

- Auteur : Blanchon Vincent

- Parution : 9 mai 2012

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ®  Blanchon Vincent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Prérequis

- II. Le composant Zend\Cache

- III. Les adaptateurs.

- IV. Les plugins

- IV-A. Le plugin de gestion d'exception

- IV-B. La sérialisation

- V. Analyse des plugins

- VI. Le composant Zend\Cache\Pattern

- VII. Conclusion et ressources

I - Prérequis

La version du Zend Framework 2 utilisée pour cet article est la 2.0.0beta3. L'utilisation du Zend Framework 2 nécessite au minimum la version 5.3.3 de PHP.

II - Le composant Zend\Cache

Le composant de cache du framework a été entièrement revu afin de pouvoir mieux séparer les différentes responsabilités liées à la création du cache de notre application.

Les sous-composants qui gèrent le stockage des caches se trouvent dans le namespace Zend\Cache\Storage\Adaptateur où l'on retrouve les principaux gestionnaires de stockage connu: APC, memcached, fichier, etc.

III - Les adaptateurs.

La configuration des adaptateurs se réalise à l'aide de classes d'options dédiées à chacun d'entre eux, par exemple, pour la gestion du cache par fichier:

exemple de création de cache

$cache = new \Zend\Cache\Storage\Adapter\Filesystem();
$cacheOptions = new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/'));
$cache->setOptions($cacheOptions);if(!$cache->hasItem('ma-cle')){
$cache->addItem('ma-cle','ma valeur');
}

Les classes d'options portent le nom de l'adaptateur concerné, concaténé avec le suffixe « Options »: ApcOptions, MemcachedOptions, etc. et héritent de la classe Zend\Stdlib\Options qui offre la possibilité de recevoir un tableau d'options afin de peupler l'objet d'option. Les clés du tableau d'options sont transformées en nom de méthode d'altération, « cache_dir » sera par exemple transformé en « setCacheDir() » où la valeur « __DIR__ . '/data/' » lui sera fournie. Pour plus de détails, le chapitre 5.1 sur la configuration des modules analyse le fonctionnement de la classe Zend\Stdlib\Options.

Lorsque l'adaptateur est instancié et configuré, il devient alors utilisable. Notre exemple ajoute la valeur « ma valeur » au fichier « zfcache-ma-cle.dat », créé dans notre cache. Celui-ci contient la valeur brute « ma valeur » que l'on a définie.

Afin de nous éviter de devoir créer nos différents objets et de les peupler manuellement, une fabrique de cache existe grâce à la classe Zend\Cache\StorageFactory qui permet de créer directement nos adaptateurs et plugins:

exemple de création de cache

$cache = \Zend\Cache\StorageFactory::factory(array(
'adapter' => array(
'name' => 'filesystem',
'options' => array('cache_dir'=>__DIR__ . '/data/')
),
'plugins' => array(
'exception_handler' => array('throw_exceptions' => false),
),
));
$cache->addItem('ma-cle','ma valeur');

La fabrique permet aussi de prendre en charge des instances d'objet:

exemple de création de cache

$cache = \Zend\Cache\StorageFactory::factory(array(
'adapter' => array(
'name' => new \Zend\Cache\Storage\Adapter\Filesystem(),
'options' => new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/')
)
),
'plugins' => array(
'exception_handler' => array('throw_exceptions' => false),
),
);

D'autres méthodes de récupération, de suppression ou de mise à jour de caches sont disponibles depuis l'adaptateur et il est également possible d'agir sur le contenu de notre fichier de cache grâce à l'utilisation de nos différents plugins.

IV - Les plugins

Les plugins offrent des fonctionnalités supplémentaires à l'adaptateur de cache et permettent aussi d'agir sur le contenu du cache. Les plugins vont permettre la sérialisation ou encore la désactivation du lancement des exceptions générées par les adaptateurs de cache. Prenons l'exemple de deux plugins : la gestion d'exceptions et la sérialisation.

IV-A - Le plugin de gestion d'exception

Reprenons notre premier exemple en désactivant le lancement de nos exceptions, ce qui nous permet de ne pas contrôler l'existence du fichier de cache:

création de cache avec le plugin de gestion d'exception

$cache = new \Zend\Cache\Storage\Adapter\Filesystem();
$cache = new \Zend\Cache\Storage\Adapter\Filesystem();
$cacheOptions = new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/')
);
$cache->setOptions($cacheOptions);
$cachePlugin = new \Zend\Cache\Storage\Plugin\ExceptionHandler();
$cachePluginOptions = new \Zend\Cache\Storage\Plugin\PluginOptions(
array('throw_exceptions'=>false));
$cachePlugin->setOptions($cachePluginOptions);
$cache->addPlugin($cachePlugin);$cache->addItem('ma-cle','ma valeur');

Aucune exception n'est lancée ici, mais la valeur de notre cache n'aura pas été mise à jour. Si nous souhaitons nous assurer de la bonne suppression du cache pour la mise à jour des données sans nous soucier des exceptions, il est nécessaire d'ajouter l'instruction liée à la suppression:

suppression et ajout de cache

$cache->removeItem('ma-cle') ;
$cache->addItem('ma-cle','ma valeur');

IV-B - La sérialisation

Le plugin de sérialisation permet de sérialiser automatiquement la valeur du cache en définissant un objet de sérialisation de type Zend\Serializer\Adapter:

création de cache avec le plugin de séralisation

$cache = new \Zend\Cache\Storage\Adapter\Filesystem();
$cacheOptions = new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/'));
$cache->setOptions($cacheOptions);
$cachePlugin = new \Zend\Cache\Storage\Plugin\Serializer();
$cachePluginOptions = new \Zend\Cache\Storage\Plugin\PluginOptions(
array('serializer'=>new \Zend\Serializer\Adapter\PhpSerialize())
);
$cachePlugin->setOptions($cachePluginOptions);
$cache->addPlugin($cachePlugin);
$cache->addItem('ma-cle',' ma valeur');

Le fichier de cache contient maintenant la chaîne de caractère "s:10:" ma valeur";". De nombreux objets de sérialisation sont disponibles dans le framework: Json, PhpCode, PhpSerialize, etc.

V - Analyse des plugins

Les plugins profitent pleinement des possibilités de gestion d'évènements du framework. Chaque plugin écoute les évènements de l'adaptateur depuis la classe AbstractAdapter qui les notifient avant et après chacune des actions effectuées. Ce fonctionnement permet aux plugins de pouvoir effectuer leurs modifications sur l'objet de stockage ou les valeurs de nos caches.

Prenons comme exemple l'ajout d'un couple clé/valeur:

Zend\Cache\Adapter\Filesystem.php

public function addItem($key, $value, array $options = array())
{
$baseOptions = $this->getOptions();
if ($baseOptions->getWritable() && $baseOptions->getClearStatCache()) {
clearstatcache();
}
return parent::addItem($key, $value, $options);
}

Zend\Cache\Adapter\AbstractAdapter.php

public function addItem($key, $value, array $options = array())
{
if (!$this->getOptions()->getWritable()) {
return false;
}
$this->normalizeOptions($options);
$this->normalizeKey($key);
$args = new ArrayObject(array(
'key'     => & $key,
'value'   => & $value,
'options' => & $options,
));
try {
$eventRs = $this->triggerPre(__FUNCTION__, $args);
if ($eventRs->stopped()) {
return $eventRs->last();
}
$result = $this->internalAddItem($key, $value, $options);
return $this->triggerPost(__FUNCTION__, $args, $result);
} catch (\Exception $e) {
return $this->triggerException(__FUNCTION__, $args, $e);
}
}

La méthode « addItem() » de la classe AbstractAdapter gère l'ajout dans le cache. Une fois les contrôles et les normalisations effectués, la classe mère de notre adaptateur crée un tableau de paramètres contenant les références de nos clé et valeur:

Zend\Cache\Adapter\AbstractAdapter.php

public function addItem($key, $value, array $options = array())
{
[&#8230;]
$args = new ArrayObject(array(
'key'     => & $key,
'value'   => & $value,
'options' => & $options,
));
[&#8230;]
}

Les variables étant enregistrées dans notre tableau par leur référence, nous aurons donc accès aux valeurs modifiées par les plugins depuis ces références. Une fois cette opération effectuée, un premier évènement de prétraitement est lancé afin de notifier les plugins qui souhaitent agir sur les contenus de nos variables avant enregistrement:

Zend\Cache\Adapter\AbstractAdapter.php

public function addItem($key, $value, array $options = array())
{
[&#8230;]
try {
$eventRs = $this->triggerPre(__FUNCTION__, $args);
if ($eventRs->stopped()) {
return $eventRs->last();
}
$result = $this->internalAddItem($key, $value, $options);
return $this->triggerPost(__FUNCTION__, $args, $result);
} catch (\Exception $e) {
return $this->triggerException(__FUNCTION__, $args, $e);
}
}

Les deux méthodes qui permettent de lancer les évènements de prétraitement et post-traitement sont « triggerPre() » et « triggerPost() »:

Zend\Cache\Adapter\AbstractAdapter.php

protected function triggerPre($eventName, ArrayObject $args
{
return $this->events()->trigger(new Event($eventName . '.pre', $this, $args));
}

protected function triggerPost($eventName, ArrayObject $args, &$result)
{
$postEvent = new PostEvent($eventName . '.post', $this, $args, $result);
$eventRs   = $this->events()->trigger($postEvent);
if ($eventRs->stopped()) {
return $eventRs->last();
}
return $postEvent->getResult();
}

Ces deux méthodes sont responsables du lancement de l'évènement correspondant au nom de la fonction suivi du suffixe « .pre » ou « .post ». Examinons quels évènements écoutent nos plugins et comment ceux-ci répondent aux notifications.

Les plugins s'enregistrent dès leur passage à l'adaptateur:

Zend\Cache\Adapter\AbstractAdapter.php

public function addPlugin(Plugin $plugin)
{
$registry = $this->getPluginRegistry();
if ($registry->contains($plugin)) {
[&#8230;]
}
$plugin->attach($this->events());
$registry->attach($plugin);
return $this;
}

Le registre interne stocke ensuite l'instance de notre plugin afin de s'assurer de son unicité au sein de l'adaptateur. Les écouteurs du plugin sont ensuite attachés aux différents évènements. Prenons l'exemple du plugin Serializer:

Zend\Cache\Storage\Plugin\Serializer.php

public function attach(EventCollection $events)
{
$index = spl_object_hash($events);
if (isset($this->handles[$index])) {
throw new Exception\LogicException('Plugin already attached');
}
$handles = array();
$this->handles[$index] = & $handles;
[&#8230;]
$handles[] = $events->attach('setItem.pre',  array($this, 'onWriteItemPre'));
$handles[] = $events->attach('setItems.pre', array($this, 'onWriteItemsPre'));
$handles[] = $events->attach('addItem.pre',  array($this, 'onWriteItemPre'));
$handles[] = $events->attach('addItems.pre', array($this, 'onWriteItemsPre'));
[&#8230;]
return $this;
}

Le plugin Serializer s'attache sur l'évènement « additem.pre », ce qui lui permet d'agir sur la valeur de notre cache avant son écriture:

Zend\Cache\Storage\Plugin\Serializer.php

public function onWriteItemsPre(Event $event)
{
$options    = $this->getOptions();
$serializer = $options->getSerializer();
$params     = $event->getParams();
$params['value'] = $serializer->serialize($params['value']);
}

La méthode écouteur récupère l'objet de sérialisation afin de pouvoir agir sur la valeur du cache. Il n'est pas nécessaire de retourner la valeur du cache modifiée, celle-ci étant passée par référence, notre adaptateur aura accès au contenu de la variable maintenant sérialisée.

Il devient alors très simple de créer son propre plugin de cache. Nos plugins personnalisés devront simplement s'attacher sur les évènements dont ils ont besoin pour fonctionner et implémenter l'interface Zend\Cache\Storage\Plugin.

VI - Le composant Zend\Cache\Pattern

Le composant Zend\Cache\Pattern permet de résoudre les problématiques de performances précises. Ce composant permet de cacher les retours de fonctions de callback ou d'objet. Il permet aussi la génération de cache pour les classes. Cependant, ce composant tient compte du contexte et du résultat du retour des méthodes des objets cachés. En effet, pour un même objet et une même méthode, si cette dernière retourne un résultat différent, un objet de cache sera créé à chaque fois.

Analysons un exemple avec un cache d'objet:

exemple de cache d'objet

$config = new \Zend\Config\Config(array('cle'=>'valeur'));
$proxyConfig = new \Zend\Cache\Pattern\ObjectCache();
$proxyConfigOptions = new \Zend\Cache\Pattern\PatternOptions(
array(
'object' => $config,
'storage' => new \Zend\Cache\Storage\Adapter\Filesystem(
new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/')
)
)
)
);
$proxyConfig->setOptions($proxyConfigOptions);
$value = $proxyConfig->offsetGet('cle');

Au premier passage de ces instructions, un fichier de cache propre au retour de la fonction est généré. Les prochains appels à cette méthode de ce même objet pourront alors utiliser ce fichier de cache. Ce cache peut s'avérer pratique lors de traitements récurrents, longs et ayant des résultats identiques. Cependant, il est nécessaire de s'assurer que l'objet appelé fonctionne d'une manière identique à chaque appel au risque de devoir générer des caches à chaque appel :

exemple de cache d'objet

$config = new \Zend\Config\Config(array('cle'=>time()),true);
$proxyConfig = new \Zend\Cache\Pattern\ObjectCache();
$proxyConfigOptions = new \Zend\Cache\Pattern\PatternOptions(
array(
'cache_output' => true,
'object' => $config,
'storage' => new \Zend\Cache\Storage\Adapter\Filesystem(
new \Zend\Cache\Storage\Adapter\FilesystemOptions(
array('cache_dir'=>__DIR__ . '/data/')
)
)
));
$proxyConfig->setOptions($proxyConfigOptions);
$value = $proxyConfig->offsetGet('cle');

L'utilisation du composant Zend\Cache\Pattern\ObjectCache s'avère très mauvaise sur cet exemple, un cache sera généré à chaque passage de ses instructions du fait du changement permanent de la valeur de retour de la fonction « time() ». Afin de maîtriser la mise en cache de nos objets, il est nécessaire d'analyser la création et la gestion du cache de notre composant.

Lors de l'appel à l'objet Config depuis le composant ObjectCache, celui-ci génère une clé basée sur le contexte de l'objet afin que chaque mise en cache soit reliée à un contexte précis en limitant les effets de bords comme l'on a vu lors de l'exemple précédent. En effet, il est nécessaire de vérifier la composition interne de l'objet, car une méthode ne retournera peut être pas le même résultat si un de ses attributs est modifié.

Lors de la création de la clé, gérée par la classe parente Zend\Cache\Pattern\CallbackCache qui génère le nom du cache, le composant sérialise l'objet utilisé qu'il pilote:

Zend\Cache\Pattern\CallbackCache.php

protected function _generateKey($callback, array $args, array $options)
{
[&#8230;]
$serializedObject = @serialize($object);
[&#8230;]
$callbackKey.= $serializedObject;
[&#8230;]
$serializedArgs = @serialize(array_values($args));
[&#8230;]
$argumentKey = $serializedArgs;
[&#8230;]
return md5($callbackKey.$argumentKey);
}

La sérialisation de l'objet tient compte des noms et valeurs des attributs. L'objet piloté doit alors être strictement identique d'un appel à l'autre afin d'utiliser le cache généré.

La mise en cache des objets et de leurs valeurs de retour peut s'avérer très pratique lors de long traitement relativement identiques. La génération de cache et les vérifications que cela engendre nécessite de ne pas en abuser et de pouvoir justifier l'utilisation d'un tel composant. Une mauvaise utilisation de ce cache peut s'avérer beaucoup plus pénalisant que performant.

VII - Conclusion et ressources

Le cache du Zend Framework 2 est capable de répondre à la plupart des problématiques de goulots d'étranglement, mais ne doit pas être utilisé afin de masquer les autres problèmes de performances des application.
Cet article est tiré en partie du livre "Au cœur de Zend Framework 2" disponible dans le début du quatrième trimestre de l'année 2012. Dans ce livre, vous retrouverez en détails tous les composants intervenants lors du rendu de la vue.

Retrouvez mes modules ou autres fonctionnalités, créés pour le Zend Framework 2, sur mon compte github disponible à l'adresse https://github.com/blanchonvincent.

Retrouvez aussi la liste des tutoriels du Zend Framework 2 sur mon compte developpez.com : blanchon-vincent.developpez.com.