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 réalise à l'aide de classes d'options dédiées à chacun d'entre eux, par exemple, pour la gestion du cache par fichier:
$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 concerné, concaténé avec le suffixe « Options »: ApcOptions, MemcachedOptions, etc. et héritent de la classe Zend\Stdlib\Options qui offre la possibilité de recevoir un tableau d'options afin de peupler l'objet d'option. Les clés du tableau d'options sont transformées en nom de méthode d'altération, « cache_dir » sera par exemple transformé en « setCacheDir() » où la valeur « __DIR__ . '/data/' » lui sera fournie. Pour plus de détails, le chapitre 5.1 sur la configuration des modules analyse le fonctionnement de la classe Zend\Stdlib\Options.
Lorsque l'adaptateur est instancié et configuré, il devient alors utilisable. Notre exemple ajoute la valeur « ma valeur » au fichier « zfcache-ma-cle.dat », créé dans notre cache. Celui-ci contient la valeur brute « ma valeur » que l'on a définie.
Afin de nous éviter de devoir créer nos différents objets et de les peupler manuellement, une fabrique de cache existe grâce à la classe Zend\Cache\StorageFactory qui permet de créer directement nos adaptateurs et plugins:
$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:
$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 méthodes de récupération, de suppression ou de mise à jour de caches sont disponibles depuis l'adaptateur et il est également possible d'agir sur le contenu de notre fichier de cache grâce à l'utilisation de nos différents plugins.
IV. Les plugins▲
Les plugins offrent des fonctionnalités supplémentaires à l'adaptateur de cache et permettent aussi d'agir sur le contenu du cache. Les plugins vont permettre la sérialisation ou encore la désactivation du lancement des exceptions généré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 désactivant le lancement de nos exceptions, ce qui nous permet de ne pas contrôler l'existence du fichier de cache:
$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 lancée ici, mais la valeur de notre cache n'aura pas été mise à jour. Si nous souhaitons nous assurer de la bonne suppression du cache pour la mise à jour des données sans nous soucier des exceptions, il est nécessaire d'ajouter l'instruction liée à la suppression:
$cache
->
removeItem('
ma-cle
'
) ;
$cache
->
addItem('
ma-cle
'
,
'
ma valeur
'
);
IV-B. La sérialisation▲
Le plugin de sérialisation permet de sérialiser automatiquement la valeur du cache en définissant un objet de sérialisation de type Zend\Serializer\Adapter:
$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 caractère "s:10:" ma valeur";". De nombreux objets de sérialisation sont disponibles dans le framework: Json, PhpCode, PhpSerialize, etc.
V. Analyse des plugins▲
Les plugins profitent pleinement des possibilités de gestion d'évènements du framework. Chaque plugin écoute les évènements de l'adaptateur depuis la classe AbstractAdapter qui les notifient avant et après chacune des actions effectué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 clé/valeur:
public function addItem($key
,
$value
,
array $options
=
array())
{
$baseOptions
=
$this
->
getOptions();
if ($baseOptions
->
getWritable() &&
$baseOptions
->
getClearStatCache()) {
clearstatcache();
}
return parent::
addItem($key
,
$value
,
$options
);
}
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 méthode « addItem() » de la classe AbstractAdapter gère l'ajout dans le cache. Une fois les contrôles et les normalisations effectués, la classe mère de notre adaptateur crée un tableau de paramètres contenant les références de nos clé et valeur:
public function addItem($key
,
$value
,
array $options
=
array())
{
[&
#8230;]
$args
=
new ArrayObject(array(
'
key
'
=>
&
$key
,
'
value
'
=>
&
$value
,
'
options
'
=>
&
$options
,
));
[&
#8230;]
}
Les variables étant enregistrées dans notre tableau par leur référence, nous aurons donc accès aux valeurs modifiées par les plugins depuis ces références. Une fois cette opération effectuée, un premier évènement de prétraitement est lancé afin de notifier les plugins qui souhaitent agir sur les contenus de nos variables avant enregistrement:
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 évènements de prétraitement et post-traitement sont « triggerPre() » et « triggerPost() »:
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 méthodes sont responsables du lancement de l'évènement correspondant au nom de la fonction suivi du suffixe « .pre » ou « .post ». Examinons quels évènements écoutent nos plugins et comment ceux-ci répondent aux notifications.
Les plugins s'enregistrent dès leur passage à l'adaptateur:
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 unicité au sein de l'adaptateur. Les écouteurs du plugin sont ensuite attachés aux différents évènements. Prenons l'exemple du plugin Serializer:
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'évènement « additem.pre », ce qui lui permet d'agir sur la valeur de notre cache avant son écriture:
public function onWriteItemsPre(Event $event
)
{
$options
=
$this
->
getOptions();
$serializer
=
$options
->
getSerializer();
$params
=
$event
->
getParams();
$params
[
'
value
'
]
=
$serializer
->
serialize($params
[
'
value
'
]
);
}
La méthode écouteur récupère l'objet de sérialisation afin de pouvoir agir sur la valeur du cache. Il n'est pas nécessaire de retourner la valeur du cache modifiée, celle-ci étant passée par référence, notre adaptateur aura accès au contenu de la variable maintenant sérialisée.
Il devient alors très simple de créer son propre plugin de cache. Nos plugins personnalisés devront simplement s'attacher sur les évènements dont ils ont besoin pour fonctionner et implémenter l'interface Zend\Cache\Storage\Plugin.
VI. Le composant Zend\Cache\Pattern▲
Le composant Zend\Cache\Pattern permet de résoudre les problématiques de performances précises. Ce composant permet de cacher les retours de fonctions de callback ou d'objet. Il permet aussi la génération de cache pour les classes. Cependant, ce composant tient compte du contexte et du résultat du retour des méthodes des objets cachés. En effet, pour un même objet et une même méthode, si cette dernière retourne un résultat différent, un objet de cache sera créé à chaque fois.
Analysons un exemple avec un 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 généré. Les prochains appels à cette méthode de ce même objet pourront alors utiliser ce fichier de cache. Ce cache peut s'avérer pratique lors de traitements récurrents, longs et ayant des résultats identiques. Cependant, il est nécessaire de s'assurer que l'objet appelé fonctionne d'une manière identique à chaque appel au risque de devoir générer des caches à chaque appel :
$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'avère très mauvaise sur cet exemple, un cache sera généré à 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 nécessaire d'analyser la création et la gestion du cache de notre composant.
Lors de l'appel à l'objet Config depuis le composant ObjectCache, celui-ci génère une clé basée sur le contexte de l'objet afin que chaque mise en cache soit reliée à un contexte précis en limitant les effets de bords comme l'on a vu lors de l'exemple précédent. En effet, il est nécessaire de vérifier la composition interne de l'objet, car une méthode ne retournera peut être pas le même résultat si un de ses attributs est modifié.
Lors de la création de la clé, gérée par la classe parente Zend\Cache\Pattern\CallbackCache qui génère le nom du cache, le composant sérialise l'objet utilisé qu'il pilote:
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 sérialisation de l'objet tient compte des noms et valeurs des attributs. L'objet piloté doit alors être strictement identique d'un appel à l'autre afin d'utiliser le cache généré.
La mise en cache des objets et de leurs valeurs de retour peut s'avérer très pratique lors de long traitement relativement identiques. La génération de cache et les vérifications que cela engendre nécessite de ne pas en abuser et de pouvoir justifier l'utilisation d'un tel composant. Une mauvaise utilisation de ce cache peut s'avérer beaucoup plus pé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"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/blanchonvincenthttps://github.com/blanchonvincent.
Retrouvez aussi la liste des tutoriels du Zend Framework 2 sur mon compte developpez.com : blanchon-vincent.developpez.comblanchon-vincent.developpez.com.