Le design pattern Observer avec Symfony2

Rappelez-vous, dans l’article précédent, j’ai présenté le design pattern Observer, mais avec l’implémentation de la Bibliothèque standard Spl.

En voici l’implémentation avec Symfony2.

Les changements

  • Suppression des interfaces et classes abstraites
  • Mise en place d’un event pour l’observable (le sujet)
  • Mise en place de listeners (ou de subscribers) pour les observers (les observateurs)
  • L’attribut changed disparait dans cette implémentation (mais vous pouvez le réinstaurer bien sûr)

Let’s go! Symfony2 rocks!!!

1) Création de l’event DonneesMeteoEvent

namespace DP\SfObserverBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use DP\SfObserverBundle\Entity\Observable\DonneesMeteo;

class DonneesMeteoEvent extends Event
{
    private $donneesMeteo;

    public function __construct(DonneesMeteo $donneesMeteo)
    {
        $this->donneesMeteo = $donneesMeteo;
    }

    public function getDonneesMeteo()
    {
        return $this->donneesMeteo;
    }
}

C’est nouveau mais indispensable lorsque l’on utilise des events en Symfony2. Rappelez-vous, pour l’authentification sans FosUserBundle, j’avais aussi créé un event qui ne faisait que renvoyer l’entité. Ici, c’est pareil, mon event prend dans son constructeur mon observable, et me le rend par un getter.

2) Créons notre event de mise à jour des mesures

namespace DP\SfObserverBundle;

final class DPSfObserverEvents
{
    const MESURES_UPDATED = 'donnees_meteo.update';
}

3) Créons la classe concrète Observable

<?php

namespace DP\SfObserverBundle\Entity\Observable;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use DP\SfObserverBundle\Event\DonneesMeteoEvent;
use DP\SfObserverBundle\DPSfObserverEvents;

/**
 * DonneesMeteo => celui qui est observé
 */
class DonneesMeteo
{
    private $temperature;
    private $humidity;
    private $pressure;
    private $dispatcher;

    function getTemperature()
    {
        return $this->temperature;
    }

    function getHumidity()
    {
        return $this->humidity;
    }

    function getPressure()
    {
        return $this->pressure;
    }

    function setDispatcher(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    public function setMesures($temperature, $humidity, $pressure)
    {
        $this->temperature = $temperature;
        $this->humidity    = $humidity;
        $this->pressure    = $pressure;
        $event             = new DonneesMeteoEvent($this);

        $this->dispatcher->dispatch(DPSfObserverEvents::MESURES_UPDATED, $event);
    }
}

Très peu de changements ici, excepté le fait qu’un event est créé, DonneesMeteoEvent, et qu’il prend en paramètre $this.

Très important, au moment où les mesures sont mises à jour, le dispatcher de Symfony2 est appelé et dispatch l’événement donnees_meteo.update.

4) Nos observers listeners:
AffichageConditionsListener,
AffichagePrevisionsListener,
AffichageStatsListener

namespace DP\SfObserverBundle\Listener;

use DP\SfObserverBundle\Event\DonneesMeteoEvent;

class AffichageConditionsListener
{
    private $temperature;
    private $humidity;
    private $pressure;

    public function getTemperature()
    {
        return $this->temperature;
    }

    public function getHumidity()
    {
        return $this->humidity;
    }

    public function getPressure()
    {
        return $this->pression;
    }

    public function setTemperature($temperature)
    {
        $this->temperature = $temperature;
    }

    public function setHumidity($humidity)
    {
        $this->humidity = $humidity;
    }

    public function setPressure($pressure)
    {
        $this->pressure = $pressure;
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo      = $event->getDonneesMeteo();
        $this->temperature = $donneesMeteo->getTemperature();
        $this->humidity    = $donneesMeteo->getHumidity();
        $this->pressure    = $donneesMeteo->getPressure();
    }

    public function getNewValues()
    {
        return array('temperature' => $this->temperature, 'humidite' => $this->humidity, 'pression' => $this->pressure);
    }
}
namespace DP\SfObserverBundle\Listener;

