Créez votre propre framework… avec les composants Symfony2 (partie 9) | KeiruaProd
Je suis développeur web freelance et propose des formations à Symfony2 ! Contactez-moi pour en discuter.

Cet article est la traduction d’un article original de Fabien Potencier, à l’origine de Symfony2, disponible ici.

Actuellement, il manque à notre framework une caractéristique essentielle à tout bon framework : l’extensibilité. Être extensible signifie que le développeur doit pouvoir facilement s’intégrer dans le cycle de vie du framework pour modifier la manière dont les requêtes sont gérées.

De quel genre d’intégration parlons nous ? D’authentification ou de système de cache par exemple. Pour être flexible, il faut que le développeur puise s’intégrer de manière plug-and-play. Beaucoup d’applications appliquent des concepts similaires, tel que WordPress ou Drupal. Dans certains langages, il y a même des standards tel que WSGI en Python ou Rack en Ruby.

Comme il n’y a pas de standard en PHP, nous allons utiliser un design pattern bien connu, Observer, pour permettre d’attacher n’importe quel type de comportement à notre framework. Le composant EventDispatcher de Symfony2 implémente une version légère de ce patron de conception :


{
"require": {
"symfony/class-loader": "2.1.*",
"symfony/http-foundation": "2.1.*",
"symfony/routing": "2.1.*",
"symfony/http-kernel": "2.1.*",
"symfony/event-dispatcher": "2.1.*"
},
"autoload": {
"psr-0": { "Simplex": "src/", "Calendar": "src/" }
}
}

Comment ça marche ? Le dispatcher, objet central du système de répartition des évènements, notifie des écouteurs (listeners) qu’un évènement a été transmis. Dit d’une autre manière : votre code fournit un évènement au dispatcher, le dispatcher prévient tous ceux qui écoutent cet évènement, et les écouteurs font ce qu’ils souhaitent de l’évènement.

Comme exemple, créons un écouteur qui va, de manière transparente, ajouter le code de Google
Analytics code à toutes les réponses.

Pour que cela marche, le framework doit transmettre un évènement juste avant de renvoyer l’instance de la réponse :


matcher = $matcher;
$this->resolver = $resolver;
$this->dispatcher = $dispatcher;
}

public function handle(Request $request)
{
try {
$request->attributes->add($this->matcher->match($request->getPathInfo()));

$controller = $this->resolver->getController($request);
$arguments = $this->resolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);
} catch (ResourceNotFoundException $e) {
$response = new Response('Not Found', 404);
} catch (\Exception $e) {
$response = new Response('An error occurred', 500);
}

// dispatch a response event
$this->dispatcher->dispatch('response', new ResponseEvent($response, $request));

return $response;
}
}

A chaque fois que le framework gère une requête, un évènement ResponseEvent est envoyé :


response = $response;
$this->request = $request;
}

public function getResponse()
{
return $this->response;
}

public function getRequest()
{
return $this->request;
}
}

La dernière étape de la création du dispatcher dans le contrôleur de façade, c’est de faire écouter l’évènement response par un listener :


addListener('response', function (Simplex\ResponseEvent $event) {
$response = $event->getResponse();

if ($response->isRedirection()
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $event->getRequest()->getRequestFormat()
) {
return;
}

$response->setContent($response->getContent().'GA CODE');
});

$framework = new Simplex\Framework($dispatcher, $matcher, $resolver);
$response = $framework->handle($request);

$response->send();

Cet écouteur sert juste pour la démonstration, vous devriez ajouter le code Google Analytics juste avant le tag body.

Comme vous pouvez le voir, addListener() associe un callback PHP valide à un évènement nommé (« response »); le nom de l’évènement doit être le même que celui de l’appel à dispatch().

Dans l’écouteur, nous ajoutons le code Google Analytics seulement si la réponse n’est pas une redirection et si le format de sortie est en HTML (ces conditions démontrent à quel point il est facile de manipuler les données de la requête et de la réponse depuis votre code).

Très bien pour le moment, mais ajoutons un nouvel écouteur pour le même évènement. Disons que je veux définir la valeur de « Content-Length«  du header de la réponse si il n’est pas déjà paramétré :


$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
$response = $event->getResponse();
$headers = $response->headers;

if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
$headers->set('Content-Length', strlen($response->getContent()));
}
});

Selon si vous avez ajouté ce morceau de code avant ou après l’enregistrement du précédent écouteur, vous aurez la bonne ou la mauvaise valeur de Content-Length dans le header. Parfois, l’ordre des écouteurs importe mais par défaut, tous les écouteurs sont enregistrés avec la même priorité, 0. Pour dire au dispatcher d’envoyer un évènement tôt, changez la priorité pour un nombre positif. Les nombres négatifs peuvent par contre être utilisés pour des écouteurs à basse priorité. Ici, nous voulons que l’écouteur pour Content-Length soit exécuté en dernier, donc on lui met comme priorité -255 :


$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
$response = $event->getResponse();
$headers = $response->headers;

if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
$headers->set('Content-Length', strlen($response->getContent()));
}
}, -255);

Lorsque vous créez votre framework, pensez aux priorités (réservez des nombres pour les écouteurs internes par exemple), et documentez les abondamment.

Refactorons un peu le code en déplacant l’écouteur Google dans sa propre classe :


getResponse();

if ($response->isRedirection()
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $event->getRequest()->getRequestFormat()
) {
return;
}

$response->setContent($response->getContent().'GA CODE');
}
}

Faisons la même chose avec l’autre écouteur :


getResponse();
$headers = $response->headers;

if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
$headers->set('Content-Length', strlen($response->getContent()));
}
}
}

Notre contrôleur de façade devrait maintenant ressembler à ça :


$dispatcher = new EventDispatcher();
$dispatcher->addListener('response', array(new Simplex\ContentLengthListener(), 'onResponse'), -255);
$dispatcher->addListener('response', array(new Simplex\GoogleListener(), 'onResponse'));

Même si le code est maintenant convenablement séparé dans des classes, il y a toujours un léger problème : la connaissance de la priorité est codé en dur dans le contrôleur de façade, au lieu d’être définie dans les écouteurs eux-même. Dans chaque application, il va vous falloir vous souvenir des priorités appropriées. De plus, les noms des méthodes des écouteurs sont également exposées ici, ce qui signifie que si l’on refactore le code, il faudra changer tout le code qui en dépend. Bien sûr, il y a une solution : utiliser des inscription (subscribers) au lieu des écouteurs.


$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Simplex\ContentLengthListener());
$dispatcher->addSubscriber(new Simplex\GoogleListener());

Une inscription sait tout de l’évènement auquel elle s’intéresse et fournit les informations au dispatcher via la méthode getSubscribedEvents(). Regardez la nouvelle version du « GoogleListener » :


'onResponse');
}
}

Et il y a également une nouvelle version du « ContentLengthListener » :

array('onResponse', -255));
}
}

Une unique inscription pour héberger autant d’écouteurs que vous le voulez, vers autant d’évènements que vous le souhaitez.

Pour rendre votre framework vraiment flexible, n’hésitez pas à ajouter plus d’évènements; et pour le rendre génial dès la sortie de sa boite, ajoutez plus d’écouteurs. Je vous rappelle que cette série n’a pas pour but de créer un framework générique, mais un qui soit taillé sur mesure à vos besoins. Arrêtez quand vous le pensez nécessaire, et faites évoluer le code de votre côté.

Répondre

Unable to load the Are You a Human PlayThru™. Please contact the site owner to report the problem.