Nouveau dans la version 2.1.
La création d’applications maintenables est à la fois une science et un art. Il est connu que la clé pour avoir des codes de bonne qualité est d’avoir un couplage plus lâche et une cohésion plus élevée. La cohésion signifie que toutes les méthodes et propriétés pour une classe sont fortement reliés à la classe elle même et non pas d’essayer de faire le travail que d’autre objets devraient faire, tandis que un couplage plus lâche est la mesure du degré de resserrement des interconnexions d’une classe aux objets externes, et comment cette classe en dépend.
Tandis que la plupart des structures CakePHP et des librairies par défaut vous aideront à atteindre ce but, il y a certains cas où vous avez besoin de communiquer proprement avec les autres parties du système sans avoir à coder en dur ces dépendances, ainsi réduire la cohésion et accroître le couplage de classe. Un motif de conception (design pattern) très réussi dans l’ingénierie software est le modèle observateur (Observer pattern), où les objets peuvent générer des événements et notifier à des écouteurs (listener) possiblement anonymes des changements d’états internes.
Les écouteurs (listener) dans le modèle observateur (Observer pattern) peuvent s’abonner à de tels événements et choisir d’interagir sur eux, modifier l’état du sujet ou simplement faire des logs. Si vous avez utilisez JavaScript dans le passé, vous avez la chance d’être déjà familier avec la programmation événementielle.
CakePHP émule plusieurs aspects sur la façon dont les événements sont déclenchés et managés dans des frameworks JavaScript comme le populaire jQuery, tout en restant fidèle à sa conception orientée objet. Dans cette implémentation, un objet événement est transporté à travers tous les écouteurs qui détiennent l’information et la possibilité d’arrêter la propagation des événements à tout moment. Les écouteurs peuvent s’enregistrer eux-mêmes ou peuvent déléguer cette tâche a d’autres objets et avoir la chance de modifier l’état et l’événement lui-même pour le reste des callbacks.
Le sous-système d’event est au coeur des callbacks de Model, de Behavior, de Controller, de View et de Helper. Si vous n’avez jamais utilisé aucun d’eux, vous êtes déjà quelque part familier avec les events dans CakePHP.
Supposons que vous codez un Plugin de gestion de panier, et que vous vouliez vous focaliser sur la logique lors de la commande. Vous ne voulez pas à ce moment là inclure la logique pour l’expédition, l’email ou la décrémentation du produit dans le stock, mais ce sont des tâches importantes pour les personnes utilisant votre plugin. Si vous n’utilisiez pas les évènements vous auriez pu implémenter cela en attachant des behaviors à vos modèles ou en ajoutant des composants à votre controller. Typiquement, quand vous n’utilisez pas directement le modèle observateur (observer pattern) vous feriez cela en attachant des behaviors à la volée à vos models, et peut être quelques components aux controllers.
A la place, vous pouvez utiliser les événements pour vous permettre de séparer clairement ce qui concerne votre code et permettre d’ajouter des besoins supplémentaires dans votre plugin en utilisant les événements. Par exemple dans votre plugin Cart, vous avez un model Order qui gère la création des commandes. Vous voulez notifier au reste de l’application qu’une commande a été créée. Pour garder votre model Order propre, vous pouvez utiliser les événements:
// Cart/Model/Order.php
App::uses('CakeEvent', 'Event');
class Order extends AppModel {
public function place($order) {
if ($this->save($order)) {
$this->Cart->remove($order);
$event = new CakeEvent('Model.Order.afterPlace', $this, array(
'order' => $order
));
$this->getEventManager()->dispatch($event);
return true;
}
return false;
}
}
Le code ci-dessus vous permet de notifier aux autres parties de l’application qu’une commande a été créée. Vous pouvez ensuite faire des tâches comme envoyer les notifications par mail, mettre à jour le stock, créer un fichier de log des statistiques pertinentes et d’autres tâches dans des objets séparés qui se focalisent sur ces préoccupations.
Dans CakePHP, les événements sont attrapés par les gestionnaires d’événements.
Les gestionnaires d’événements sont disponibles dans chaque Table, View et
Controller en utilisant getEventManager()
:
$events = $this->getEventManager();
Chaque Model a un gestionnaire d’événements séparé, alors que View et Controller en partagent un. Cela permet aux événements de Model d’être autonomes, et permet aux components ou aux controllers d’agir sur les événements créés dans la vue si nécessaire.
En plus des gestionnaires au niveau des instances d’événement, CakePHP fournit
un gestionnaire d’événements global qui vous permet d’écouter tout événement
déclenché dans une application. C’est utile quand attacher des écouteurs à une
instance spécifique semble lent ou difficile. Le gestionnaire global est une
instance singleton de CakeEventManager
qui reçoit chaque événement
avant que les gestionnaires d’instance ne le reçoivent. En plus de recevoir les
événements en premier, le gestionnaire global maintient aussi une pile de
priorité distincte pour les écouteurs. Une fois qu’un événement a été dispatché
au gestionnaire global, il sera dispatché au gestionnaire au niveau de
l’instance. Vous pouvez accéder au gestionnaire global en utilisant une méthode
statique:
// Dans n'importe quel fichier de config ou morceau de code qui s'exécute avant l'événement
App::uses('CakeEventManager', 'Event');
CakeEventManager::instance()->attach(
$aCallback,
'Model.Order.afterPlace'
);
Un élément important que vous devriez considérer est qu’il y a des événements qui seront déclenchés en ayant le même nom mais différents sujets, donc les vérifier dans l’objet événement est généralement requis dans chacune des fonctions qui sont attachées globalement pour éviter quelques bugs. Souvenez-vous qu’une extrême flexibilité implique une extrême complexité.
Modifié dans la version 2.5: Avant 2.5, les listeners du gestionnaire global étaient gardés dans une liste séparée et déclenchés avant que les instances de listeners le soient.
Une fois obtenue l’instance d’un gestionnaire d’événement, vous pourrez distribuer
les événements via dispatch()
. Cette méthode prend en argument un instance
de la classe CakeEvent
. Regardons comment distribuer un événement:
// Create a new event and dispatch it.
$event = new CakeEvent('Model.Order.afterPlace', $this, array(
'order' => $order
));
$this->getEventManager()->dispatch($event);
CakeEvent
reçoit 3 arguments dans son constructeur. Le premier
est le nom de l’événement, vous devrez essayer de garder ce nom aussi unique
que possible, tout en le rendant lisible. Nous vous suggérons les conventions
suivantes: Layer.eventName pour les événements généraux qui surviennent à
un niveau de couche(ex:Controller.startup,`View.beforeRender` ) et
Layer.Class.eventName pour les événements qui surviennent dans une classe
spécifique sur une couche, par exemple Model.User.afterRegister ou
Controller.Courses.invalidAccess.
Le second argument est le sujet subject, ce qui signifie l’objet associé
à l’événement, habituellement quand c’est la même classe de déclenchement
d’événements que lui même, $this sera communément utilisé.
Bien que Component
pourrait lui aussi déclencher les événements
du controller. La classe du sujet est importante parce que les écouteurs
(listeners) auront des accès immédiats aux propriétés des objets et la chance
de les inspecter ou de les changer à la volée.
Finalement, le troisième argument est le paramètre d’événement. Ceci peut être n’importe quelle donnée que vous considérez comme étant utile à passer avec laquelle les écouteurs peuvent interagir. Même si cela peut être n’importe quel type d’argument, nous vous recommandons de passer un tableau associatif, pour rendre l’inspection plus facile.
La méthode CakeEventManager::dispatch()
accepte les objets
événements comme arguments et notifie a tous les écouteurs et callbacks le
passage de cet objet. Ainsi les écouteurs géreront toute la logique autour
de l’événement afterPlace, vous pouvez enregistrer l’heure, envoyer des
emails, éventuellement mettre à jour les statistiques de l’utilisateur dans
des objets séparés et même déléguer cela à des tâches hors-ligne si vous
en avez besoin.
Les écouteurs (Listeners) sont une alternative, et souvent le moyen le plus
propre d’enregistrer les callbacks pour un événement. Ceci est fait en
implémentant l’interface CakeEventListener
dans chacune de classes
ou vous souhaitez enregistrer des callbacks. Les classes l’implémentant doivent
fournir la méthode implementedEvents()
et retourner un tableau associatif
avec tous les noms d’événements que la classe gérera.
Pour en revenir à notre exemple précédent, imaginons que nous avons une classe
UserStatistic responsable du calcul d’information utiles et de la compilation
de statistiques dans le site global. Ce serait naturel de passer une instance
de cette classe comme un callback, au lieu d’implémenter une fonction statique
personnalisé ou la conversion de n’importe quel autre contournement
pour déclencher les méthodes de cette classe. Un écouteur (listener)
UserStatistics
est créé comme ci-dessous:
// Dans app/Lib/Event/UserStatistic.php
App::uses('CakeEventListener', 'Event');
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Order.afterPlace' => 'updateBuyStatistic'
);
}
public function updateBuyStatistic($event) {
// Code to update statistics
}
}
// Dans un controller ou à n'importe quel endroit où $this->Order est accessible
// Attache l'objet UserStatistic au gestionnaire d'événement 'Order' (commande)
$statistics = new UserStatistic();
$this->Order->getEventManager()->attach($statistics);
Comme vous pouvez le voir dans le code ci-dessus, la fonction attach peut
manipuler les instances de l’interface CakeEventListener. En interne, le
gestionnaire d’événement lira le tableau retourné par la méthode
implementedEvents
et relie les callbacks en conséquence.
Comme montré dans l’exemple ci-dessus, les écouteurs d’événement sont placés
par convention dans app/Lib/Event
. Suivre cette convention vous permet
de facilement localiser vos classes d’écouteurs. Il est aussi recommandé
d’attacher les écouteurs globaux pendant le processus de bootstrap de votre
application:
// Dans app/Config/bootstrap.php
// Charge les écouteurs d'événement globaux.
require_once APP . 'Config' . DS . 'events.php'
Un exemple de fichier de bootstrap d’événement pour notre application de caddie ressemblerait à ceci:
// Dans app/Config/events.php
// Charge les écouteurs d'événement
App::uses('UserStatistic', 'Lib/Event');
App::uses('ProductStatistic', 'Lib/Event');
App::uses('CakeEventManager', 'Event');
// Attache les écouteurs.
CakeEventManager::instance()->attach(new UserStatistic());
CakeEventManager::instance()->attach(new ProductStatistic());
Tandis que les objects d’écoute d’événements sont généralement une meilleure
manière d’implémenter les écouteurs, vous pouvez aussi attacher n’importe quel
callable
comme écouteur d’événement. Par exemple, si nous voulions
enregistrer chaque commande dans les fichiers de journalisation, nous
utiliserions une simple fonction anonyme pour le faire:
// Les fonctions anonymes requièrent PHP 5.3+
$this->Order->getEventManager()->attach(function($event) {
CakeLog::write(
'info',
'A new order was placed with id: ' . $event->subject()->id
);
}, 'Model.Order.afterPlace');
En plus des fonctions anonymes, vous pouvez utiliser n’importe quel type de callable
supporté par PHP:
$events = array(
'email-sending' => 'EmailSender::sendBuyEmail',
'inventory' => array($this->InventoryManager, 'decrement'),
);
foreach ($events as $callable) {
$eventManager->attach($callable, 'Model.Order.afterPlace');
}
Dans certains cas, vous souhaitez exécuter un callback et être sûre qu’il sera exécuté avant, ou après tous les autres callbacks déjà lancés. Par exemple, repensons à notre exemple de statistiques utilisateur. Il serait judicieux de n’exécuter cette méthode que si nous sommes sûrs que l’événement n’a pas été annulé, qu’il n’y a pas d’erreur et que les autres callbacks n’ont pas changés l’état de “order” lui même. Pour ces raisons vous pouvez utiliser les priorités.
Les priorités sont gérés en utilisant un nombre associé au callback lui même. Plus haut est le nombre, plus tard sera lancée la méthode. Les priorités par défaut des méthodes des callbacks et écouteurs sont définis à “10”. Si vous voulez que votre méthode soit lancée avant, alors l’utilisation de n’importe quelle valeur plus basse que cette valeur par défaut vous aidera à le faire, même en mettant la priorité à 1 ou une valeur négative pourrait fonctionner. D’une autre façon si vous désirez exécuter le callback après les autres, l’usage d’un nombre au dessus de 10 fonctionnera.
Si deux callbacks se trouvent alloués avec le même niveau de priorité, ils seront exécutés avec une règle FIFO, la première méthode d’écouteur (listener) attachée est appelée en premier et ainsi de suite. Vous définissez les priorités en utilisant la méthode attach pour les callbacks, et les déclarer dans une fonction implementedEvents pour les écouteurs d’événements:
// Paramétrage des priorités pour un callback
$callback = array($this, 'doSomething');
$this->getEventManager()->attach($callback, 'Model.Order.afterPlace', array('priority' => 2));
// Paramétrage des priorité pour un écouteur(listener)
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Order.afterPlace' => array('callable' => 'updateBuyStatistic', 'priority' => 100),
);
}
}
Comme vous pouvez le voir, la principale différence pour les objets CakeEventListener c’est que vous avez à utiliser un tableau pour spécifier les méthodes appelables et les préférences de priorités. La clé appelable callable est une entrée de tableau spéciale que le gestionnaire (manager) lira pour savoir quelle fonction dans la classe il devrait appeler.
Certain développeurs pourraient préférer avoir les données d’événements passées comme des paramètres de fonctions au lieu de recevoir l’objet événement. Bien que ce soit une préférence étrange et que l’utilisation d’objet événement est bien plus puissant, ceci a été nécessaire pour fournir une compatibilité ascendante avec le précédent système d’événement et pour offrir aux développeurs chevronnés une alternative pour ce auquel ils sont habitués.
Afin de changer cette option, vous devez ajouter l’option passParams au troisième argument de la méthode attach, ou le déclarer dans le tableau de retour implementedEvents de la même façon qu’avec les priorités:
// Paramétrage des priorités pour le callback
$callback = array($this, 'doSomething');
$this->getEventManager()->attach($callback, 'Model.Order.afterPlace', array('passParams' => true));
// Paramétrage des priorités pour l'écouteur (listener)
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Order.afterPlace' => array('callable' => 'updateBuyStatistic', 'passParams' => true),
);
}
public function updateBuyStatistic($orderData) {
// ...
}
}
Dans l’exemple ci-dessus la fonction doSomething et la méthode updateBuyStatistic recevrons $orderData au lieu de l’objet $event. C’est comme cela parce que dans notre premier exemple nous avons déclenché l’événement Model.Order.afterPlace avec quelques données:
$this->getEventManager()->dispatch(new CakeEvent('Model.Order.afterPlace', $this, array(
'order' => $order
)));
Note
Les paramètres ne peuvent être passés comme arguments de fonction que si la donnée d’événement est un tableau. N’importe quel autre type de données sera converti en paramètre de fonction, ne pas utiliser cette option est souvent plus adéquate.
Il y a des circonstances ou vous aurez besoin de stopper des événements de sorte que l’opération commencée est annulée. Vous voyez un exemple de cela dans les callbacks des models (ex. beforesave) dans lesquels il est possible de stopper une opération de sauvegarde si le code détecte qu’il ne peut pas aller plus loin.
Afin de stopper les événements vous pouvez soit retourner false dans vos callbacks ou appeler la méthode stopPropagation sur l’objet événement:
public function doSomething($event) {
// ...
return false; // stoppe l'événement
}
public function updateBuyStatistic($event) {
// ...
$event->stopPropagation();
}
Stopper un événement peut avoir deux effets différents. Le premier peut toujours être attendu; n’importe quel callback après l’événement qui à été stoppé ne sera appelé. La seconde conséquence est optionnelle et dépend du code qui déclenche l’événement, par exemple, dans votre exemple afterPlace cela n’aurait pas de sens d’annuler l’opération tant que les données n’aurons pas toutes été enregistrées et le Caddie vidé. Néanmoins, si nous avons une beforePlace arrêtant l’événement cela semble valable.
Pour vérifier qu’un événement a été stoppé, vous appelez la méthode isStopped() dans l’objet événement:
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event);
if ($event->isStopped()) {
return false;
}
if ($this->Order->save($order)) {
// ...
}
// ...
}
Dans l’exemple précédent la vente ne serait pas enregistrée si l’événement est stoppé durant le processus beforePlace.
Chacune des fois ou un callback retourne une valeur, celle ci est stockée dans la propriété $result de l’objet événement. C’est utile dans certains cas où laisser les callbacks modifier les paramètres principaux de processus augmente la possibilité de modifier l’aspect d’exécution des processus. Regardons encore notre exemple beforePlace et laissons les callbacks modifier la donnée $order (commande).
Les résultats d’événement peuvent être modifiés soit en utilisant directement la propriété result de l’objet event ou en retournant une valeur dans le callback lui même.
// Un écouteur (listener) de callback
public function doSomething($event) {
// ...
$alteredData = $event->data['order'] + $moreData;
return $alteredData;
}
// Un autre écouteur (listener) de callback
public function doSomethingElse($event) {
// ...
$event->result['order'] = $alteredData;
}
// Utilisation du résultat de l'événement
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event);
if (!empty($event->result['order'])) {
$order = $event->result['order'];
}
if ($this->Order->save($order)) {
// ...
}
// ...
}
Comme vous l’avez peut-être aussi remarqué, il est possible de modifier n’importe quelle propriété d’un objet événement et d’être sûr que ces nouvelles données seront passées au prochain callback. Dans la majeur partie des cas, fournir des objets comme donnée événement ou résultat et modifier directement les objets est la meilleur solution puisque la référence est maintenue et que les modifications sont partagées à travers les appels callbacks.
Si pour quelque raison que ce soit, vous voulez retirer certains callbacks
depuis le gestionnaire d’événement, appelez juste la méthode
CakeEventManager::detach()
en utilisant comme arguments les
deux premiers paramètres que vous avez utilisés pour les attacher:
// Attacher une fonction
$this->getEventManager()->attach(array($this, 'doSomething'), 'My.event');
// Détacher la fonction
$this->getEventManager()->detach(array($this, 'doSomething'), 'My.event');
// Attacher une fonction anonyme (PHP 5.3+ seulement);
$myFunction = function($event) { ... };
$this->getEventManager()->attach($myFunction, 'My.event');
// Détacher la fonction anonyme
$this->getEventManager()->detach($myFunction, 'My.event');
// Attacher un écouteur Cake (CakeEventListener)
$listener = new MyEventListener();
$this->getEventManager()->attach($listener);
// Détacher une simple clé d'événement depuis un écouteur (listener)
$this->getEventManager()->detach($listener, 'My.event');
// Détacher tous les callbacks implémentés par un écouteur (listener)
$this->getEventManager()->detach($listener);
Les événements sont une bonne façon de séparer les préoccupations dans votre application et rend les classes a la fois cohérentes et découplées des autres, néanmoins l’utilisation des événements n’est pas la solution à tous les problèmes. Les Events peuvent être utilisés pour découpler le code de l’application et rendre les plugins extensibles.
Gardez à l’esprit que beaucoup de pouvoir implique beaucoup de responsabilité. Utiliser trop d’events peut rendre le debug plus difficile et nécessite des tests d’intégration supplémentaires.