use DP\SfObserverBundle\Event\DonneesMeteoEvent;

class AffichagePrevisionsListener
{
    private $currentPressure;
    private $lastPressure;
    private $prevision;

    public function getCurrentPressure()
    {
        return $this->currentPressure;
    }

    public function setCurrentPressure($currentPressure)
    {
        $this->currentPressure = $currentPressure;
    }

    public function getLastPressure()
    {
        return $this->lastPressure;
    }

    public function getPrevision()
    {
        return $this->prevision;
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo          = $event->getDonneesMeteo();
        $this->lastPressure    = $this->currentPressure;
        $this->currentPressure = $donneesMeteo->getPressure();
    }

    public function getNewValues()
    {
        if ($this->currentPressure > $this->lastPressure) {
            $this->prevision = "Improving weather on the way!";
        } else if ($this->currentPressure == $this->lastPressure) {
            $this->prevision = "More of the same";
        } else if ($this->currentPressure < $this->lastPressure) {
            $this->prevision = "Watch out for cooler, rainy weather";
        }

        return array('prevision' =>$this->prevision);
    }
}
namespace DP\SfObserverBundle\Listener;

use DP\SfObserverBundle\Event\DonneesMeteoEvent;

class AffichageStatsListener
{
    private $maxTemp;
    private $minTemp;
    private $sumTemp = 0.0;
    private $numReadings;

    public function setMaxTemp($maxTemp)
    {
        $this->maxTemp = $maxTemp;
    }

    public function getMaxTemp()
    {
        return $this->maxTemp;
    }

    public function setMinTemp($minTemp)
    {
        $this->minTemp = $minTemp;
    }

    public function getMinTemp()
    {
        return $this->minTemp;
    }
    public function getSumTemp()
    {
        return $this->sumTemp;
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo = $event->getDonneesMeteo();
        $temp         = $donneesMeteo->getTemperature();
        $this->sumTemp += $temp;
        $this->numReadings++;

        if ($temp > $this->maxTemp) {
            $this->maxTemp = $temp;
        }

        if ($temp < $this->minTemp) {
            $this->minTemp = $temp;
        }
    }

    public function getNewValues()
    {
        $temperature = ($this->sumTemp / $this->numReadings) . "/" . $this->maxTemp . "/" . $this->minTemp;

        return array("AvgMaxMinTemperature" => $temperature);
    }
}

5) Rattachons nos listeners à l’observable

Nous avons presque fini. A présent, il faut indiquer à nos listeners qu’ils doivent appeler la fonction update à chaque fois que l’événement donnees_meteo.update est dispatché.

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="dp.donnees_meteo.class">DP\SfObserverBundle\Entity\Observable\DonneesMeteo</parameter>
        <parameter key="dp.affichage_condition_listener.class">DP\SfObserverBundle\Listener\AffichageConditionsListener</parameter>
        <parameter key="dp.affichage_prevision_listener.class">DP\SfObserverBundle\Listener\AffichagePrevisionsListener</parameter>
        <parameter key="dp.affichage_stat_listener.class">DP\SfObserverBundle\Listener\AffichageStatsListener</parameter>
    </parameters>
    <services>
        <service id="dp.donnees_meteo"
                 class="%dp.donnees_meteo.class%">
            <call method="setDispatcher">
             <argument type="service" id="event_dispatcher" />
            </call>
        </service>
        <service id="dp.affichage_condition_listener"
                 class="%dp.affichage_condition_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
        <service id="dp.affichage_prevision_listener"
                 class="%dp.affichage_prevision_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
        <service id="dp.affichage_stat_listener"
                 class="%dp.affichage_stat_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
    </services>
</container>

Ici, nous indiquons à Symfony  que nous avons trois listeners (avec le tag kernel.event_listener), qui se déclenche à l’événement donnees_meteo.update, et que nous voulons exécuter la méthode update lorsqu’il est dispatché.

