Un PreExecute pour votre Controller via des Listeners

Voici une petite aide pour créer un PreExecute de votre Controller. Qu’est-ce que ça veut dire ? Avant d’exécuter votre Controller, vous voudriez faire quelque chose ! C’est possible, grâce aux Events!

Maj: Source sur Github

Un event, évènement, repose sur le pattern Observateur. Exemple:

  • Quelqu’un fait quelque chose, tous ceux qui l’observent sont immédiatement prévenus !
  • Un objet Timer émet toutes les 5 secondes un top. Des objets observateurs s’enregistrent au près du Timer pour être prévenus dès qu’il y a un top !

Observateur – définit une relation entre objets de type un-à-plusieurs, de façon que, lorsque un objet change d’état, tous ceux qui en dépendent en soient notifiés et soient mis à jour automatiquement.

Symfony2 implémente ce pattern.

Entrons dans le vif du sujet, imaginons que nous ayons un site e-commerce à concevoir, et que nous permettions au client de pouvoir remplir son panier soit en étant enregistré dans la base de données, soit avec une session.

Soit on colle des IF partout dans son Controller et on fait en sorte que ça fonctionne, soit on utilise le pattern Stratégie.

Le pattern Stratégie définit une famille d’algorithmes, encapsule chacun d’eux et les rend interchangeables.
Stratégie permet à l’algorithme de varier indépendamment des clients qui l’utilisent.

Un schéma vaut mieux qu’un long discours:

Notre classe Panier comporte une propriété de type GestionPanier, de telle sorte que dans notre code nous pourrons écrire:

<?php
 class Panier {
	protected $gestionPanier;
	/**
	*
	* Ajouter un produit dans le panier 	 
	* @param IDproduit $IDProd 	 
	* @return Product 	 
	*/ 	
	function ajouterProduit($IDProd)
 	{ 		
		return $this->gestionPanier->ajouterProduit($IDProd);
	}
}
 

On délègue la gestion du panier aux classes GestionBaseDeDonnees et GestionSession. Pour ajouter un produit, le panier demande à l’objet référencé par gestionPanier de le faire à sa place.

Peu importe de quelle sorte d’objet il s’agit, ce qui nous intéresse c’est qu’il sait ajouter un produit !

Passé cette étape, le reste est trivial. Voici ce qu’il reste à faire:

  • Ecrire le code qui implémente l’interface GestionPanier
  • Ajouter deux Listeners (observateurs) qui écoutent les évènements core.controller et security.interactive_login
  • Ajouter une méthode preExecute dans notre Controller

Première étape

L’interface

<php
namespace CCC\PanierBundle\Model;

interface GestionPanierInterface
{

	/**
	 *
	 * Ajouter un produit dans le panier
	 * @param IDproduit $IDProd
	 * @return Product
	 */
	function ajouterProduit($IDProd);

}

GestionBaseDeDonnees:

<?php 
namespace CCC\PanierBundle\Model; 

class GestionBaseDeDonnees implements GestionPanierInterface 
{ 	
	private $em; 	
	private $securite; 	
	private $user; 	 
	
	public function __construct(EntityManager $em, SecurityContext $securite) 	{ 		        $this->em = $em;
			$this->securite = $securite;
			$this->user = $this->securite->getToken()->getUser();
	}

	/**
	 *
	 * Ajouter un produit dans le panier
	 * @param IDproduit $IDProd
	 * @return Product
	 */
	public function ajouterProduit($id)
	{
		$panier = $this->checkPanierExist();

		// Récupérer le produit via le slug
		$query = $this->em->createQuery('SELECT u, v FROM CCC\PanierBundle\Entity\Product u LEFT JOIN u.productType v where u.productType = v.id and u.id= ?1');
		$query->setParameter(1, $id);

		try {
			$produit = $query->getSingleResult();
		} catch (NoResultException $e) {
			$produit = null;
		}

		// Le reste de votre code et votre implémentation

		return $produit;
	}
}

GestionSession:

<?php 
namespace CCC\PanierBundle\Model; 

class GestionSession implements GestionPanierInterface 
{ 	
	private $session; 	
	private $em; 	

	public function __construct(Session $session, EntityManager $em) 	
	{ 		
		$this->session = $session;
		$this->em = $em;
	}

