Le compilerPass est extrêmement utile pour faire appel aux variables de l’environnement courant, bien entendu, mais aussi pour les services tagués.
En effet, Symfony nous permet, lorsque le compilerPass s’exécute (après ceux de tous les autres bundles), d’appeler un service en particulier, ainsi que l’une de ses fonctions, et de l’exécuter en allant dynamiquement rechercher tous les services ayant un tag personnalisé. C’est parti!
Un service de paiement
Créons deux services de paiement: Be2bill et Paypal.
namespace App\PortalBundle\Services; class Be2bill extends AbstractPaymentService { public function getHtml($url, $parameters, $displaySubmitBtn, $message) { return 'Be2BillServiceForm'; } /** * @inheritdoc */ public function addFail() { // send mail and log error } /** * @inheritdoc */ public function getLabel() { return 'Be2bill'; } }
namespace App\PortalBundle\Services; class Paypal extends AbstractPaymentService { public function getHtml($url, $parameters, $displaySubmitBtn, $message) { return 'PaypalServiceForm'; } /** * @inheritdoc */ public function addFail() { // send mail and log error } /** * @inheritdoc */ public function getLabel() { return 'Paypal'; } }
Bien évidemment, on imagine, lorsqu’on fait appel à getHtml, qu’il vont générer le formulaire adéquat et nous le renvoyer. La fonction getLabel est très importante, nous allons le voir d’ici peu.
Ces deux services de paiement étendent une classe abstraite:
namespace App\PortalBundle\Services; abstract class AbstractPaymentService implements GenericPaymentServiceInterface { /** * @inheritDoc */ abstract public function getHtml($url, $parameters, $displaySubmitBtn, $message); /** * @inheritDoc */ abstract function addFail(); /** * @inheritdoc */ public function isTypeMatch($labelClass) { return $labelClass === $this->getLabel(); } /** * @inheritDoc */ abstract function getLabel(); }
Il y a une fonction très intéressante, isTypeMatch, qui permet, en fonction du label passé, de savoir s’il correspond à l’un ou l’autre des services de paiment.
Cette classe abstraite implémente une interface qui va rapidement nous servir:
namespace App\PortalBundle\Services; interface GenericPaymentServiceInterface { /** * @param $url * @param $parameters * @param $displaySubmitBtn * @param $message * @return mixed */ public function getHtml($url, $parameters, $displaySubmitBtn, $message); /** * @return mixed */ public function addFail(); /** * @param $labelClass * @return GenericPaymentServiceInterface */ public function isTypeMatch($labelClass); /** * @return string LabelClass */ public function getLabel(); }
Créons un container de services de paiement
Ce n’est pas tout, nous avons deux services de paiement, une classe abstraite et une interface. A présent, j’aimerais bien avoir un service qui, dynamiquement, récupère tous les services de paiement. Pour cela, je vais d’abord créer ce service de paiement « container »:
namespace App\PortalBundle\Services; class PaymentContainerService { private $paymentServices; public function __construct() { $this->paymentServices = array(); } public function addPaymentService(GenericPaymentServiceInterface $paymentService) { $this->paymentServices[] = $paymentService; } public function getPaymentServices() { return $this->paymentServices; } }
Très simple, il permet de récupérer tous les services de paiement, mais aussi d’en ajouter. Remarquez que le type du service de paiement est l’interface GenericPaymentServiceInterface.
Créons un manager de services de paiement
C’est quasiment la dernière étape. Ce manager de service aura comme dépendance le PaymentContainerService qui contiendra tous les services de paiement existants. Il pourra, selon le service de paiement demander, déterminer quel service de paiement il doit retourner.
namespace App\PortalBundle\Services; use App\PortalBundle\Services\Interfaces\PaymentManagerServiceInterface; class PaymentManagerService implements PaymentManagerServiceInterface { /** * @var PaymentContainerService */ private $paymentContainerService; /** * @inheritdoc */ public function getPaymentClass($paymentClassLabel) { foreach ($this->paymentContainerService->getPaymentServices() as $paymentService) { if ($paymentService->isTypeMatch($paymentClassLabel)) { return $paymentService; } } throw new \Exception('None payment service found for class ' . $paymentClassLabel); } /** * @inheritdoc */ public function setPaymentContainerService(PaymentContainerService $paymentContainerService) { $this->paymentContainerService = $paymentContainerService; } }
Comme vous le voyez, on fait appel à un label de service de paiement. A ce moment là, le paiementContainerService qui contient tous les services de paiement (donc ici Be2Bill et Paypal) dans un array, est utilisé. On boucle dessus, et la fameuse fonction isTypeMatch de l’abstractPaymentService est appelée pour savoir s’il faut retourner une instance de Be2Bill ou une instance de Paypal.
Déclarons les services
A présent, pour que la magie opère, il est temps d’enregistrer nos 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"> <services> <service id="app_portal.be2bill_payment_service" class="App\PortalBundle\Services\Be2bill"> <tag name="app_portal.payment_services"/> </service> <service id="app_portal.paypal_payment_service_" class="App\PortalBundle\Services\Paypal"> <tag name="app_portal.payment_services"/> </service> <service id="app_portal.payment_container_service" class="App\PortalBundle\Services\PaymentContainerService"> </service> <service id="app_portal.payment_manager_service" class="App\PortalBundle\Services\PaymentManagerService"> <call method="setPaymentContainerService"> <argument type="service" id="app_portal.payment_container_service"/> </call> </service> </services> </container>
Nous avons déclaré nos deux services de paiement, avec un tag personnalisé: app_portal.payment_services, puis notre service PaymentContainerService qui contiendra les deux services de paiement, puis notre PaymentManagerService qui a comme dépendance le PaymentContainerService. Ouf!
Il ne reste plus qu’une étape, déclarer notre compilerPass, et le coder bien sûr!
Notre compilerPass personnalisé
namespace App\PortalBundle\DependencyInjection\CompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Reference; class PaymentCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->has('app_portal.payment_container_service')) { return; } $definition = $container->findDefinition( 'app_portal.payment_container_service' ); $taggedServices = $container->findTaggedServiceIds( 'app_portal.payment_services' ); foreach ($taggedServices as $id => $tags) { $definition->addMethodCall( 'addPaymentService', array(new Reference($id)) ); } } }
C’est là que ça devient intéressant. Nous demandons à Symfony de récupérer notre service app_portal.payment_container_service. Puis, s’il le trouve, de récupérer tous les services portant le tag app_portal.payment_services (soit Be2Bill et Paypal).
Enfin, sur chacun de ces services tagués, nous appelons la méthode addPaymentService du service PaymentContainerService afin qu’il ait dans son array les deux services de paiement.
Pour finir, il faut enregistrer notre compilerPass directement dans AppPortalBundle.php:
namespace App\PortalBundle; use App\PortalBundle\DependencyInjection\CompilerPass\PaymentCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class AppPortalBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new PaymentCompilerPass()); } }
Bon, la démonstration est quasi terminée, mais on va aller jusqu’au bout, ce sera plus amusant. Nous allons configurer nos services de paiement dans config_dev.yml et config_prod.yml (ici, seulement config_dev.yml pour la démonstration):
app_portal: payment_organisms: default: Be2bill Be2bill: identifier: MYWEBSITE password: 123456789 url: https://secure-test.be2bill.com/front/form/process Paypal: identifier: mywebsite@mywebsite.com password: ~ url: http://www.sandbox.paypal.com/cgi-bin/webscr
Bon, ce n’est pas tout ça, mais si vous allez sur n’importe quelle page de votre site, ça ne fonctionnera plus, car il vous faut déclarer ces paramètres dans votre fichier Configuration.php du répertoire DependencyInjection de votre bundle:
namespace App\PortalBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; /** * This is the class that validates and merges configuration from your app/config files * * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} */ class Configuration implements ConfigurationInterface { /** * {@inheritdoc} */ public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('app_portal'); $rootNode ->children() ->arrayNode('payment_organisms') ->children() ->variableNode('default') ->cannotBeEmpty()->end() ->arrayNode('Be2bill') ->children() ->variableNode('identifier') ->cannotBeEmpty() ->end() ->variableNode('password') ->cannotBeEmpty() ->end() ->variableNode('url') ->cannotBeEmpty() ->end() ->end() ->end() ->arrayNode('Paypal') ->children() ->variableNode('identifier') ->cannotBeEmpty() ->end() ->variableNode('password') ->end() ->variableNode('url') ->cannotBeEmpty() ->end() ->end() ->end() ->end() ->end() ->end() ->end(); return $treeBuilder; } }
Et puis, nous allons faire en sorte que ces valeurs de configuration soient disponibles dans le container en les déclarant dans AppPortalExtension.php:
namespace App\PortalBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; /** * This is the class that loads and manages your bundle configuration * * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} */ class AppPortalExtension extends Extension { /** * {@inheritdoc} */ public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('twig.xml'); $loader->load('services.xml'); $container->setParameter('app_portal.payment_organisms', $config['payment_organisms']); } }
Création d’un controller de paiement
Imaginons que nous arrivons, via un contrôleur, directement sur une page avec le bon formulaire selon que l’on ait indiqué dans la configuration que le service de paiement par défaut est Be2Bill ou Paypal (bien évidemment, dans la réalité, vous arriveriez sur une page où la personne choisirait via une option le mode de paiement souhaité, puis validerait, et par une requête ajax, le bon formulaire serait récupéré, posté, et vous arriveriez sur la page de paiement du service de paiement sélectionné. Mais nous allons faire simple ici):
namespace App\PortalBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; class PaymentController extends Controller { /** * @Route("/payment", name="app_portal_payment") * @Template("AppPortalBundle:Payment:form.html.twig") */ public function indexAction() { $values = array( 'client_id' => 1, 'order_id' => 'REF1234567', 'amount' => 5.0, 'description' => 'my description', ); return array( 'values' => $values, 'displaySubmitBtn' => false, 'message' => 'payment', 'createAlias' => true, ); } }
La vue du formulaire de paiement
Nous arrivons donc à une vue qui doit nous donner un formulaire:
{% extends '@AppPortal/layout.html.twig' %} {% block content %} {{ payment_form(values, displaySubmitBtn, message, createAlias)|raw }} {% endblock %}
Bien évidemment, il nous faut une fonction twig pour générer ce formulaire, puisqu’il est différent selon l’organisme de paiement. C’est parti pour créer cette fonction, en commençant par son service (déclaré ici dans un fichier twig.xml qui est déjà déclaré dans AppPortalExtension, voyez plus haut):
<?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"> <services> <service id="app_portal.twig.payment_form_extension" class="App\PortalBundle\Twig\PaymentForm"> <argument type="service" id="app_portal.payment_manager_service" /> <argument>%app_portal.payment_organisms%</argument> <tag name="twig.extension" /> </service> </services> </container>
Puis par son code:
<?php namespace App\PortalBundle\Twig; use App\PortalBundle\Services\GenericPaymentServiceInterface; use App\PortalBundle\Services\PaymentManagerService; use Symfony\Component\Translation\Exception\NotFoundResourceException; class PaymentForm extends \Twig_Extension { /** * @var string $defaultPaymentOrganism */ private $defaultPaymentOrganism = null; /** * @var PaymentManagerService */ private $paymentManagerService; /** * @var array */ private $configPayment; public function __construct(PaymentManagerService $paymentManagerService, $configPayment) { $this->paymentManagerService = $paymentManagerService; $this->configPayment = $configPayment; $this->defaultPaymentOrganism = ucfirst($this->configPayment['default']); } public function getFunctions() { return array( new \Twig_SimpleFunction('payment_form', array($this ,'getPaymentForm')), ); } public function getName() { return 'payment_form'; } public function getPaymentForm($values, $displaySubmitBtn, $message) { $clientId = $values['client_id']; $description = $values['description']; $orderId = $values['order_id']; $amount = intval($values['amount'] * 100); $parameters = array( 'AMOUNT' => $amount, 'CLIENTIDENT' => $clientId, 'DESCRIPTION' => $description, 'ORDERID' => $orderId, ); $paymentSolution = $this->paymentManagerService->getPaymentClass($this->defaultPaymentOrganism); if (!$paymentSolution instanceof GenericPaymentServiceInterface) { throw new NotFoundResourceException('no GenericPaymentService found for ' . $this->defaultPaymentOrganism); } return $paymentSolution->getHtml($this->getUrl(), $parameters, $displaySubmitBtn, $message); } private function getUrl() { if (!isset($this->configPayment[$this->defaultPaymentOrganism])) { throw new NotFoundResourceException('no configPayment for ' . $this->defaultPaymentOrganism); } return $this->configPayment[$this->defaultPaymentOrganism]['url']; } }
Nous récupérons donc dans le constructeur le mode de paiement par défaut et le paymentManagerService.
La fonction getPaymentForm récupère la bonne solution de paiement avec la fonction getPaymentClass, puis elle appelle son code html en lui passant l’url qui lui correspond et qu’elle a récupéré via le fichier config_dev.yml ou config_prod.yml.
Et c’est terminé! La boucle est donc bouclé.
Pour résumer:
- Nous avons créé deux services de paiement, Be2Bill et Paypal, qui étendaient une classe abstraite qui possédait la fonction isTypeMatch
- Nous avons créé un service container de solutions de paiement, dont l’array est rempli via le compilerPass qui fait appel à tous les services de paiement tagués
- Nous avons créé un service de paiement manager qui a le service de paiement container injecté et qui récupère le bon service de paiement selon le label passé en paramètre
- Nous avons créé une fonction twig qui va lire la configuration pour connaître le service de paiement par défaut, récupérer les informations associées, faire appel au service de paiement manager qui va récupérer le bon service de paiement (Be2Bill ou Paypal) et qui va retourner le formulaire adéquat.
Pour finir, une petite preuve en image que le service container de solutions de paiement a bien dans son array Be2Bill et Paypal, et qu’en changeant le service de paiement par défaut dans le fichier config_dev/prod.yml, on retourne bien le bon formulaire:
namespace App\PortalBundle\Services; use App\PortalBundle\Services\Interfaces\PaymentManagerServiceInterface; class PaymentManagerService implements PaymentManagerServiceInterface { /** * @var PaymentContainerService */ private $paymentContainerService; /** * @inheritdoc */ public function getPaymentClass($paymentClassLabel) { dump($this->paymentContainerService->getPaymentServices()); foreach ($this->paymentContainerService->getPaymentServices() as $paymentService) { if ($paymentService->isTypeMatch($paymentClassLabel)) { return $paymentService; } } throw new \Exception('None payment service found for class ' . $paymentClassLabel); }
/Users/johnsaulnier/Sites/Formation/src/App/PortalBundle/Services/PaymentManagerService.php:19: array (size=2) 0 => object(AppPortalBundleServicesBe2bill_0000000014c5339200000001443383dd339f9c796b92ccc065ed88f1578ff126)[504] private 'valueHolder57e81146837ac391652820' => null private 'initializer57e81146837e0494172812' => object(Closure)[505] public 'this' => object(appDevDebugProjectContainer)[391] ... public 'parameter' => array (size=2) ... 1 => object(AppPortalBundleServicesPaypal_0000000014c5339100000001443383dd339f9c796b92ccc065ed88f1578ff126)[506] private 'valueHolder57e8114685666269586652' => null private 'initializer57e811468569c127818235' => object(Closure)[507] public 'this' => object(appDevDebugProjectContainer)[391] ... public 'parameter' => array (size=2) ...
<?php namespace App\PortalBundle\Twig; [...] public function getPaymentForm($values, $displaySubmitBtn, $message) { [...] $paymentSolution = $this->paymentManagerService->getPaymentClass($this->defaultPaymentOrganism); if (!$paymentSolution instanceof GenericPaymentServiceInterface) { throw new NotFoundResourceException('no GenericPaymentService found for ' . $this->defaultPaymentOrganism); } var_dump($paymentSolution); return $paymentSolution->getHtml($this->getUrl(), $parameters, $displaySubmitBtn, $message); }
/Users/johnsaulnier/Sites/Formation/src/App/PortalBundle/Twig/PaymentForm.php:65: object(AppPortalBundleServicesBe2bill_0000000014c5339200000001443383dd339f9c796b92ccc065ed88f1578ff126)[504] private 'valueHolder57e81146837ac391652820' => object(App\PortalBundle\Services\Be2bill)[834] private 'initializer57e81146837e0494172812' => null
Changez le défault en Paypal et ce sera le service Paypal qui sera retourné:
app_portal: payment_organisms: default: Paypal
/Users/johnsaulnier/Sites/Formation/src/App/PortalBundle/Twig/PaymentForm.php:65: object(AppPortalBundleServicesPaypal_000000002cf91a79000000016782415d339f9c796b92ccc065ed88f1578ff126)[4957] private 'valueHolder57e812a9cf02d971372845' => object(App\PortalBundle\Services\Paypal)[4960] private 'initializer57e812a9cf059096866580' => null