6) Notre contrôleur

That’s it. Il n’y a plus qu’à appeler le chef d’orchestre.

<?php

namespace DP\SfObserverBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller
{

    /**
     * @Route("/")
     * @Template()
     */
    public function indexAction()
    {
        // déclaration des paramètres des écouteurs
        $affichageConditionsListener   = $this->container->get('dp.affichage_condition_listener');
        $affichageStatsListener   = $this->container->get('dp.affichage_stat_listener');
        $affichagePrevisionsListener   = $this->container->get('dp.affichage_prevision_listener');
        $affichageStatsListener->setMinTemp(10);
        $affichageStatsListener->setMaxTemp(30);
        $affichagePrevisionsListener->setCurrentPressure(1000);

        $donneesMeteo = $this->container->get('dp.donnees_meteo');
        $donneesMeteo->setMesures(25, 10, 1200); // le sujet met à jour ses données

        return array(
            'affichageConditions' => $affichageConditionsListener->getNewValues(),
            'affichageStats'      => $affichageStatsListener->getNewValues(),
            'affichagePrevisions' => $affichagePrevisionsListener->getNewValues()
        );
    }
}

Je sette les valeurs nécessaires pour les différents services (affichageStatsListener et affichagePrevisionsListener). Je mets à jour les données, météo… et c’est tout! En le faisant, je dispatche l’événement donnees_meteo.update, et je mets à jour mes listeners.

Et si l’on veut utiliser des subscribers à la place de listeners?

Bonne question! Un point pour le monsieur du fond!

Déjà, les subscribers, qu’est-ce que c’est? Eh bien, c’est la même chose que les listeners, mais il est possible, dans chacun d’eux, de paramétrer plusieurs événements en indiquant à chaque fois la méthode appelée. Et il est même possible d’indiquer un ordre de priorité pour que les fonctions appelées ne se marchent pas sur les pieds.

namespace DP\SfObserverBundle\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use DP\SfObserverBundle\Event\DonneesMeteoEvent;
use DP\SfObserverBundle\DPSfObserverEvents;

class AffichageConditionsSubscriber implements EventSubscriberInterface
{
    private $temperature;
    private $humidity;
    private $pressure;

    public function getTemperature()
    {
        return $this->temperature;
    }

    public function getHumidity()
    {
        return $this->humidity;
    }

    public function getPressure()
    {
        return $this->pressure;
    }

    public function setTemperature($temperature)
    {
        $this->temperature = $temperature;
    }

    public function setHumidity($humidity)
    {
        $this->humidity = $humidity;
    }

    public function setPressure($pressure)
    {
        $this->pressure = $pressure;
    }

    static public function getSubscribedEvents()
    {
        return array(
            DPSfObserverEvents::MESURES_UPDATED => array('update', 0),
        );
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo      = $event->getDonneesMeteo();
        $this->temperature = $donneesMeteo->getTemperature();
        $this->humidity    = $donneesMeteo->getHumidity();
        $this->pressure    = $donneesMeteo->getPressure();
    }

    public function getNewValues()
    {
        return array('temperature' => $this->temperature, 'humidite' => $this->humidity, 'pression' => $this->pressure);
    }
}

Ici, voyez comment on déclare les évenements dans la fonction getSubscribedEvents.

Imaginez que j’ai une autre fonction (appelée ici methodeAAppeler2) qui va être appellée lorsque nous dispatcherons l’événement donnees_meteo.update, mais que je veux qu’il soit appelée après la méthode update.
Imaginez également qu’il y a un autre événement (appelons le evenement2) qui va se produire lorsque nous le dispatcherons, et qu’il devra appeler une méthode appelée methodeAAppeler3.

Je ferai simplement ainsi:

static public function getSubscribedEvents()
    {
        return array(
            DPSfObserverEvents::MESURES_UPDATED => array('update', 0),
            'evenement2' => array('methodeAAppeler2', 0)
        );
    }

Le second chiffre de l’array me permet de dire que update sera appelée en premier.