	/**
	 * Retourne un panier
	 * @return panier
	 *
	 */
	function checkPanierExist()
	{
		return $this->creationPanier();
	}

	private function creationPanier(){
	   if (!$this->session->has('panier')){
	   		$this->session->set('panier', array());
	   }
	   return $this->session->get('panier');
	}

	/**
	 *
	 * Ajouter un produit dans le panier
	 * @param IDproduit $IDProd
	 * @return Product
	 */
	public function ajouterProduit($id)
	{
		$panier = $this->checkPanierExist();

		$produit = $this->em->getRepository('CSF\CartBundle\Entity\Products')
						->getProductByID($IDProd);

		$panier[] = array(
					'id' => $ligne,
					'quantite' => 1,
					'calculPrix' => $prod->getPrix(),
					'product' => array(
						'id' => $IDProd,
						'name' => $prod->getName(),
						'slug' => $prod->getSlug(),
						'prix' => $prod->getPrix(),
						'description' => $prod->getDescription(),
						'productType' => array(
							'id'   => $prod->getProductType()->getId(),
							'name' => $prod->getProductType()->getName(),
						)
					));

		// Le reste de votre code et votre implémentation

		return $produit;
	}

}

Et le code pour Panier:

<?php 
namespace CCC\PanierBundle\Model; 

class Panier 
{ 	
	protected $gestionPanier;
	// Par défaut on considère que l'utilisateur n'est pas identifié 	
	// On injecte les services 	
	public function __construct(GestionPanierInterface $handle, Session $session) 	
	{ 		
		$this->gestionPanier = $handle;
	}

	/*
	* Permets de modifier la classe implémentant l'interface
	* Si l'utilisateur est identifié via la base de données
	*/
	public function setHandleCommande(GestionPanierInterface $handle)
	{
		$this->gestionPanier = $handle;
	}

	/**
	 *
	 * Ajouter un produit dans le panier
	 * @param IDproduit $IDProd
	 * @return Product
	 */
	function ajouterProduit($IDProd)
	{
		return $this->gestionPanier->ajouterProduit($IDProd);
	}
}
 

Deuxième étape

On va créer les Listeners, pour cela je les place dans un dossier dans mon bundle que je nomme Listener, ou comme il vous plaira.

Les Listeners si vous vous rappelez ce que j’ai dis en haut, se contentent une fois qu’ils sont enregistrés au près de l’objet à observer, d’écouter -attendre- que l’objet en question émette son évènement.

Ici en premier lieu, on veut écouter l’évènement qui se propage lorsque l’utilisateur s’identifie sur le site:

<?php 
namespace CCC\PanierBundle\Listener; 

use Symfony\Component\HttpFoundation\Session; 
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;

class GestionUtilisateurListener 
 {		 	
	 protected $session; 
	 
	 /* 	
	 * On injecte au constructeur le service Session 	
	 */ 	
	 public function __construct(Session $session) 	
	{ 		
		$this->session = $session;
	}

	/*
	* La méthode appelée par l'objet qui diffuse son évènement
	*/
	public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
		// L'utilisateur est identifié, on signale qu'il faudra utiliser à chaque fois
		// La classe implémentant la base de données
        $this->session->set('panier_session', false);
    }
} 

Le second Listener va écouter l’évènement diffusé avant l’exécution du Controller:

<?php 
namespace CSF\CartBundle\Listener; 

use Symfony\Component\HttpKernel\HttpKernelInterface; 
use Symfony\Component\HttpKernel\Event\FilterControllerEvent; 
class GestionControllerListener 
{	 	
	public function onCoreController(FilterControllerEvent $event) 	
	{ 	    
	if (HttpKernelInterface::MASTER_REQUEST === $event--->getRequestType()) {
            $_controller = $event->getController();
            if (isset($_controller[0]) && $_controller[0] instanceof OrderController ) {
                $controller = $_controller[0];
                $controller->preExecute();
            }
	    }

	}
} 

Dans le code, il est important de préciser quel Controller on vise sinon la méthode preExecute qui n’existe pas ailleurs provoquera une erreur.

Enfin on rajoute dans notre fichier pour l’Injection de Dépendances, tous les services que nous allons utiliser:

Je vous renvoi à la documentation officielle concernant l’Injection de Dépendances si vous ne savez pas comment cela fonctionne, il faudrait sinon y consacrer un article entier. Lien

parameters:
    panier.security.interactive_login_listener.class: CCC\PanierBundle\Listener\GestionUtilisateurListener
    panier.gestion.filter_controller_event.class: CCC\PanierBundle\Listener\GestionControllerListener
services:
   gestion.base_de_donnees:
      class: CCC\PanierBundle\Model\GestionBaseDeDonnees
      arguments: [@doctrine.orm.entity_manager, @security.context]
   gestion.session:
      class: CCC\PanierBundle\Model\GestionSession
      arguments: [@doctrine.orm.entity_manager]
   gestion.panier:
      class: CCC\PanierBundle\Model\Panier
      arguments: [@gestion.session, @session]

   panier.security.interactive_login_listener:
      class: %panier.security.interactive_login_listener.class%
      arguments: [@session]
      tags:
            -  { name: kernel.listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
   panier.gestion.filter_controller_event:
      class: %panier.gestion.filter_controller_event.class%
      tags:
            -  { name: kernel.listener, event: core.controller, method: onCoreController } 

Troisième étape

Il ne reste plus qu’à écrire la méthode preExecute dans notre Controller.

<?php
// ... le reste du controller
public function preExecute()
{
   $session = $this->get('session');

   if($session->has('panier_session')) // L'utilisateur est déjà passé sur le site ?
   {
     if($session->get('panier_session') == false && $this->get('security.context')->isGranted('ROLE_USER')) // Est-il identifié ?
     {
       // Oui, alors on utilisera la classe avec les méthodes de la base de données
       $this->get('gestion.panier')->setHandleCommande($this->get('gestion.base_de_donnees'));
     }else{
       // Non, alors il utilisera la session
       $session->set('panier_session', true);
       $this->get('gestion.panier')->setHandleCommande($this->get('gestion.session'));
     }
   }else{
       $session->set('panier_session', true);
       $this->get('gestion.panier')->setHandleCommande($this->get('gestion.session'));
   }
} 

Implémentation

Et maintenant dans une action de mon Controller, je peux faire quelque chose comme ça:

<?php

/**
* @Route("/Ajouter/{id}", name="ajout_panier")
* @Template()
* Ajouter un produit dans le panier
*/
public function ajouterAction($id)
{
     $gestion = $this->get('gestion.panier'); // On utilise notre classe Panier qui délègue les méthodes
     $produit = $gestion->ajouterProduit($id);

     $this->get('session')->setFlash('panier', $produit.' bien ajouté dans votre panier !');

     return new RedirectResponse($this->generateUrl('_panier'));

} 

Et c’est terminé !

Rappel:

Les Listeners : Ils écoutent des évènements et permettent de déclencher quelque chose. le Bundle FOSUserBundle utilise par exemple un Listener lorsque l’utilisateur s’identifie, pour mettre à jour sa dernière date de connexion.

La documentation à lire de SF2. Le concept du pattern Observateur sur Wiki.

Pattern Stratégie : Les classes de Gestion du panier représentent ce que peut faire un panier, de différentes manières (différentes façon d’ajouter un produit).

N’hésitez pas à laisser des commentaires ou si vous auriez fait autrement et plus efficace !

Je mettrai une version complète sur Github sûrement, elle sera un peu différente..!

4 Réponses à Un PreExecute pour votre Controller via des Listeners

  1. aymeric dit :

    Bonjour,
    Il est possible d avoir un code source fonctionnel.
    Merci

  2. spike31 dit :

    J’ai mis mes sources sur Github (en haut de l’article), attention c’est fonctionnel chez moi, mais je n’ai pas laissé les instructions pour installer tous les bundles que j’utilise !

  3. Ping : Digesting BasketBundle | Craft It Online!

  4. cordoval dit :

    Thanks so much for this article, I just wish you could use names in english and also that you would explain in more detail that there are actually two listeners and not just one. One for the user even and the other to delegate the job to certain subclass via a handle.

Répondre

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Twitter picture

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Connexion à %s

Suivre

Get every new post delivered to your Inbox.