Personnellement, j’aime bien les subscribers, mais j’utilise les listeners si je dois faire une toute petite application.

Voyons à présent nos deux autre subscribers:

namespace DP\SfObserverBundle\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use DP\SfObserverBundle\Event\DonneesMeteoEvent;

class AffichagePrevisionsSubscriber implements EventSubscriberInterface
{
    private $currentPressure;
    private $lastPressure;
    private $prevision;
    
    static public function getSubscribedEvents()
    {
        return array(
            'donnees_meteo.update' => array('update', 0),
        );
    }

    public function getCurrentPressure()
    {
        return $this->currentPressure;
    }

    public function setCurrentPressure($currentPressure)
    {
        $this->currentPressure = $currentPressure;
    }

    public function getLastPressure()
    {
        return $this->lastPressure;
    }

    public function getPrevision()
    {
        return $this->prevision;
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo          = $event->getDonneesMeteo();
        $this->lastPressure    = $this->currentPressure;
        $this->currentPressure = $donneesMeteo->getPressure();
    }

    public function getNewValues()
    {
        if ($this->currentPressure > $this->lastPressure) {
            $this->prevision = "Improving weather on the way!";
        } else if ($this->currentPressure == $this->lastPressure) {
            $this->prevision = "More of the same";
        } else if ($this->currentPressure < $this->lastPressure) {
            $this->prevision = "Watch out for cooler, rainy weather";
        }

        return array('prevision' =>$this->prevision);
    }
}
namespace DP\SfObserverBundle\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use DP\SfObserverBundle\Event\DonneesMeteoEvent;

class AffichageStatsSubscriber implements EventSubscriberInterface
{
    private $maxTemp;
    private $minTemp;
    private $sumTemp = 0.0;
    private $numReadings;
    
    static public function getSubscribedEvents()
    {
        return array(
            'donnees_meteo.update' => array('update', 0),
        );
    }

    public function setMaxTemp($maxTemp)
    {
        $this->maxTemp = $maxTemp;
    }

    public function getMaxTemp()
    {
        return $this->maxTemp;
    }

    public function setMinTemp($minTemp)
    {
        $this->minTemp = $minTemp;
    }

    public function getMinTemp()
    {
        return $this->minTemp;
    }
    
    public function getSumTemp()
    {
        return $this->sumTemp;
    }

    public function update(DonneesMeteoEvent $event)
    {
        $donneesMeteo = $event->getDonneesMeteo();
        $temp         = $donneesMeteo->getTemperature();
        $this->sumTemp += $temp;
        $this->numReadings++;

        if ($temp > $this->maxTemp) {
            $this->maxTemp = $temp;
        }

        if ($temp < $this->minTemp) {
            $this->minTemp = $temp;
        }
    }

    public function getNewValues()
    {
        $temperature = ($this->sumTemp / $this->numReadings) . "/" . $this->maxTemp . "/" . $this->minTemp;

        return array("AvgMaxMinTemperature" => $temperature);
    }
}

N’oublions pas de déclarer  nos subscribers en tant que services:

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="dp.donnees_meteo.class">DP\SfObserverBundle\Entity\Observable\DonneesMeteo</parameter>
        <parameter key="dp.affichage_condition_listener.class">DP\SfObserverBundle\Listener\AffichageConditionsListener</parameter>
        <parameter key="dp.affichage_prevision_listener.class">DP\SfObserverBundle\Listener\AffichagePrevisionsListener</parameter>
        <parameter key="dp.affichage_stat_listener.class">DP\SfObserverBundle\Listener\AffichageStatsListener</parameter>
        <parameter key="dp.affichage_condition_subscriber.class">DP\SfObserverBundle\Subscriber\AffichageConditionsSubscriber</parameter>
        <parameter key="dp.affichage_prevision_subscriber.class">DP\SfObserverBundle\Subscriber\AffichagePrevisionsSubscriber</parameter>
        <parameter key="dp.affichage_stat_subscriber.class">DP\SfObserverBundle\Subscriber\AffichageStatsSubscriber</parameter>
    </parameters>
    <services>
        <service id="dp.donnees_meteo"
                 class="%dp.donnees_meteo.class%">
            <call method="setDispatcher">
             <argument type="service" id="event_dispatcher" />
            </call>
        </service>
        <service id="dp.affichage_condition_listener"
                 class="%dp.affichage_condition_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
        <service id="dp.affichage_prevision_listener"
                 class="%dp.affichage_prevision_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
        <service id="dp.affichage_stat_listener"
                 class="%dp.affichage_stat_listener.class%">
            <tag name="kernel.event_listener" event="donnees_meteo.update" method="update" />
        </service>
        <service id="dp.affichage_condition_subscriber"
                 class="%dp.affichage_condition_subscriber.class%">
            <tag name="kernel.event_subscriber" />
        </service>
        <service id="dp.affichage_prevision_subscriber"
                 class="%dp.affichage_prevision_subscriber.class%">
            <tag name="kernel.event_subscriber" />
        </service>
        <service id="dp.affichage_stat_subscriber"
                 class="%dp.affichage_stat_subscriber.class%">
            <tag name="kernel.event_subscriber" />
        </service>
    </services>
</container>

Et pour finir, le contrôleur

Il ne va que très peu différent de notre contrôleur précédent:

namespace DP\SfObserverBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller
{
    /**
     * @Route("/subscriber")
     * @Template("DPSfObserverBundle:Default:index.html.twig")
     */
    public function indexSubscriberAction()
    {
        // déclaration des subscribers
        $affichageConditionsSubscriber   = $this->container->get('dp.affichage_condition_subscriber');
        $affichageStatsSubscriber   = $this->container->get('dp.affichage_stat_subscriber');
        $affichagePrevisionsSubscriber   = $this->container->get('dp.affichage_prevision_subscriber');

        $affichageStatsSubscriber->setMinTemp(10);
        $affichageStatsSubscriber->setMaxTemp(30);
        $affichagePrevisionsSubscriber->setCurrentPressure(1000);

        $donneesMeteo = $this->container->get('dp.donnees_meteo');
        $donneesMeteo->setMesures(25, 10, 1200); // le sujet met à jour ses données

        return array(
            'affichageConditions' => $affichageConditionsSubscriber->getNewValues(),
            'affichageStats'      => $affichageStatsSubscriber->getNewValues(),
            'affichagePrevisions' => $affichagePrevisionsSubscriber->getNewValues()
        );

    }
}

 

Comme d’habitude, le dépôt github: https://github.com/jpsymfony/dp-observer.git.

Le répertoire correspondant est SfObserverBundle.

Rédigé par

2 comments

  1. Salut mr on peut pas déclencher les évènements sans actualiser la page index ? comme par exemple le messages de Facebook

    1. Hello, i tu veux faire ça, il faut déclencher des events), qui pourraient publier des messages sur un exchange rabbitMQ. Tu auras une queue qui écoutera les messages correspondant à une routing key, et en js, tu pourras indiquer la notification dans la barre de l’utilisateur. Mieux vaut gérer ça dans un système de file d’attente telle que rabbitMQ plutôt qu’en synchron (pour éviter la surcharge serveur), surtout que ça dépile rapidement

      Du coup, le workflow serait:
      1) une personne publie un message
      2) un event est lancé (avec le dispatch)
      3) un message est publié dans rabbitMQ
      4) un consumer javascript récupère le message et met à jour la vue (https://www.rabbitmq.com/tutorials/tutorial-one-javascript.html)
      5) un consumer Symfony enregistre en base le message envoyé par l’utilisateur (http://blog.vincent-chalamon.fr/implementer-rabbitmq-dans-une-api-symfony-2/). Préfère ce bundle plus récent: https://github.com/php-amqplib/RabbitMqBundle

      Sinon, pour un système de notifications en direct, il me semble que node.js est plus approprié

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.