This document is the contents page for "The Cookbook for Symfony 2.3". It lists over 60 sections that provide guidance on how to accomplish various tasks with Symfony, such as asset management, bundle organization, forms, security, logging, and more. Each section includes a brief title describing the task covered. The contents are distributed under an open license allowing for sharing and modification with attribution. Users are invited to report any errors or issues.
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100%(1)100% found this document useful (1 vote)
809 views336 pages
Symfony Cookbook 2.3 FR
This document is the contents page for "The Cookbook for Symfony 2.3". It lists over 60 sections that provide guidance on how to accomplish various tasks with Symfony, such as asset management, bundle organization, forms, security, logging, and more. Each section includes a brief title describing the task covered. The contents are distributed under an open license allowing for sharing and modification with attribution. Users are invited to report any errors or issues.
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 336
The Cookbook
for Symfony 2.3
generated on June 26, 2014 The Cookbook (2.3) This work is licensed under the Attribution-Share Alike 3.0 Unported license (http://creativecommons.org/ licenses/by-sa/3.0/). You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the following conditions: Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same, similar or a compatible license. For any reuse or distribution, you must make clear to others the license terms of this work. The information in this book is distributed on an as is basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author(s) nor SensioLabs shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work. If you find typos or errors, feel free to report them by creating a ticket on the Symfony ticketing system (http://github.com/symfony/symfony-docs/issues). Based on tickets and users feedback, this book is continuously updated. Contents at a Glance Comment utiliser Assetic pour grer vos ressources..............................................................................6 Comment minifier les JavaScripts et les feuilles de style avec YUI Compressor .................................... 11 Comment utiliser Assetic et les fonctions Twig pour optimiser les images ........................................... 13 Comment appliquer un filtre Assetic une extension de fichier spcifique .......................................... 16 Comment utiliser les bonnes pratiques pour structurer vos Bundles.................................................... 18 Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle........................... 23 Comment surcharger n'importe quelle partie d'un bundle .................................................................. 26 Comment supprimer le AcmeDemoBundle........................................................................................ 29 Comment exposer une configuration smantique pour un Bundle ...................................................... 32 Comment utiliser Varnish pour acclrer mon site Web ..................................................................... 41 Comment Matriser et Crer de nouveaux Environnements ................................................................ 43 Comment surcharger la structure de rpertoires par dfaut de Symfony .............................................. 48 Comment configurer les paramtres externes dans le conteneur de services......................................... 51 Comment stocker les sessions dans la base de donnes grce PdoSessionStorage .............................. 54 Comment utiliser le routeur Apache .................................................................................................. 57 Comment crer une commande pour la Console................................................................................ 60 Comment utiliser la Console ............................................................................................................. 63 Comment gnrer des URLs et envoyer des emails depuis la Console.................................................. 65 Comment activer les logs dans la commande console ......................................................................... 67 Comment personnaliser les pages d'erreur ......................................................................................... 71 Comment dfinir des contrleurs en tant que Services........................................................................ 73 Comment optimiser votre environnement pour le debuggage ............................................................. 75 Dployer une application Symfony2 .................................................................................................. 77 Comment grer les uploads de fichier avec Doctrine........................................................................... 81 Comment utiliser les extensions Doctrine: Timestampable, Sluggable, Translatable, etc...................... 89 Comment enregistrer des listeners ( couteurs en franais) et des souscripteurs d'vnement........... 90 Comment utiliser la couche DBAL de Doctrine .................................................................................. 92 Comment gnrer des Entits partir d'une base de donnes existante ............................................... 94 Comment travailler avec plusieurs gestionnaires d'entits et connexions ............................................. 98 Comment dfinir des fonctions DQL personnalises ........................................................................ 101 Comment dfinir des Relations avec des Classes Abstraites et des Interfaces ..................................... 102 Comment implmenter un simple formulaire de cration de compte................................................. 105 Comment envoyer un Email ............................................................................................................ 111 Comment utiliser Gmail pour envoyer des Emails ............................................................................ 113 Comment travailler avec les Emails pendant le Dveloppement ........................................................ 114 Comment utiliser le Spool d'Email.............................................................................................. 116 PDF brought to you by generated on June 26, 2014 Contents at a Glance | iii Comment mettre en place des filtres avant et aprs un processus donn............................................ 118 Comment tendre une Classe sans utiliser l'Hritage ........................................................................ 123 Comment personnaliser le Comportement d'une Mthode sans utiliser l'Hritage ............................. 126 Comment personnaliser le rendu de formulaire................................................................................ 128 Comment utiliser les Convertisseurs de Donnes ............................................................................. 140 Comment modifier dynamiquement les formulaires en utilisant les vnements ................................ 146 Comment imbriquer une Collection de Formulaires......................................................................... 157 Comment Crer un Type de Champ de Formulaire Personnalis ...................................................... 169 Comment crer une extension de type de formulaire ........................................................................ 174 Comment rduire la duplication de code avec "inherit_data" ............................................................ 180 Comment tester unitairement vos formulaires.................................................................................. 183 Comment configurer des donnes vierges pour une classe de Formulaire .......................................... 188 Comment utiliser la fonction submit() pour grer les soumissions de formulaires.............................. 190 Comment utiliser l'Option de Champ de Formulaire Virtual .......................................................... 193 Comment utiliser Monolog pour crire des Logs .............................................................................. 196 Comment configurer Monolog pour envoyer les erreurs par Email.................................................... 200 Comment loguer des messages dans diffrents fichiers ..................................................................... 202 Comment crer un Collecteur de Donnes personnalis ................................................................... 204 Comment dclarer un nouveau Format de Requte et un Type Mime ............................................... 207 Comment forcer les routes toujours utiliser HTTPS ou HTTP........................................................ 209 Comment autoriser un caractre / dans un paramtre de route .................................................... 211 Comment configurer une redirection vers une autre route sans contrleur personnalis .................... 212 Comment utiliser des mthodes HTTP autres que GET et POST dans les routes ............................... 213 Comment utiliser des paramtres du conteneur de services dans vos routes ...................................... 215 Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit)......................... 217 Comment ajouter la fonctionnalit de login Se souvenir de moi ................................................... 227 Comment implmenter votre propre Voteur pour ajouter des adresses IP sur une liste noire.............. 230 Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) ............... 233 Comment utiliser les concepts d'ACL avancs.................................................................................. 237 Comment forcer HTTPS ou HTTP pour des URLs Diffrentes ......................................................... 241 Comment personnaliser votre formulaire de login............................................................................ 242 Comment scuriser n'importe quel service ou mthode de votre application ..................................... 246 Comment crer un Fournisseur d'Utilisateur personnalis ................................................................ 250 Comment crer un Fournisseur d'Authentification Personnalis ....................................................... 255 Comment changer le comportement par dfaut du chemin cible....................................................... 264 Comment utiliser le Serializer .......................................................................................................... 266 Comment crer un listener ( couteur en franais) d'vnement............................................... 268 Comment travailler avec les champs d'applications ( scopes en anglais) ........................................ 271 Comment travailler avec les Passes de Compilation dans les Bundles ................................................ 275 Exemple de Session Proxy ............................................................................................................... 276 Faire que la Locale soit "persistente" durant la session de l'utilisateur................................................ 278 Configurer le Dossier o les Fichiers pour les Sessions sont Enregistrs............................................. 280 Combler une application legacy avec les sessions de Symfony........................................................... 282 En quoi Symfony2 diffre de Symfony1............................................................................................ 284 Comment injecter des variables dans tous les modles (i.e. Variables Globales)................................. 290 Comment utiliser et enregistrer des chemins Twig namespacs......................................................... 292 Comment utiliser PHP plutt que Twig dans les templates ............................................................... 294 iv | Contents at a Glance Contents at a Glance | 4 Comment crire une Extension Twig personnalise.......................................................................... 299 Comment rendre un template sans passer par un contrleur............................................................. 302 Comment simuler une authentification HTTP dans un Test Fonctionnel .......................................... 304 Comment simuler une authentification avec un token dans un test fonctionnel................................. 305 Comment tester les interactions de multiples clients......................................................................... 307 Comment utiliser le Profiler dans un test fonctionnel ....................................................................... 309 Comment tester du code interagissant avec une base de donnes...................................................... 311 Comment tester les dpts Doctrine ................................................................................................ 314 Comment personnaliser le processus de bootstrap avant les tests...................................................... 316 Comment crer une Contrainte de Validation Personnalise............................................................. 318 Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 ................................. 322 Comment crer et stocker un projet Symfony2 dans git .................................................................... 326 Comment crer et stocker un projet Symfony2 dans Subversion ....................................................... 330 PDF brought to you by generated on June 26, 2014 Contents at a Glance | v Listing 1-1 Chapter 1 Comment utiliser Assetic pour grer vos ressources Assetic associe deux concepts majeurs : les ressources et les filtres. Les ressources sont des fichiers comme les feuilles de style, les JavaScript et les images. Les filtres peuvent tre appliqus ces fichiers avant qu'ils ne soient servis au navigateur. Cela permet de grer sparment les fichiers ressources qui sont stocks par l'application des fichiers qui sont rellement prsents l'utilisateur. Sans Assetic, vous servez directement les fichiers qui sont stocks dans votre application : 1 <script src="{{ asset('js/script.js') }}" type="text/javascript" /> Mais avec Assetic, vous pouvez manipuler ces ressources de la manire dont vous le dsirez (ou les charger de n'importe o) avant de les servir. Cela signifie que vous pouvez : Minifier et combiner toutes vos CSS ou vos fichiers JavaScript Excuter tous (ou juste une partie) vos fichiers CSS ou JS en passant par des compilateurs comme LESS, SASS ou CoffeeScript. Optimiser vos images Ressources Utiliser Assetic plutt que servir les fichiers directement offre de nombreux avantages. Les fichiers n'ont pas besoin d'tre stocks l o il seront servis, et peuvent provenir de plusieurs sources, notamment d'un bundle. Vous pouvez utiliser Assetic pour vos fichiers CSS ou Javascript Le principe est identique entre les deux l'exception d'une syntaxe qui diffre lgrement. Inclure des fichiers Javascript Pour inclure des fichiers Javascript, utilisez le tag javascript dans n'importe quel template. On va en gnral s'en servir dans le block javascripts, si vous utilisez les noms de block par dfaut de la Distribution Standard de Symfony : PDF brought to you by generated on June 26, 2014 Chapter 1: Comment utiliser Assetic pour grer vos ressources | 6 Listing 1-2 Listing 1-3 Listing 1-4 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} Vous pouvez aussi inclure vos feuilles de style: voir Inclure des fichiers CSS. Dans cet exemple, tous les fichiers du dossier Resources/public/js/ du bundle AcmeFooBundle vont tre chargs et servis depuis un autre endroit. Le tag rellement affich pourrait ressembler : 1 <script src="/app_dev.php/js/abcd123.js"></script> C'est un point cl: Une fois que vous avez laiss Assetic grer vos ressources, les fichiers sont servis depuis un autre endroit. Ceci pourra provoquer des problmes pour les fichiers CSS contenant des chemins relatifs pour leurs images. Voir Corriger les chemins CSS avec le filtre cssrewrite. Inclure des fichiers CSS Pour vos feuilles de styles CSS, vous pouvez utiliser la mme mthodologie mais avec le tag stylesheets. Si vous utilisez les block par dfaut de la Distribution Standard, ce tag prendra place dans un block stylesheets : 1 2 3 {% stylesheets 'bundles/acme_foo/css/*' filter='cssrewrite' %} <link rel="stylesheet" href="{{ asset_url }}" /> {% endstylesheets %} Mais comme Assetic modifie les chemins de vos ressources, les images de fond (ou autres) qui utilisent des chemins relatifs se retrouveront casss, sauf si vous utilisez le filtre cssrewrite. Remarquez que dans le premier exemple qui inclut les fichiers javascripts, vous faites rfrence aux fichiers avec un chemin comme suit : @AcmeFooBundle/Resources/public/file.js, mais dans celui-ci, vous faites rfrence aux fichiers CSS avec leur vrai chemin public : bundles/acme_foo/ css. Vous pouvez utiliser l'un ou l'autre. Sachez juste qu'il existe un problme connu qui peut faire chouer le filtre cssrewrite avec la syntaxe @AcmeFooBundle. Corriger les chemins CSS avec le filtre cssrewrite Vu que Assetic gnre de nouvelles URLs pour vos ressource, tous les chemins relatifs dans vos fichiers CSS vont tre casss. Pour corriger a, assurez-vous d'utiliser le filtre cssrewrite avec votre tag stylesheets. Il va parser votre CSS et corriger les chemins pour prendre en compte le nouvel emplacement. Vous pouvez voir un exemple dans la section prcdente : Quand vous utilisez le filtre cssrewrite, ne faites pas appel vos CSS avec la syntaxe @AcmeFooBundle. Pour plus de dtails, voir la note dans la section du dessus. PDF brought to you by generated on June 26, 2014 Chapter 1: Comment utiliser Assetic pour grer vos ressources | 7 Listing 1-5 Listing 1-6 Combiner des ressources Vous pouvez aussi combiner plusieurs fichiers en un seul. Cela aide rduire le nombre de requtes HTTP, ce qui est trs important pour les performances. Cela vous permet aussi de maintenir les fichiers plus facilement en les dcoupants en petites parties plus faciles grer. Cela peut tre un plus pour la rusabilit de votre projet puisque vous pouvez facilement sparer les fichiers spcifiques au projet des fichiers qui peuvent tre rutiliss dans d'autres applications, mais toujours les servir comme un fichier unique : 1 2 3 4 5 6 {% javascripts '@AcmeFooBundle/Resources/public/js/*' '@AcmeBarBundle/Resources/public/js/form.js' '@AcmeBarBundle/Resources/public/js/calendar.js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} En environnement de dev, chaque fichier est toujours servi individuellement pour que vous puissiez dbugguer plus facilement. Cependant, en environnement de prod (ou plus prcisment, quand l'option debug est false), ils seront affichs dans une unique balise script qui contiendra le contenu de tous vos fichiers JavaScript. Si vous dcouvrez Assetic et essayez d'utiliser votre application en environnement de prod (en utilisant le contrleur app.php), vous verrez probablement que vos CSS et JS plantent. Pas de panique ! C'est fait exprs. Pour plus de dtails sur l'utilisation d'Assetic en environnement de prod, lisez Exporter les ressources. Et combiner les fichiers ne s'applique pas uniquement vos fichiers. Vous pouvez aussi utiliser Assetic pour combiner les ressources tierces, comme jQuery, vos fichiers dans un fichier unique : 1 2 3 4 5 {% javascripts '@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js' '@AcmeFooBundle/Resources/public/js/*' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} Filtres Une fois qu'elles sont gres par Assetic, vous pouvez appliquer des filtres vos ressources avant qu'elles ne soient servies. Cela inclut les filtres qui compressent vos ressources pour rduire la taille des fichiers (pour de meilleures performances). D'autres filtres peuvent compiler des fichiers CoffeeScript en JavaScript ou convertir vos fichiers SASS en CSS. En fait, Assetic possde une longue liste de filtres disponibles. Plusieurs de ces filtres ne font pas le travail directement, mais utilisent des bibliothques tierces pour faire le gros du travail. Cela signifie que vous devrez souvent installer une bibliothque tierce pour utiliser un filtre. Le grand avantage d'utiliser Assetic pour faire appel ces bibliothques (plutt que de les utiliser directement) est qu'au lieu de les excuter la main aprs avoir modifi les fichiers, Assetic prendra tout en charge pour vous, et supprimera dfinitivement cette tape du processus de dveloppement et de dploiement. Pour utiliser un filtre, vous aurez d'abord besoin de le spcifier dans la configuration d'Assetic. Ajouter un filtre dans la configuration ne signifie pas qu'il est utilis, mais juste qu'il est prt l'tre (vous allez l'utiliser ci-dessous). Par exemple, pour utiliser le JavaScript YUI Compressor, la configuration suivante doit tre ajoute : PDF brought to you by generated on June 26, 2014 Chapter 1: Comment utiliser Assetic pour grer vos ressources | 8 Listing 1-7 Listing 1-8 Listing 1-9 Listing 1-10 1 2 3 4 5 # app/config/config.yml assetic: filters: yui_js: jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" Maintenant, pour vraiment utiliser le filtre sur un groupe de fichiers JavaScript, ajoutez ce code dans votre template : 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} Vous pouvez trouver un guide plus dtaill sur la configuration et l'utilisation des filtres Assetic ainsi que des informations sur le mode debug d'Assetic en lisant Comment minifier les JavaScripts et les feuilles de style avec YUI Compressor. Contrler l'URL utilise Si vous le souhaitez, vous pouvez contrler les URLs gnres par Assetic. Cela se fait dans le template, et le chemin est relatif par rapport la racine publique : 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} Symfony contient galement une mthode pour le cache busting (technique empchant la mise en cache), o l'URL gnre par Assetic contient un paramtre qui peut tre incrment, via la configuration, chaque dploiement. Pour plus d'informations, lisez l'option de configuration assets_version. Exporter les ressources En environnement de dev, Assetic gnre des chemins vers des fichiers CSS et JavaScript qui n'existent pas physiquement sur votre ordinateur. Mais ils sont nanmoins affichs car un contrleur interne de Symfony ouvre les fichiers et sert leur contenu (aprs avoir excut tous les filtres). Cette manire dynamique de servir des ressources traites est gniale car cela signifie que vous pouvez immdiatement voir les modifications que vous apportez vos fichiers. Mais l'inconvnient est que cela peut parfois tre un peu lent. Si vous utilisez beaucoup de filtres, cela peut tre carrment frustrant. Heureusement, Assetic fournit un moyen pour exporter vos ressources vers des fichiers rels au lieu de les gnrer dynamiquement. Exporter les ressources en environnement de prod En environnement de prod, vos fichiers JS et CSS sont reprsents chacun par une balise unique. En d'autres termes, plutt que de voir chacun des fichiers JavaScript que vous incluez dans votre code source, vous verrez quelque chose comme ceci : PDF brought to you by generated on June 26, 2014 Chapter 1: Comment utiliser Assetic pour grer vos ressources | 9 Listing 1-11 Listing 1-12 Listing 1-13 Listing 1-14 Listing 1-15 1 <script src="/app_dev.php/js/abcd123.js"></script> De plus, ce fichier n'existe pas vraiment et n'est pas non plus affich dynamiquement par Symfony (car les ressources sont en environnement de dev). C'est fait exprs : laisser Symfony gnrer ces fichiers dynamiquement en production serait tout simplement trop lent. Au lieu de cela, chaque fois que vous excutez votre application dans l'environnement de prod (et par consquent, chaque fois que vous dployez), vous devriez excuter la commande suivante : 1 $ php app/console assetic:dump --env=prod --no-debug Cela gnrera et crira physiquement chaque fichier dont vous avez besoin (ex /js/abcd123.js). Si vous mettez jour vos ressources, vous aurez besoin de relancer cette commande pour regnrer vos fichiers. Exporter les ressources en environnement de dev Par dfaut, chaque chemin de ressource gnr en environnement de dev est pris en charge dynamiquement par Symfony. Cela n'a pas d'inconvnient (vous pouvez voir vos changements immdiatement), sauf que les ressources peuvent tre lentes charger. Si vous trouvez que vos ressources sont chargs trop lentement, suivez ce guide. Premirement, dites Symfony de ne plus essayer de traiter ces fichiers dynamiquement. Apportez les modifications suivantes dans le fichier config_dev.yml : 1 2 3 # app/config/config_dev.yml assetic: use_controller: false Ensuite, puisque Symfony ne gnre plus ces fichiers pour vous, vous aurez besoin de les exporter manuellement. Pour ce faire, lancez la commande suivante : 1 $ php app/console assetic:dump Elle crit physiquement tous les fichiers de ressource dont vous avez besoin pour l'environnement de dev. Le gros inconvnient est que vous devrez faire cela chaque fois que vous modifiez une ressource. Heureusement, en passant l'option --watch, la commande regnrera automatiquement les ressources modifies : 1 $ php app/console assetic:dump --watch Lancer cette commande en environnement de dev peut gnrer un florilge de fichiers. Pour conserver votre projet bien organis, il peut tre intressant de mettre les fichiers gnrs dans un rpertoire spar (ex /js/compiled) : 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} PDF brought to you by generated on June 26, 2014 Chapter 1: Comment utiliser Assetic pour grer vos ressources | 10 Listing 2-1 Chapter 2 Comment minifier les JavaScripts et les feuilles de style avec YUI Compressor Yahoo! fournit un excellent utilitaire pour minifier les JavaScripts et les feuilles de style pour qu'elles soient plus rapides charger, YUI Compressor 1 . Grce Assetic, vous pourrez tirer profit de cet outil trs facilement. Tlchargez le JAR YUI Compressor YUI Compressor est crit en Java est distribu sous forme de JAR. Tlchargez le JAR 2 sur le site de Yahoo! et enregistrez le sous app/Resources/java/yuicompressor.jar. Configurez les filtres YUI Maintenant vous devez configurer les deux filtres Assetic dans votre application, l'un pour minifier les JavaScripts avec YUI Compressor et l'autre pour minifier les feuilles de style : 1 2 3 4 5 6 7 8 # app/config/config.yml assetic: # java: "/usr/bin/java" filters: yui_css: jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" yui_js: jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" 1. http://developer.yahoo.com/yui/compressor/ 2. http://yuilibrary.com/projects/yuicompressor/ PDF brought to you by generated on June 26, 2014 Chapter 2: Comment minifier les JavaScripts et les feuilles de style avec YUI Compressor | 11 Listing 2-2 Listing 2-3 Listing 2-4 Les utilisateurs de Windows ne doivent pas oublier de mettre jour l'emplacement de Java. Dans Windows8 x64 bit, il s'agit de C:\Program Files (x86)\Java\jre6\bin\java.exe par dfaut Vous avez maintenant accs aux deux nouveaux filtres Assetic dans votre application : yui_css et yui_js. Ils utiliseront YUI Compressor pour minifier respectivement les feuilles de style et les JavaScripts. Minifiez vos Ressources Maintenant YUI Compressor est configur, mais rien ne se passera tant que vous n'appliquez pas ces filtres une ressource (asset). Puisque vos ressources font partie de la couche Vue, ce travail doit tre fait dans vos templates : 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} L'exemple ci-dessus part du principe que vous avez un bundle appel AcmeFooBundle et que vos fichiers JavaScript se trouvent dans le rpertoire Resources/public/js dans votre bundle. Ce n'est, en fait, pas trs important car vous pouvez inclure vos fichiers JavaScript o vous le voulez. En rajoutant le filtre yui_js la ressource ci-dessus, vous devriez voir que les JavaScripts minifis sont chargs beaucoup plus rapidement. Le mme procd peut tre utilis pour minifier vos feuilles de style. 1 2 3 {% stylesheets '@AcmeFooBundle/Resources/public/css/*' filter='yui_css' %} <link rel="stylesheet" type="text/css" media="screen" href="{{ asset_url }}" /> {% endstylesheets %} Dsactiver la minification en Mode Debug Les JavaScripts et feuilles de styles minifis sont trs difficiles lire; et encore moins dbugguer. Pour palier cela, Assetic vous permet de dsactiver un filtre lorsque votre application est en mode debug. Vous pouvez faire cela en prfixant le nom du filtre dans votre template par un point d'interrogation : ?. Cela indique Assetic de n'appliquer les filtres que si le mode debug n'est pas actif. 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='?yui_js' %} <script src="{{ asset_url }}"></script> {% endjavascripts %} Plutt que d'ajouter le filtre vos balises assets, vous pouvez aussi l'activer de faon globale en ajoutant l'attribut apply-to la configuration du filtre, par exemple apply_to: "\.js$" pour le filtre yui_js. Pour que le filtre ne s'applique qu'en production, ajoutez le au fichier config_prod au lieu du fichier de configuration commun. Pour plus de dtails sur comment appliquer des filtres en fonction des extensions de fichiers, lisez Filtrer en se basant sur les extensions. PDF brought to you by generated on June 26, 2014 Chapter 2: Comment minifier les JavaScripts et les feuilles de style avec YUI Compressor | 12 Listing 3-1 Listing 3-2 Chapter 3 Comment utiliser Assetic et les fonctions Twig pour optimiser les images Parmi ses nombreux filtres, Assetic possde quatre filtres qui peuvent tre utiliss pour optimiser les images la vole. Cela vous permet de tirer profit de tailles de fichiers rduites sans utiliser d'diteur d'image pour rduire chaque image. Les rsultats sont mis en cache et peuvent tre rutiliss en production pour qu'il n'y ait pas d'impact sur les performances pour vos utilisateurs finaux. Utiliser Jpegoptim Jpegoptim 1 est un utilitaire pour optimiser les fichiers JPEG. Pour l'utiliser avec Assetic, ajoutez le bout de code suivant votre configuration Assetic : 1 2 3 4 5 # app/config/config.yml assetic: filters: jpegoptim: bin: path/to/jpegoptim Notez que pour utiliser jpegoptim, il faut qu'il soit dj install sur votre systme. L'option bin pointe vers le fichier binaire compil. Il peut maintenant tre utilis dans un template : 1 2 3 4 {% image '@AcmeFooBundle/Resources/public/images/example.jpg' filter='jpegoptim' output='/images/example.jpg' %} <img src="{{ asset_url }}" alt="Example"/> {% endimage %} 1. http://www.kokkonen.net/tjko/projects.html PDF brought to you by generated on June 26, 2014 Chapter 3: Comment utiliser Assetic et les fonctions Twig pour optimiser les images | 13 Listing 3-3 Listing 3-4 Listing 3-5 Listing 3-6 Listing 3-7 Supprimer toutes les donnes EXIF Par dfaut, appliquer ce filtre ne supprime que certaines meta-informations du fichier. Les donnes EXIF et les commentaires ne sont pas supprims, mais vous pouvez les supprimer en utilisant l'option strip_all : 1 2 3 4 5 6 # app/config/config.yml assetic: filters: jpegoptim: bin: path/to/jpegoptim strip_all: true Rduire la qualit maximum Le niveau de qualit du JPEGn'est pas modifi par dfaut. Vous pouvez rduire un peu la taille des images en dfinissant un niveau de qualit maximum plus bas que le niveau actuel. Cela se fera videmment au dtriment de la qualit de l'image : 1 2 3 4 5 6 # app/config/config.yml assetic: filters: jpegoptim: bin: path/to/jpegoptim max: 70 Fonctions Twig : syntaxe courte Si vous utilisez Twig, il est possible de faire tout ceci avec une syntaxe raccourcie en activant et en utilisant une fonction spciale Twig. Commencez par ajouter la configuration suivante : 1 2 3 4 5 6 7 8 # app/config/config.yml assetic: filters: jpegoptim: bin: path/to/jpegoptim twig: functions: jpegoptim: ~ Le template Twig peut maintenant tre modifi comme suit : 1 <img src="{{ jpegoptim('@AcmeFooBundle/Resources/public/images/example.jpg') }}" alt="Example"/> Vous pouvez spcifier le rpertoire cible dans la configuration de la manire suivante : 1 2 3 4 5 6 # app/config/config.yml assetic: filters: jpegoptim: bin: path/to/jpegoptim twig: PDF brought to you by generated on June 26, 2014 Chapter 3: Comment utiliser Assetic et les fonctions Twig pour optimiser les images | 14 7 8 functions: jpegoptim: { output: images/*.jpg } PDF brought to you by generated on June 26, 2014 Chapter 3: Comment utiliser Assetic et les fonctions Twig pour optimiser les images | 15 Listing 4-1 Listing 4-2 Chapter 4 Comment appliquer un filtre Assetic une extension de fichier spcifique Les filtres Assetic peuvent tre appliqus des fichiers individuels, des groupes de fichiers ou mme, comme vous allez le voir ici, des fichiers qui ont une extension spcifique. Pour vous montrer comment grer chaque cas, supposons que vous ayez le filtre Assetic CoffeeScript qui compile les fichiers CoffeeScript en JavaScript. La configuration principale contient juste les chemins vers coffee et node. Leurs valeurs par dfaut respectives sont /usr/bin/coffee et /usr/bin/node: 1 2 3 4 5 6 # app/config/config.yml assetic: filters: coffee: bin: /usr/bin/coffee node: /usr/bin/node Filtrer un fichier unique Vous pouvez maintenant compiler un fichier unique CoffeeScript en JavaScript depuis vos templates : 1 2 3 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' filter='coffee' %} <script src="{{ asset_url }}" type="text/javascript"></script> {% endjavascripts %} C'est tout ce dont vous avez besoin pour compiler ce fichier CoffeeScript et le servir comme JavaScript compil. PDF brought to you by generated on June 26, 2014 Chapter 4: Comment appliquer un filtre Assetic une extension de fichier spcifique | 16 Listing 4-3 Listing 4-4 Listing 4-5 Filtrer des fichiers multiples Vous pouvez aussi combiner plusieurs fichiers CoffeeScript en un unique fichier en sortie : 1 2 3 4 5 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' '@AcmeFooBundle/Resources/public/js/another.coffee' filter='coffee' %} <script src="{{ asset_url }}" type="text/javascript"></script> {% endjavascripts %} Les deux fichiers seront maintenant servis comme un unique fichier compil en JavaScript. Filtrer en se basant sur les extensions Un des plus grands avantages d'Assetic est de pouvoir rduire le nombre de ressources pour rduire le nombre de requtes HTTP. Dans le but d'en tirer le plus grand avantage possible, il pourrait tre intressant de combiner tous vos fichiers CoffeeScript et JavaScript ensembles puisqu'ils seront finalement dlivrs comme JavaScript. Malheureusement, se contenter d'ajouter les fichiers JavaScript aux fichiers combiner ne fonctionnera pas car le JavaScript ne passera pas la compilation CoffeeScript. Ce problme peut tre vit en ajoutant l'option apply_to la configuration, ce qui vous permettra de spcifier qu'un filtre devra toujours tre appliqu une extension de fichier particulire. Dans ce cas, vous pouvez spcifier que le filtre Coffee s'applique tous les fichiers .coffee : # app/config/config.yml assetic: filters: coffee: bin: /usr/bin/coffee node: /usr/bin/node apply_to: "\.coffee$" Avec cela, vous n'avez plus besoin de spcifier le filtre coffee dans le template. Vous pouvez aussi lister les fichiers JavaScript classique, chacun d'eux sera combin et dlivr en un unique fichier JavaScript (seuls les fichiers .coffee passeront travers le filtre CoffeeScript) : 1 2 3 4 5 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' '@AcmeFooBundle/Resources/public/js/another.coffee' '@AcmeFooBundle/Resources/public/js/regular.js' %} <script src="{{ asset_url }}" type="text/javascript"></script> {% endjavascripts %} PDF brought to you by generated on June 26, 2014 Chapter 4: Comment appliquer un filtre Assetic une extension de fichier spcifique | 17 Chapter 5 Comment utiliser les bonnes pratiques pour structurer vos Bundles Un bundle est un rpertoire qui a une structure bien dfinie et qui peut hberger peu prs tout : des classes aux contrleurs en passant par les ressources web. Mme si les bundles sont trs flexibles, vous devriez suivre quelques unes des bonnes pratiques si vous voulez les distribuer. Nom du Bundle Un bundle est aussi un espace de noms PHP. Ce dernier doit suivre les standards 1 d'introprabilit technique pour les espaces de noms PHP 5.3 et les noms de classes : il commence par un segment vendor , suivi par zro ou plusieurs segments catgories, et il se termine par le nom raccourci de l'espace de noms, qui doit finir par un suffixe Bundle. Un espace de noms devient un bundle aussitt que vous lui ajoutez une classe bundle. Le nom de la classe bundle doit suivre ces rgles simples : Utiliser uniquement des caractres alphanumriques et des tirets bas ( underscore en anglais) ; Utiliser un nom en notation dite CamelCase ; Utiliser un nom court et descriptif (pas plus de 2 mots) ; Prfixer le nom avec la concatnation du vendor (et optionnellement l'espace de noms de la catgorie) ; Suffixer le nom avec Bundle. Vous trouverez ci-dessous des espaces de noms de bundle et des noms de classes valides : Espace de noms Nom de la Classe Bundle Acme\Bundle\BlogBundle AcmeBlogBundle Acme\Bundle\Social\BlogBundle AcmeSocialBlogBundle 1. http://symfony.com/PSR0 PDF brought to you by generated on June 26, 2014 Chapter 5: Comment utiliser les bonnes pratiques pour structurer vos Bundles | 18 Listing 5-1 Espace de noms Nom de la Classe Bundle Acme\BlogBundle AcmeBlogBundle Par convention, la mthode getName() de la classe bundle devrait retourner le nom de la classe. Si vous partagez publiquement votre bundle, vous devez utiliser le nom de la classe bundle comme nom de dpt (AcmeBlogBundle et non pas BlogBundle par exemple). Les Bundles du coeur de Symfony2 ne prfixent pas la classe Bundle avec Symfony et ajoutent toujours un sous-espace de noms Bundle ; par exemple : FrameworkBundle 2 . Chaque bundle possde un alias, qui est la version raccourcie en miniscules du nom du bundle en utilisant des tirets bas (acme_hello pour AcmeHelloBundle, ou acme_social_blog pour Acme\Social\BlogBundle par exemple). Cet alias est utilis pour renforcer l'unicit l'intrieur d'un bundle (voir ci-dessous pour des exemples d'utilisation). Structure de Rpertoires La structure basique du rpertoire d'un bundle HelloBundle doit tre comme suit : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 XXX/... HelloBundle/ HelloBundle.php Controller/ Resources/ meta/ LICENSE config/ doc/ index.rst translations/ views/ public/ Tests/ Le(s) rpertoire(s) XXX reflte(nt) la structure de l'espace de noms du bundle. Les fichiers suivants sont obligatoires : HelloBundle.php ; Resources/meta/LICENSE: La licence complte pour le code ; Resources/doc/index.rst: Le fichier racine pour la documentation du bundle. Ces conventions assurent que les outils automatiss puissent compter sur cette structure par dfaut pour travailler. 2. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/FrameworkBundle.html PDF brought to you by generated on June 26, 2014 Chapter 5: Comment utiliser les bonnes pratiques pour structurer vos Bundles | 19 La profondeur des sous-rpertoires devrait tre rduite au minimum pour les classes et fichiers les plus utiliss (2 niveaux au maximum). Plus de niveaux peuvent tre dfinis pour les fichiers non-stratgiques et moins utiliss. Le rpertoire du bundle est en lecture seule. Si vous avez besoin d'crire des fichiers temporaires, stockez- les dans le dossier cache/ ou log/ de l'application hbergeant votre bundle. Des outils peuvent gnrer des fichiers dans la structure du rpertoire du bundle, mais uniquement si les fichiers gnrs vont faire partie du dpt. Les classes et fichiers suivants ont des emplacements spcifiques : Type Rpertoire Commandes Command/ Contrleurs Controller/ Extensions du Conteneur de Services DependencyInjection/ Listeners d'vnements EventListener/ Configuration Resources/config/ Ressources Web Resources/public/ Fichiers de traduction Resources/translations/ Templates Resources/views/ Tests Unitaires et Fonctionnels Tests/ Classes La structure du rpertoire du bundle est utilise en tant que hirarchie d'espace de noms. Par exemple, un contrleur HelloController est stock dans Bundle/HelloBundle/Controller/HelloController.php et le nom complet qualifi de la classe est Bundle\HelloBundle\Controller\HelloController. Tous les fichiers et classes doivent suivre les standards de codage de Symfony2 ( coding standards en anglais). Certaines classes devraient tre vues comme des faades et donc tre aussi courtes que possible, comme les Commands , Helpers , Listeners et Controllers . Les classes se connectant au dispatcher ( rpartiteur en franais) d'vnements devraient tre suffixes avec Listener. Les classes d'exceptions devraient tre stockes dans un sous-espace de noms Exception. Vendors Un bundle ne doit pas embarquer de bibliothques PHP tierces. Il devrait compter sur le chargement automatique ( autoloading en anglais) standard de Symfony2 la place. Un bundle ne devrait pas embarquer de bibliothques tierces crites en JavaScript, CSS, ou quelconque autre langage. PDF brought to you by generated on June 26, 2014 Chapter 5: Comment utiliser les bonnes pratiques pour structurer vos Bundles | 20 Tests Un bundle devrait venir avec un ensemble de tests crits avec PHPUnit et stocks dans le rpertoire Tests/. Les tests devraient suivre les principes suivants : La suite de tests doit tre excutable avec une simple commande phpunit lance depuis une application ; Les tests fonctionnels devraient tre utiliss uniquement pour tester la sortie de la rponse et quelques informations de profilage si vous en avez ; Les tests devraient couvrir au moins 95% de tout votre code. Une suite de test ne doit pas contenir de script AllTests.php, mais doit reposer sur l'existence d'un fichier phpunit.xml.dist. Documentation Toutes les classes et fonctions doivent contenir une PHPDoc complte. Une documentation complte devrait aussi tre fournie dans le format reStructuredText, dans le rpertoire Resources/doc/ ; le fichier Resources/doc/index.rst est l'unique fichier obligatoire et doit tre le point d'entre de la documentation. Contrleurs En tant que bonne pratique, les contrleurs dans un bundle prvu pour tre distribu d'autres ne doivent pas tendre la classe de base Controller 3 . Ils peuvent implmenter ContainerAwareInterface 4 ou tendre ContainerAware 5 la place. Si vous jetez un oeil aux mthodes de la classe Controller 6 , vous verrez qu'elles ne sont que des raccourcis pratiques pour faciliter la courbe d'apprentissage. Routage Si le bundle fournit des routes, elles doivent tre prfixes avec l'alias du bundle. Par exemple, pour un AcmeBlogBundle , toutes les routes doivent tre prfixes avec acme_blog_. Templates Si un bundle fournit des templates, ils doivent utiliser Twig. Un bundle ne doit pas fournir de layout principal, except s'il fournit une application entirement fonctionnelle. 3. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Controller/Controller.html 4. http://api.symfony.com/2.3/Symfony/Component/DependencyInjection/ContainerAwareInterface.html 5. http://api.symfony.com/2.3/Symfony/Component/DependencyInjection/ContainerAware.html 6. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Controller/Controller.html PDF brought to you by generated on June 26, 2014 Chapter 5: Comment utiliser les bonnes pratiques pour structurer vos Bundles | 21 Listing 5-2 Listing 5-3 Fichiers de Traduction Si un bundle fournit des traductions de messages, ces dernires doivent tre dfinies au format XLIFF ; le domaine devrait tre nomm aprs le nom du bundle (bundle.hello). Un bundle ne doit pas craser les messages existants venant d'un autre bundle. Configuration Pour fournir plus de flexibilit, un bundle peut procurer des paramtres configurables en utilisant les mcanismes intgrs de Symfony2. Pour des paramtres de configuration simples, comptez sur les entres par dfaut de parameters de la configuration de Symfony2. Les paramtres Symfony2 sont de simples paires cl/valeur ; une valeur tant n'importe quelle valeur PHP valide. Chaque nom de paramtre devrait commencer avec l'alias du bundle, bien que ceci ne soit qu'une suggestion de bonne pratique. Le reste du nom du paramtre va utiliser un point (.) pour sparer les diffrentes parties (par exemple : acme_hello.email.from). L'utilisateur final peut fournir des valeurs dans diffrents types de fichier de configuration : 1 2 3 # app/config/config.yml parameters: acme_hello.email.from: [email protected] Rcuprez les paramtres de configuration dans votre code depuis le conteneur: 1 $container->getParameter('acme_hello.email.from'); Mme si ce mcanisme est assez simple, vous tes grandement encourag utiliser la configuration smantique dcrite dans le cookbook. Si vous dfinissez des services, ils devraient aussi tre prfixs avec l'alias du bundle. En savoir plus grce au Cookbook Comment exposer une configuration smantique pour un Bundle PDF brought to you by generated on June 26, 2014 Chapter 5: Comment utiliser les bonnes pratiques pour structurer vos Bundles | 22 Listing 6-1 Chapter 6 Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle Lorsque vous travaillerez avec des bundles tiers, vous allez probablement rencontrer une situation o vous voudrez surcharger un fichier de ce bundle tiers en le remplacant par un fichier de l'un de vos propres bundles. Symfony vous fournit une manire trs pratique de surcharger des fichiers comme des contrleurs, des templates et d'autres fichiers prsents dans le dossier Resources/ d'un bundle. Par exemple, supposons que vous installiez le FOSUserBundle 1 , mais que vous souhaitez surcharger son template de base layout.html.twig, ainsi que l'un de ses contrleurs. Supposons aussi que vous ayez votre propre AcmeUserBundle o vous voulez avoir les fichiers de substitution. Commencez par dclarer le FOSUserBundle comme parent de votre bundle: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/UserBundle/AcmeUserBundle.php namespace Acme\UserBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class AcmeUserBundle extends Bundle { public function getParent() { return 'FOSUserBundle'; } } En effectuant ce simple changement, vous pouvez dsormais surcharger plusieurs parties du FOSUserBundle en crant simplement un fichier ayant le mme nom. Malgr le nom de la mthode, il n'y a pas de relation parent/enfant entre les bundles. Il s'agit juste d'une manire d'tendre et de surcharger un bundle existant. 1. https://github.com/friendsofsymfony/fosuserbundle PDF brought to you by generated on June 26, 2014 Chapter 6: Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle | 23 Listing 6-2 Surcharger des contrleurs Supposons que vous vouliez ajouter de la fonctionnalit registerAction du RegistrationController rsidant dans le FOSUserBundle. Pour faire cela, crez simplement votre propre fichier RegistrationController.php, surcharger la mthode originale du bundle, et changez sa fonctionnalit: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // src/Acme/UserBundle/Controller/RegistrationController.php namespace Acme\UserBundle\Controller; use FOS\UserBundle\Controller\RegistrationController as BaseController; class RegistrationController extends BaseController { public function registerAction() { $response = parent::registerAction(); // do custom stuff return $response; } } Suivant le degr de changement de fonctionnalit dont vous avez besoin, vous pourriez appeler parent::registerAction() ou alors remplacer compltement sa logique par la vtre. Surcharger des contrleurs de cette faon fonctionne uniquement si le bundle rfre au contrleur en utilisant la syntaxe standard FOSUserBundle:Registration:register dans les routes et templates. Ceci est la bonne pratique. Surcharger des ressources : templates, routage, validation, etc. La plupart des ressources peuvent aussi tre surcharges en crant simplement un fichier au mme emplacement que dans votre bundle parent. Par exemple, il est trs courant d'avoir besoin de surcharger le template layout.html.twig du FOSUserBundle afin qu'il utilise le layout de base de votre application. Comme le fichier rside l'emplacement Resources/views/layout.html.twig dans le FOSUserBundle, vous pouvez crer votre propre fichier au mme endroit dans le AcmeUserBundle. Symfony va compltement ignorer le fichier tant dans le FOSUserBundle, et utiliser le vtre la place. Il en va de mme pour les fichiers de routage, de configuration de la validation et pour les autres ressources. Surcharger des ressources fonctionne uniquement lorsque vous rfrez des ressources via la mthode @FosUserBundle/Resources/config/routing/security.xml. Si vous rfrez des ressources sans utiliser le raccourci @NomDuBundle, ces dernires ne peuvent alors pas tre surcharges. PDF brought to you by generated on June 26, 2014 Chapter 6: Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle | 24 Les fichiers de traduction ne fonctionnent pas de la mme manire que celle dcrite ci-dessus. Tous les fichiers de traduction sont accumuls dans un ensemble de groupements (un pour chaque domaine). Symfony charge les fichiers de traduction des bundles en premier (dans l'ordre dans lequel les bundles sont initialiss) et ensuite ceux de votre rpertoire app/Resources. Si la mme traduction est spcifie dans deux ressources, c'est la traduction venant de la ressource charge en dernier qui gagnera. PDF brought to you by generated on June 26, 2014 Chapter 6: Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle | 25 Chapter 7 Comment surcharger n'importe quelle partie d'un bundle Ce document est une rfrence succincte sur comment surcharger diffrentes partie d'un bundle tiers. Templates Pour des informations sur la surcharge de templates, lisez * La Surcharge de templates de Bundle * Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle Routage Le routage n'est jamais import automatiquement dans Symfony2. Si vous voulez inclure les routes d'un bundle, alors elles doivent tre importes manuellement un endroit de votre application (ex app/ config/routing.yml). La manire la plus simple de surcharger les routes d'un bundle est de ne pas les importer du tout. Plutt que d'importer les routes d'un bundle tiers, copiez simplement le fichier de routage dans votre application, modifiez le, et importez le. Contrleurs En partant du principe que les bundles tiers n'utisent pas de contrleurs qui ne soient pas des services (ce qui est presque toujours le cas), vous pouvez facilement surcharger les contrleurs grce l'hritage de bundle. Pour plus d'informations, lisez Comment utiliser l'hritage de bundle pour surcharger certaines parties d'un bundle. PDF brought to you by generated on June 26, 2014 Chapter 7: Comment surcharger n'importe quelle partie d'un bundle | 26 Listing 7-1 Listing 7-2 Listing 7-3 Services & Configuration Pour surcharger/tendre un service, vous avez deux options. Premirement, vous pouvez redfinir la valeur du paramtre qui contient le nom de la classe du service en spcifiant votre propre classe dans app/ config/config.yml. Bien sr, cela n'est possible que si le nom de la classe est dfini comme paramtre dans la configuration du service du bundle. Par exemple, pour surcharger la classe utilis par le service translator``de Symfony, vous pouvez surcharger le paramtre ``translator.class. Savoir quel paramtre surcharger peut ncessiter un peu de recherche. Pour le Translator, le paramtre est dfini et utilis dans le fichier Resources/config/translation.xml du FrameworkBundle : 1 2 3 # app/config/config.yml parameters: translator.class: Acme\HelloBundle\Translation\Translator Deuximement, si la classe n'est pas spcifie dans les paramtres, si vous voulez vous assurer que la classe est bien toujours surcharge lorsque votre bundle est utilis, ou si vous avez besoin de faire un peu plus de modifications, vous devrez utiliser une passe de compilation: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/Acme/DemoBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php namespace Acme\DemoBundle\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; class OverrideServiceCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $definition = $container->getDefinition('original-service-id'); $definition->setClass('Acme\DemoBundle\YourService'); } } Dans cet exemple, vous retrouvez la dfinition du service original, et vous changez son nom de classe en votre propre nom de classe. Lisez Comment travailler avec les Passes de Compilation dans les Bundles pour savoir comment utiliser les passes de compilation. Si vous voulez faire plus que simplement surcharger la classe, comme par exemple ajouter une mthode, vous ne pouvez utiliser que la mthode de la passe de compilation. Entits et mapping En cours... Formulaires Pour surcharger un type de formulaire, il faut l'enregistrer comme service (c'est--dire que vous devez le tagger avec form.type ). Vous pourrez alors le surchargez comme vous surchargeriez n'importe quel service, comme c'est expliqu dans Services & Configuration. Bien sr, cela ne fonctionnera que si le type est appel par son alias, et non pas s'il est instanci. Exemple: 1 $builder->add('name', 'custom_type'); PDF brought to you by generated on June 26, 2014 Chapter 7: Comment surcharger n'importe quelle partie d'un bundle | 27 Listing 7-4 au lieu de: 1 $builder->add('name', new CustomType()); Mtadonnes de Validation En cours... Traductions En cours... PDF brought to you by generated on June 26, 2014 Chapter 7: Comment surcharger n'importe quelle partie d'un bundle | 28 Listing 8-1 Chapter 8 Comment supprimer le AcmeDemoBundle La Standard Edition de Symfony2 est livr avec une dmo complte qui vit l'intrieur d'un bundle appel AcmeDemoBundle. C'est un bon repre pour tout dmarrage d'un projet, mais vous aurez probablement envie de finalement le retirer. Cet article prsente AcmeDemoBundle comme exemple, mais vous pouvez suivre les mmes tapes pour supprimer n'importe quel bundle. 1. Dsinscrire le bundle dans AppKernel Pour dconnecter le bundle du framework, vous devez le supprimer de la mthode AppKernel::registerBundles(). Le bundle se trouve normalement dans le tableau $bundles mais il n'est enregistr que pour l'environnement de dveloppement. Vous pouvez le trouver l'interieur de l'instruction if ci-dessous 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // app/AppKernel.php // ... class AppKernel extends Kernel { public function registerBundles() { $bundles = array(...); if (in_array($this->getEnvironment(), array('dev', 'test'))) { // commentez ou supprimez cette ligne // $bundles[] = new Acme\DemoBundle\AcmeDemoBundle(); // ... } } } PDF brought to you by generated on June 26, 2014 Chapter 8: Comment supprimer le AcmeDemoBundle | 29 Listing 8-2 2. Supprimer la configuration du bundle Maintenant que Symfony ne connat plus le bundle, vous devez supprimer toute configuration ou configuration de routing qui se rfre au bundle dans le rpertoire app/config. 2.1 Supprimer le routing du bundle Vous pouvez trouver le routing du AcmeDemoBundle dans le fichier app/config/routing_dev.yml. Supprimez la notation _acme_demo en bas de ce fichier. 2.2 Supprimer la configuration du bundle Certains bundles contiennent des configurations dans l'un des fichiers app/config/config*.yml. Assurez-vous de supprimer la configuration associe ces fichiers. Vous pouvez rapidement reprer la configuration d'un bundle en cherchant la chaine acme_demo (ou n'importe quel autre nom de bundle, i.e fos_user pour le le FOSUserBundle). Le AcmeDemoBundle n'a pas de configuration. Toutefois, le bundle est utilis dans la configuration de scurit dans le fichier app/config/security.yml. Vous pouvez l'utiliser pour votre configuration de scurit mais vous pouvez aussi la supprimer. 3. Supprimer le bundle depuis le systme de fichier Maintenant que vous avez supprim toutes les rfrences du bundle dans votre application, vous pouvez supprimer le bundle depuis le systme de fichier. Le bundle se trouve dans le rpertoire src/Acme/ DemoBundle. Vous devez supprimer ce dossier et vous pouvez aussi supprimer le dossier Acme. Si vous ne connaissez pas o se trouve le bundle, vous pouvez utiliser la mthode getPath() 1 pour rcuprer le chemin vers le bundle: 1 echo $this->container->get('kernel')->getBundle('AcmeDemoBundle')->getPath(); 4. Supprimer l'integration dans d'autres bundles Ceci ne concerne pas le bundle AcmeDemoBundle - aucun autre bundle ne dpend de lui, donc pouvez sauter cette tape. Certains bundles s'appuient sur d'autres, si vous supprimez l'un d'eux, l'autre bundle ne fonctionnera probablement plus. Assurez-vous donc avant de supprimer un bundle qu'aucun autre bundle, tiers ou votre propre bundle, ne dpend de ce bundle. 1. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/Bundle/BundleInterface.html#getPath() PDF brought to you by generated on June 26, 2014 Chapter 8: Comment supprimer le AcmeDemoBundle | 30 Si un bundle s'appuie sur un autre, le plus souvent cela signifie que ce dernier utilise certains services du bundle. Rechercher la chaine de caractre de l'alias du bundle pourrait vous aider les reprer (i.e acme_demo pour les bundles dpendant de AcmeDemoBundle). Si un bundle tiers s'appuie sur un autre bundle, vous pouvez trouver ce bundle mentionn dans le fichier composer.json se trouvant dans le dossier du bundle. PDF brought to you by generated on June 26, 2014 Chapter 8: Comment supprimer le AcmeDemoBundle | 31 Listing 9-1 Chapter 9 Comment exposer une configuration smantique pour un Bundle Si vous ouvrez le fichier de configuration de votre application (en gnral app/config/config.yml), vous allez y trouver un certain nombre de diffrents espaces de noms de configuration, tel que framework, twig, et doctrine. Chacun d'entre eux configure un bundle spcifique, vous permettant de paramtrer des choses un haut niveau et ainsi de laisser le bundle effectuer tous les changements qui restent ; ces derniers tant complexes et de bas niveau. Par exemple, ce qui suit dit au FrameworkBundle d'activer l'intgration de formulaire, qui implique de dfinir pas mal de services ainsi que l'intgration d'autres composants lis : 1 2 3 framework: # ... form: true Lorsque vous crez un bundle, vous avez deux choix concernant la gestion de la configuration : 1. Configuration normale des services (facile) : Vous pouvez spcifier vos services dans un fichier de configuration (par exemple : services.yml) qui se trouve dans votre bundle et puis l'importer depuis la configuration principale de votre application. Cela est vraiment facile, rapide et totalement efficace. Si vous utilisez des paramtres, alors vous avez toujours la flexibilit de personnaliser votre bundle depuis la configuration de votre application. Lisez Importer la Configuration avec imports" pour plus de dtails. 2. Exposer la configuration smantique (avanc) : C'est la manire dont la configuration est faite pour les bundles coeurs (comme dcrit ci-dessus). L'ide de base est que, la place d'avoir l'utilisateur qui surcharge les paramtres individuels, vous laissez l'utilisateur configurer seulement certaines options spcifiquement cres. En tant que dveloppeur du bundle, vous analysez donc cette configuration et chargez les services dans une classe Extension . Avec cette mthode, vous n'aurez pas besoin d'importer quelconque ressource de PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 32 Listing 9-2 configuration depuis la configuration principale de votre application : la classe Extension peut grer tout cela. La seconde option - que vous allez approfondir dans cet article - est beaucoup plus flexible, mais requiert aussi plus de temps mettre en place. Si vous vous demandez quelle mthode vous devriez utiliser, c'est probablement une bonne ide de commencer avec la mthode #1, et de changer pour la #2 si besoin est. La seconde mthode possde plusieurs avantages spcifiques : Beaucoup plus puissante que de dfinir simplement des paramtres : une valeur d'option spcifique pourrait dclencher la cration de plusieurs dfinitions de services ; Possibilit d'avoir une hirarchie dans la configuration ; Fusion intelligente quand plusieurs fichiers de configuration (par exemple : config_dev.yml et config.yml) se surchargent entre eux ; Validation de la configuration (si vous utilisez une Classe de Configuration) ; Autocompltion via l'IDE lorsque vous crez un XSD et que les dveloppeurs utilisent XML. Surcharger les paramtres d'un bundle Si un Bundle fournit une classe d'Extension, alors vous ne devriez gnralement pas surcharger l'un de ses paramtres de conteneur de service. L'ide est que si une classe d'Extension est prsente, chaque paramtre qui doit tre configurable devrait tre prsent dans la configuration rendue accessible par cette classe. En d'autres termes, la classe d'extension dfinit tous les paramtres publics de configuration supports pour lesquels une rtro-compatibilit sera maintenue. Crer une classe d'Extension Si vous choisissez d'exposer une configuration smantique pour votre bundle, vous aurez d'abord besoin de crer une nouvelle classe Extension , qui va grer le processus. Cette classe devrait se trouver dans le rpertoire DependencyInjection de votre bundle et son nom devrait tre construit en remplaant le suffixe Bundle du nom de la classe du Bundle par Extension. Par exemple, la classe d'Extension de AcmeHelloBundle serait nomme AcmeHelloExtension: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\ContainerBuilder; class AcmeHelloExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { // l o toute la logique principale est effectue } public function getXsdValidationBasePath() { return __DIR__.'/../Resources/config/'; } public function getNamespace() { return 'http://www.example.com/symfony/schema/'; PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 33 Listing 9-3 Listing 9-4 Listing 9-5 22 23 } } Les mthodes getXsdValidationBasePath et getNamespace sont requises uniquement si le bundle fournit un XSD optionnel pour la configuration. La prsence de la classe prcdente signifie que vous pouvez maintenant dfinir un espace de noms de configuration acme_hello dans n'importe quel fichier de configuration. L'espace de noms acme_hello est construit sur la base du nom de la classe d'extension en enlevant le mot Extension et en passant le reste du nom en minuscules avec des tirets bas (underscores). En d'autres termes, AcmeHelloExtension devient acme_hello. Vous pouvez immdiatement commencer spcifier la configuration sous cet espace de noms : 1 2 # app/config/config.yml acme_hello: ~ Si vous suivez les conventions de nommage dcrites ci-dessus, alors la mthode load() du code de votre extension est toujours appele tant que votre bundle est dclar dans le Kernel. En d'autres termes, mme si l'utilisateur ne fournit aucune configuration (c-a-d que l'entre acme_hello n'apparat mme pas), la mthode load() sera appele avec un tableau $configs vide comme argument . Vous pouvez toujours fournir des valeurs par dfaut si vous le voulez. Analyser le tableau $configs Chaque fois qu'un utilisateur inclut l'espace de noms acme_hello dans un fichier de configuration, la configuration incluse dans cet espace est ajoute un tableau de configurations et passe la mthode load() de votre extension (Symfony2 convertit automatiquement XML et YAML en un tableau). Prenez la configuration suivante : 1 2 3 4 # app/config/config.yml acme_hello: foo: fooValue bar: barValue Le tableau pass votre mthode load() ressemblera cela: 1 2 3 4 5 6 array( array( 'foo' => 'fooValue', 'bar' => 'barValue', ) ) Notez que ceci est un tableau de tableaux, et pas juste un unique tableau plat contenant les valeurs de configuration. Cela est intentionnel. Par exemple, si acme_hello apparat dans un autre fichier de configuration - disons config_dev.yml - avec des valeurs diffrentes, alors le tableau pass votre mthode load() pourrait ressembler a: PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 34 Listing 9-6 Listing 9-7 Listing 9-8 1 2 3 4 5 6 7 8 9 10 array( array( 'foo' => 'fooValue', 'bar' => 'barValue', ), array( 'foo' => 'fooDevValue', 'baz' => 'newConfigEntry', ), ) L'ordre des deux tableaux dpend duquel est dfini en premier. C'est alors votre travail de dcider comment ces configurations devraient tre fusionnes ensemble. Par exemple, vous pourriez avoir les dernires valeurs crasant les prcdentes ou en quelque sorte les fusionner ensemble. Enfin, dans la section Classe de Configuration, vous apprendrez une manire rellement robuste de grer cela. Mais pour le moment, vous pourriez simplement les fusionner manuellement: 1 2 3 4 5 6 7 8 9 public function load(array $configs, ContainerBuilder $container) { $config = array(); foreach ($configs as $subConfig) { $config = array_merge($config, $subConfig); } // maintenant utilisez le tableau plat $config } Assurez-vous que la technique de fusion ci-dessus ait sens pour votre bundle. Ceci est juste un exemple et vous devriez tre attentif ne pas l'utiliser aveuglment. Utiliser la mthode load() Dans la mthode load(), la variable $container fait rfrence un conteneur qui connat uniquement cet espace de noms de configuration (c-a-d qu'il ne contient pas d'informations de service charges depuis d'autres bundles). Le but de la mthode load() est de manipuler le conteneur, en ajoutant et en configurant n'importe quelles mthodes ou services ncessaires votre bundle. Charger des ressources de configuration externes Une chose commune faire est de charger un fichier de configuration externe qui pourrait contenir l'ensemble des services ncessaires votre bundle. Par exemple, supposons que vous ayez un fichier services.xml contenant la plupart des configurations de service de votre bundle: 1 2 3 4 5 6 use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\Config\FileLocator; public function load(array $configs, ContainerBuilder $container) { // prparer votre variable $config PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 35 Listing 9-9 Listing 9-10 Listing 9-11 7 8 9 10 $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/ config')); $loader->load('services.xml'); } Vous pourriez mme faire ceci conditionnellement, en vous basant sur l'une des valeurs de configuration. Par exemple, supposons que vous vouliez charger un ensemble de services seulement si une option enabled est passe et dfinie comme true : 1 2 3 4 5 6 7 8 9 10 public function load(array $configs, ContainerBuilder $container) { // prparer votre variable $config $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/ config')); if (isset($config['enabled']) && $config['enabled']) { $loader->load('services.xml'); } } Configurer les services et dfinir les paramtres Une fois que vous avez charg des configurations de service, vous pourriez avoir besoin de modifier la configuration base sur certaines valeurs saisies. Par exemple, supposons que vous ayez un service dont le premier argument est une chane de caractres type que le service va utiliser en interne. Vous voudriez que ceci soit facilement configurable par l'utilisateur du bundle, donc dans votre fichier de configuration du services (par exemple : services.xml), vous dfinissez ce service et utilisez un paramtre vide - acme_hello.my_service_type - en tant que premier argument : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!-- src/Acme/HelloBundle/Resources/config/services.xml --> <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="acme_hello.my_service_type" /> </parameters> <services> <service id="acme_hello.my_service" class="Acme\HelloBundle\MyService"> <argument>%acme_hello.my_service_type%</argument> </service> </services> </container> Mais pourquoi dfinir un paramtre vide et puis le passer votre service ? La rponse est que vous allez dfinir ce paramtre dans votre classe d'extension, en vous basant sur les valeurs de configuration saisies. Par exemple, supposons que vous souhaitiez autoriser l'utilisateur dfinir cette option type via une cl nomme my_type. Pour effectuer cela, ajoutez ce qui suit la mthode load(): PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 36 Listing 9-12 1 2 3 4 5 6 7 8 9 10 11 12 13 public function load(array $configs, ContainerBuilder $container) { // prparer votre variable $config $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/ config')); $loader->load('services.xml'); if (!isset($config['my_type'])) { throw new \InvalidArgumentException('The "my_type" option must be set'); } $container->setParameter('acme_hello.my_service_type', $config['my_type']); } Maintenant, l'utilisateur peut effectivement configurer le service en spcifiant la valeur de configuration my_type : 1 2 3 4 # app/config/config.yml acme_hello: my_type: foo # ... Paramtres globaux Lorsque vous configurez le conteneur, soyez conscient que vous avez disposition les paramtres globaux suivants : kernel.name kernel.environment kernel.debug kernel.root_dir kernel.cache_dir kernel.logs_dir kernel.bundle_dirs kernel.bundles kernel.charset Tous les noms de paramtres et services commenant par un _ sont rservs pour le framework, et aucun autre ne doit tre dfini par des bundles. Validation et fusion avec une classe de configuration Jusqu'ici, vous avez effectu la fusion de vos tableaux de configuration manuellement et avez verifi la prsence de valeurs de configuration la main en utilisant la fonction PHP isset(). Un systme de Configuration optionnel est aussi disponible et peut vous aider avec la fusion, la validation, les valeurs par dfaut, et la normalisation des formats. PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 37 Listing 9-13 Listing 9-14 La normalisation des formats se rfre au fait que certains formats - principalement XML - rsultent en de lgres diffrences concernant les tableaux de configuration et que ces tableaux ont besoin d'tre normaliss afin de correspondre avec tout le reste. Pour profiter de ce systme, vous allez crer une classe de Configuration et construire un arbre qui dfinit votre configuration dans cette classe: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // src/Acme/HelloBundle/DependencyInjection/Configuration.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('acme_hello'); $rootNode ->children() ->scalarNode('my_type')->defaultValue('bar')->end() ->end(); return $treeBuilder; } Ceci est un exemple trs simple, mais vous pouvez maintenant utiliser cette classe dans votre mthode load() pour fusionner votre configuration et forcer la validation. Si n'importe quelle autre option que my_type est passe, l'utilisateur sera notifi avec une exception disant qu'une option non-supporte a t passe: 1 2 3 4 5 6 7 public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); // ... } La mthode processConfiguration() utilise l'arbre de configuration que vous avez dfini dans la classe de Configuration pour valider, normaliser et fusionner tous les tableaux de configuration ensemble. La classe de Configuration peut tre bien plus complique que ce qui est montr ici, supportant des noeuds de tableaux, des noeuds prototypes , une validation avance, une normalisation spcifique XML et une fusion avance. Vous pouvez en lire plus ce propos dans the Config Component documentation. Vous pouvez galement voir cela en action en effectuant un checkout d'une des classes de Configuration coeurs, comme par exemple la Configuration du FrameworkBundle 1 ou la Configuration du TwigBundle 2 . 1. https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php 2. https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 38 Listing 9-15 Dump de la Configuration par Dfaut New in version 2.1: La commande config:dump-reference a t ajoute dans Symfony 2.1 La commande config:dump-reference permet d'afficher sur la console la configuration yaml par dfaut du bundle. Tant que votre configuration de bundle est situe l'emplacement standard (YourBundle\DependencyInjection\Configuration) et qu'elle n'a pas de __constructor(), cela fonctionnera automatiquement. Si vous avez quelque chose de diffrent, votre classe Extension devra surcharger la mthode Extension::getConfiguration(). Cette dernire devant retourner une instance de votre Configuration. Des commentaires et exemples peuvent tre ajouts vos noeuds de configuration en utilisant les mthodes ->info() et ->example(): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // src/Acme/HelloBundle/DependencyExtension/Configuration.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('acme_hello'); $rootNode ->children() ->scalarNode('my_type') ->defaultValue('bar') ->info('ce que my_type configure') ->example('exemple de paramtre') ->end() ->end() ; return $treeBuilder; } Ce texte apparat en tant que commentaires yaml sur la sortie de la commande config:dump-reference. Conventions concernant les Extensions Quand vous crez une extension, suivez ces conventions simples : L'extension doit tre stocke dans le sous-espace de noms DependencyInjection ; L'extension doit tre nomme d'aprs le nom du bundle et suffixe avec Extension (AcmeHelloExtension pour AcmeHelloBundle) ; L'extension devrait fournir un schma XSD. Si vous suivez ces conventions simples, vos extensions seront dclares automatiquement par Symfony2. Sinon, rcrivez la mthode build() 3 de Bundle dans votre bundle: PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 39 Listing 9-16 1 2 3 4 5 6 7 8 9 10 11 12 13 // ... use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass; class AcmeHelloBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); // dclare manuellement les extensions qui ne suivent pas les conventions $container->registerExtension(new UnconventionalExtensionClass()); } } Dans ce cas, la classe d'extension doit aussi implmenter une mthode getAlias() et retourner un alias unique nomm aprs le bundle (par exemple : acme_hello). Ceci est requis car le nom de la classe ne suit pas les standards en se terminant par Extension. De plus, la mthode load() de votre extension sera appele uniquement si l'utilisateur spcifie l'alias acme_hello dans au moins un fichier de configuration. De nouveau, ceci est d au fait que la classe d'Extension ne suit pas les standards dfinis plus haut, donc rien ne se fait automatiquement. 3. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/Bundle/Bundle.html#build() PDF brought to you by generated on June 26, 2014 Chapter 9: Comment exposer une configuration smantique pour un Bundle | 40 Listing 10-1 Chapter 10 Comment utiliser Varnish pour acclrer mon site Web Du fait que le cache de Symfony2 utilise les en-ttes HTTP standards, le Symfony2 Reverse Proxy peut facilement tre remplac par n'importe quel autre reverse proxy . Varnish est un acclrateur HTTP puissant et open-source capable de dlivrer du contenu cach rapidement et incluant le support pour les Edge Side Includes. Configuration Comme vu prcdemment, Symfony2 est suffisamment intelligent pour dtecter si il parle un reverse proxy qui comprend ESI ou non. Cela fonctionne automatiquement lorsque vous utilisez le reverse proxy de Symfony2, mais vous avez besoin d'une configuration spciale pour que cela fonctionne avec Varnish. Heureusement, Symfony2 repose dj sur un autre standard crit par Akama (Architecture Edge 1 ). En consquence, les conseils de configuration dcrits dans ce chapitre peuvent tre utiles mme si vous n'utilisez pas Symfony2. Varnish supporte seulement l'attribut src pour les balises ESI (les attributs onerror et alt sont ignors). Tout d'abord, configurez Varnish afin qu'il annonce son support d'ESI en ajoutant un en-tte Surrogate- Capability aux requtes transfres l'application backend : 1 2 3 sub vcl_recv { set req.http.Surrogate-Capability = "abc=ESI/1.0"; } Puis, optimisez Varnish afin qu'il analyse uniquement le contenu de la rponse lorsqu'il y a au moins une balise ESI en vrifiant l'en-tte Surrogate-Control que Symfony2 ajoute automatiquement : 1. http://www.w3.org/TR/edge-arch PDF brought to you by generated on June 26, 2014 Chapter 10: Comment utiliser Varnish pour acclrer mon site Web | 41 Listing 10-2 Listing 10-3 1 2 3 4 5 6 7 8 9 10 sub vcl_fetch { if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; // for Varnish >= 3.0 set beresp.do_esi = true; // for Varnish < 3.0 // esi; } } La compression avec ESI n'tait pas supporte dans Varnish jusqu' la version 3.0 (lire GZIP et Varnish 2 ). Si vous n'utilisez pas Varnish 3.0, mettez un serveur web devant Varnish afin qu'il effectue la compression. Invalidation du Cache Vous ne devriez jamais avoir besoin d'invalider des donnes caches parce que l'invalidation est dj prise en compte nativement dans les modles de cache HTTP (voir Invalidation du cache). Nanmoins, Varnish peut tre configur pour accepter une mthode HTTP spcifique PURGE qui va invalider le cache pour une ressource donne : 1 2 3 4 5 6 7 8 9 10 11 12 sub vcl_hit { if (req.request == "PURGE") { set obj.ttl = 0s; error 200 "Purged"; } } sub vcl_miss { if (req.request == "PURGE") { error 404 "Not purged"; } } Vous devez protger la mthode HTTP PURGE d'une faon ou d'une autre afin d'viter que des personnes purgent vos donnes caches. 2. https://www.varnish-cache.org/docs/3.0/phk/gzip.html PDF brought to you by generated on June 26, 2014 Chapter 10: Comment utiliser Varnish pour acclrer mon site Web | 42 Listing 11-1 Chapter 11 Comment Matriser et Crer de nouveaux Environnements Chaque application est une combinaison de code et d'un ensemble de configurations qui dicte comment ce code doit fonctionner. La configuration peut dfinir la base de donnes tant utilise, si oui ou non quelque chose doit tre mis en cache, ou quel point le logging doit tre dtaill. Dans Symfony2, l'ide d' environnements repose sur le fait que la mme base de code peut tre excute en utilisant des configurations diverses et varies. Par exemple, l'environnement dev devrait utiliser une configuration qui rend le dveloppement facile et sympa, alors que l'environnement de prod devrait utiliser un ensemble de configurations optimises pour la rapidit. Environnements Diffrents, Fichiers de Configuration Diffrents Une application Symfony2 typique dmarre avec trois environnements : dev, prod, et test. Comme nous l'avons vu, chaque environnement reprsente simplement une faon d'excuter la mme base de code avec des configurations diffrentes. Ainsi, cela ne devrait pas tre une surprise pour vous que chaque environnement charge son propre fichier de configuration. Si vous utilisez le format de configuration YAML, les fichiers suivants sont utiliss : pour l'environnement dev : app/config/config_dev.yml pour l'environnement prod : app/config/config_prod.yml pour l'environnement test : app/config/config_test.yml Cela fonctionne grce un simple standard utilis par dfaut dans la classe AppKernel : 1 2 3 4 5 6 7 8 // app/AppKernel.php // ... class AppKernel extends Kernel { // ... PDF brought to you by generated on June 26, 2014 Chapter 11: Comment Matriser et Crer de nouveaux Environnements | 43 Listing 11-2 Listing 11-3 Listing 11-4 Listing 11-5 9 10 11 12 13 public function registerContainerConfiguration(LoaderInterface $loader) { $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml'); } } Comme vous pouvez le voir, lorsque Symfony2 est charg, il utilise l'environnement donn pour dterminer quel fichier de configuration charger. Cela permet d'avoir des environnements multiples d'une manire lgante, puissante et transparente. Bien sr, dans la ralit, chaque environnement diffre seulement quelque peu des autres. Gnralement, tous les environnements vont avoir en commun une mme configuration de base consquente. Ouvrez le fichier de configuration dev et vous verrez facilement comment ceci est accompli : 1 2 3 imports: - { resource: config.yml } # ... Pour partager une configuration commune, chaque fichier de configuration d'un environnement importe en premier lieu un fichier de configuration central (config.yml). Le reste du fichier peut ainsi dvier de la configuration par dfaut en surchargeant des paramtres individuels. Par exemple, par dfaut, la barre d'outils web_profiler est dsactive. Cependant, en environnement dev, la barre d'outils est active en modifiant la valeur par dfaut dans le fichier de configuration dev : 1 2 3 4 5 6 7 # app/config/config_dev.yml imports: - { resource: config.yml } web_profiler: toolbar: true # ... Excuter une Application dans Diffrents Environnements Pour excuter l'application dans chaque environnement, chargez l'application en utilisant soit le contrleur frontal app.php (pour l'environnement prod), soit app_dev.php (pour l'environnement dev) : 1 2 http://localhost/app.php -> environnement *prod* http://localhost/app_dev.php -> environnement *dev* Les URLs donnes supposent que votre serveur web est configur pour utiliser le rpertoire web/ de l'application en tant que racine. Lisez-en plus sur Installer Symfony2. Si vous ouvrez l'un de ces fichiers, vous allez rapidement voir que l'environnement utilis pour chacun est explicitement dfini : 1 2 3 4 <?php require_once __DIR__.'/../app/bootstrap_cache.php'; require_once __DIR__.'/../app/AppCache.php'; PDF brought to you by generated on June 26, 2014 Chapter 11: Comment Matriser et Crer de nouveaux Environnements | 44 Listing 11-6 Listing 11-7 5 6 7 8 9 use Symfony\Component\HttpFoundation\Request; $kernel = new AppCache(new AppKernel('prod', false)); $kernel->handle(Request::createFromGlobals())->send(); Comme vous pouvez le voir, la cl prod spcifie que cet environnement va tre excut dans l'environnement prod. Une application Symfony2 peut tre excute dans n'importe quel environnement en utilisant ce code et en changeant la chane de caractres de l'environnement. L'environnement de test est utilis lorsque vous crivez des tests fonctionnels et n'est pas accessible directement dans le navigateur via un contrleur frontal. En d'autres termes, compar aux autres environnements, il n'y a pas de fichier de contrleur frontal app_test.php. Mode de Dbuggage Quelque chose d'important - sans rapport avec le thme des environnements - est la cl false la ligne 8 du contrleur frontal ci-dessus. Cette ligne spcifie si oui ou non l'application doit tre excute en mode de dbuggage . Peu importe l'environnement, une application Symfony2 peut tre excute avec le mode dbuggage activ ou dsactiv (true ou false). Cela affecte beaucoup de choses dans l'application, comme par exemple si oui ou non les erreurs doivent tre affiches ou si les fichiers de cache sont dynamiquement reconstruits chaque requte. Bien que ce ne soit pas une condition requise, le mode de dbuggage est gnralement dfini comme true pour les environnements dev et test, et comme false pour l'environnement prod. En interne, la valeur du mode de dbuggage devient le paramtre kernel.debug utilis dans le conteneur de service. Si vous regardez le fichier de configuration de l'application, vous verrez le paramtre utilis pour, par exemple, activer ou dsactiver le logging quand vous utilisez le DBAL de Doctrine : 1 2 3 4 doctrine: dbal: logging: "%kernel.debug%" # ... Crer un Nouvel Environnement Par dfaut, une application Symfony2 possde trois environnements qui grent la plupart des cas. Bien sr, comme un environnement n'est rien d'autre qu'une chane de caractres qui correspond un ensemble de configurations, crer un nouvel environnement est assez facile. Par exemple, supposons qu'avant un dploiement, vous ayez besoin d'effectuer des essais sur votre application. Une manire de faire cela est d'utiliser presque les mmes paramtres qu'en production, mais avec le web_profiler de Symfony2 activ. Cela permet Symfony2 d'enregistrer des informations propos de votre application lorsque vous effectuez vos essais. La meilleure manire d'accomplir ceci est grce un nouvel environnement nomm, par exemple, benchmark. Commencez par crer un nouveau fichier de configuration : 1 2 # app/config/config_benchmark.yml imports: PDF brought to you by generated on June 26, 2014 Chapter 11: Comment Matriser et Crer de nouveaux Environnements | 45 Listing 11-8 Listing 11-9 Listing 11-10 Listing 11-11 3 4 5 6 - { resource: config_prod.yml } framework: profiler: { only_exceptions: false } Grce ce simple ajout, l'application supporte dsormais un nouvel environnement appel benchmark. Ce nouveau fichier de configuration importe la configuration de l'environnement prod et la modifie. Cela garantit que le nouvel environnement est identique l'environnement prod, except les changements effectus explicitement ici. Comme vous allez vouloir accder cet environnement via un navigateur, vous devriez aussi crer un contrleur frontal pour lui. Copiez le fichier web/app.php vers web/app_benchmark.php et ditez l'environnement afin qu'il contienne la valeur benchmark : 1 2 3 4 5 6 7 8 9 <?php require_once __DIR__.'/../app/bootstrap.php'; require_once __DIR__.'/../app/AppKernel.php'; use Symfony\Component\HttpFoundation\Request; $kernel = new AppKernel('benchmark', false); $kernel->handle(Request::createFromGlobals())->send(); Le nouvel environnement est maintenant accessible via: 1 http://localhost/app_benchmark.php Certains environnements, comme dev, n'ont jamais pour but d'tre accds par le public sur quelconque serveur dploy. La raison de ceci est que certains environnements, pour des raisons de dbuggage, pourraient donner trop d'informations propos de l'application ou de l'infrastructure sous-jacente. Afin d'tre sr que ces environnements ne soient pas accessibles, le contrleur frontal est gnralement protg des adresses IP externes grce au code suivant plac en haut du contrleur : 1 2 3 if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) { die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); } Les environnements et le rpertoire de Cache Symfony2 profite du cache de diffrentes manires : la configuration de l'application, la configuration du routage, les templates Twig et encore plus sont cachs dans des objets PHP stocks dans des fichiers sur le systme de fichiers. Par dfaut, ces fichiers cachs sont largement stocks dans le rpertoire app/cache. Cependant, chaque environnement cache son propre ensemble de fichiers : PDF brought to you by generated on June 26, 2014 Chapter 11: Comment Matriser et Crer de nouveaux Environnements | 46 1 2 app/cache/dev - rpertoire de cache pour l'environnement *dev* app/cache/prod - rpertoire de cache pour l'environnement *prod* Quelquefois, lorsque vous dbuggez, il pourrait tre utile d'inspecter un fichier cach pour comprendre comment quelque chose fonctionne. Quand vous faites a, rappelez-vous de regarder dans le rpertoire de l'environnement que vous tes en train d'utiliser (la plupart du temps dev lorsque vous dveloppez et dbuggez). Bien que celui-ci puisse varier, le rpertoire app/cache/dev inclut ce qui suit : appDevDebugProjectContainer.php - le conteneur de service cach qui reprsente la configuration cache de l'application ; appdevUrlGenerator.php - la classe PHP gnre sur la base de la configuration de routage et utilise lors de la gnration d'URLs ; appdevUrlMatcher.php - la classe PHP utilise pour la correspondance des routes - regardez ici pour voir la logique des expressions rgulires compiles et utilises pour faire correspondre les URLs entrantes aux diffrentes routes ; twig/ - ce rpertoire contient tous les templates Twig cachs. Vous pouvez facilement changer l'emplacement du rpertoire et son nom. Pour plus d'informations, lisez l'article Comment surcharger la structure de rpertoires par dfaut de Symfony. Aller plus loin Lisez l'article sur Comment configurer les paramtres externes dans le conteneur de services. PDF brought to you by generated on June 26, 2014 Chapter 11: Comment Matriser et Crer de nouveaux Environnements | 47 Listing 12-1 Listing 12-2 Chapter 12 Comment surcharger la structure de rpertoires par dfaut de Symfony Symfony est fourni automatiquement avec une structure de rpertoires par dfaut. Vous pouvez facilement surcharger cette structure de rpertoires et crer la vtre. La structure par dfaut est la suivante : 1 2 3 4 5 6 7 8 9 10 11 12 app/ cache/ config/ logs/ ... src/ ... vendor/ ... web/ app.php ... Surcharger le rpertoire cache Vous pouvez surcharger le rpertoire de cache en surchargeant la mthode getCacheDir dans la classe AppKernel de votre application: 1 2 3 4 5 6 7 // app/AppKernel.php // ... class AppKernel extends Kernel { // ... PDF brought to you by generated on June 26, 2014 Chapter 12: Comment surcharger la structure de rpertoires par dfaut de Symfony | 48 Listing 12-3 Listing 12-4 8 9 10 11 12 public function getCacheDir() { return $this->rootDir.'/'.$this->environment.'/cache/'; } } $this->rootDir est le chemin absolu vers le rpertoire app et $this->environment est l'environnement actuel (c-a-d dev). Dans ce cas, vous avez chang l'emplacement du rpertoire cache pour qu'il devienne app/{environment}/cache. Vous devriez avoir un rpertoire cache diffrent pour chaque environnement, sinon certains effets de bord pourraient survenir. Chaque environnement gnre ses propres fichiers de configuration en cache, et donc, chacun a besoin de son propre rpertoire pour stocker ces fichiers. Surcharger le rpertoire logs Pour surcharger le rpertoire logs, procdez de la mme manire que pour le rpertoire cache. La seule diffrence est que vous devez surcharger la mthode getLogDir: 1 2 3 4 5 6 7 8 9 10 11 12 // app/AppKernel.php // ... class AppKernel extends Kernel { // ... public function getLogDir() { return $this->rootDir.'/'.$this->environment.'/logs/'; } } Ici, vous avez chang l'emplacement du rpertoire pour app/{environment}/logs. Surcharger le rpertoire web Si vous avez besoin de renommer ou de dplacer le rpertoire web, la seule chose dont vous devez vous assurer et que le chemin vers le rpertoire app est toujours correct dans vos contrleurs frontaux app.php et app_dev.php. Si vous renommez simplement le rpertoire, alors tout est dj bon. Mais si vous le dplacez, vous aurez besoin de modifier les chemins dans ces fichiers: 1 2 require_once __DIR__.'/../Symfony/app/bootstrap.php.cache'; require_once __DIR__.'/../Symfony/app/AppKernel.php'; Certains serveurs mutualiss ont un rpertoire racine public_html. Renommer votre rpertoire de web pour public_html est une manire de faire fonctionner votre projet Symfony sur votre serveur mutualis. Une autre manire serait dployer votre application en dehors de la racine web, supprimer le rpertoire public_html puis le remplacer par une lien symbolique vers le rpertoire web de votre projet. PDF brought to you by generated on June 26, 2014 Chapter 12: Comment surcharger la structure de rpertoires par dfaut de Symfony | 49 Listing 12-5 Listing 12-6 Si vous utilisez le bundle AsseticBundle, vous devrez le configurer pour qu'il utilise le bon rpertoire web : 1 2 3 4 5 6 # app/config/config.yml # ... assetic: # ... read_from: "%kernel.root_dir%/../../public_html" Maintenant, vous devez juste exporter vos ressources pour que votre application puisse fonctionner : 1 $ php app/console assetic:dump --env=prod --no-debug PDF brought to you by generated on June 26, 2014 Chapter 12: Comment surcharger la structure de rpertoires par dfaut de Symfony | 50 Listing 13-1 Chapter 13 Comment configurer les paramtres externes dans le conteneur de services Dans le chapitre doc:/cookbook/configuration/environments, Vous avez vu comment grer la configuration de votre application. Parfois on aura cependant besoin de stocker certaines donnes hors du code du projet, par exemple des mots de passe, ou des paramtres de configuration d'une base de donnes. La flexibilit du conteneur de services Symfony vous le permet. Variables d'environnement Symfony va reprer toute variable d'environnement prfixe de SYMFONY__ et la stocker en tant que paramtre dans le conteneur de services. les doubles tirets bas sont remplacs par un point, le point n'tant pas un caractre permis dans un nom de variable d'environnement. Par exemple, si vous utilisez Apache, les variables d'environnement peuvent tre dfinies par la configuration VirtualHost suivante: 1 2 3 4 5 6 7 8 9 10 11 12 <VirtualHost *:80> ServerName Symfony2 DocumentRoot "/path/to/symfony_2_app/web" DirectoryIndex index.php index.html SetEnv SYMFONY__DATABASE__USER user SetEnv SYMFONY__DATABASE__PASSWORD secret <Directory "/path/to/symfony_2_app/web"> AllowOverride All Allow from All </Directory> </VirtualHost> PDF brought to you by generated on June 26, 2014 Chapter 13: Comment configurer les paramtres externes dans le conteneur de services | 51 Listing 13-2 Listing 13-3 Listing 13-4 Listing 13-5 L'exemple de configuration ci-dessus concerne Apache, l'aide de la directive SetEnv 1 . Cependant, ceci fonctionnera pour tout serveur permettant la dfinition de variables d'environnement. De mme, afin de permettre l'usage de variables d'environnement par la console (sans serveur), il est ncessaire de dfinir lesdites variables comme des variables shell. Sur un systme Unix: 1 2 $ export SYMFONY__DATABASE__USER=user $ export SYMFONY__DATABASE__PASSWORD=secret Maintenant que les variables d'environnement ont t dclares, elles seront prsentes dans la variable globale $_SERVER de PHP. Symfony va donc automatiquement recopier les valeurs des variables $_SERVER prfixes de SYMFONY__ en tant que paramtres du conteneur de services. Vous pourrez ainsi faire rfrence ces paramtres si ncessaire. 1 2 3 4 5 6 doctrine: dbal: driver pdo_mysql dbname: symfony2_project user: "%database.user%" password: "%database.password%" Constantes Le conteneur de services permet galement la dfinition de constantes PHP comme paramtres. Il suffit de faire correspondre le nom de votre constante une cl de paramtre et de prciser le type constant. 1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8"?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <parameters> <parameter key="global.constant.value" type="constant">GLOBAL_CONSTANT</parameter> <parameter key="my_class.constant.value" type="constant">My_Class::CONSTANT_NAME</parameter> </parameters> </container> Ceci ne fonctionne qu'avec une configuration XML. Si vous n'utilisez pas XML pour la configuration, importez un fichier XML pour pouvoir le faire: 1 2 3 # app/config/config.yml imports: - { resource: parameters.xml } 1. http://httpd.apache.org/docs/current/env.html PDF brought to you by generated on June 26, 2014 Chapter 13: Comment configurer les paramtres externes dans le conteneur de services | 52 Listing 13-6 Listing 13-7 Diverses considrations La directive imports peut tre utilise pour rcuprer des paramtres stocks ailleurs. L'import d'un fichier PHP vous permet un maximum de flexibilit dans le conteneur. Le code suivant importe un fichier parameters.php. 1 2 3 # app/config/config.yml imports: - { resource: parameters.php } Un fichier de ressource peut tre de plusieurs types. La directive imports accepte des ressources de type PHP, XML, YAML, INI, et closure. Dans le fichier parameters.php, vous allez indiquer au conteneur de services les paramtres que vous dsirez dfinir. Ceci est notamment utile lorsque d'importants lments de configuration sont disponibles dans un format non standard. L'exemple ci-dessous importe des paramtres de configuration de base de donnes pour Drupal dans le conteneur de services. 1 2 3 4 // app/config/parameters.php include_once('/path/to/drupal/sites/default/settings.php'); $container->setParameter('drupal.database.url', $db_url); PDF brought to you by generated on June 26, 2014 Chapter 13: Comment configurer les paramtres externes dans le conteneur de services | 53 Listing 14-1 Chapter 14 Comment stocker les sessions dans la base de donnes grce PdoSessionStorage Par dfaut, Symfony2 stocke les sessions dans des fichiers. La plupart des sites de moyenne et grande envergure vont cependant vouloir stocker les sessions dans la base de donnes plutt que dans des fichiers, car l'usage des bases de donnes permet plus facilement la gestion de la monte en charge dans un environnement multi-serveurs. Symfony2 incorpore une solution de stockage de sessions dans la base de donnes appele PdoSessionHandler 1 . Pour l'utiliser, il vous suffit de changer quelques paramtres dans config.yml (ou le format de configuration de votre choix): New in version 2.1: Dans Symfony2.1 la classe et l'espace de noms ont volu. Vous trouvez dornavant la classe de stockage de session dans l'espace de nom Session\Storage : Symfony\Component\HttpFoundation\Session\Storage. Notez galement que dans Symfony 2.1, vous devrez configurer handler_id et non pas storage_id comme en Symfony2.0. Ci-dessous, vous verrez que %session.storage.options% n'est plus utilis. # app/config/config.yml framework: session: # ... handler_id: session.handler.pdo parameters: pdo.db_options: db_table: session db_id_col: session_id db_data_col: session_value db_time_col: session_time services: pdo: 1. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.html PDF brought to you by generated on June 26, 2014 Chapter 14: Comment stocker les sessions dans la base de donnes grce PdoSessionStorage | 54 Listing 14-2 Listing 14-3 Listing 14-4 class: PDO arguments: dsn: "mysql:dbname=mydatabase" user: myuser password: mypassword session.handler.pdo: class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler arguments: [@pdo, %pdo.db_options%] db_table : Nom de la table des sessions dans votre base de donnes db_id_col : Nom de la colonne identifiant dans la table des sessions (de type VARCHAR(255) ou plus) db_data_col : Nom de la colonne des valeurs dans la table des sessions (de type TEXT ou CLOB) db_time_col : Nom de la colonne temps dans la table des sessions (INTEGER) Partager les informations de connection la base de donnes Avec cette configuration, les paramtres de connexion la base de donnes ne concernent que le stockage des sessions. Ceci peut fonctionner si vous ddiez une base de donnes aux sessions. Mais si vous dsirez stocker les informations de session dans la mme base de donnes que le reste des donnes du projet, vous pouvez rutiliser les paramtres de connexion dfinis dans dans parameter.ini en rfrenant lesdits paramtres : pdo: class: PDO arguments: - "mysql:dbname=%database_name%" - %database_user% - %database_password% Exemple d'instruction SQL MySQL L'instruction SQL pour la cration d'une table de sessions sera probablement proche de : 1 2 3 4 5 6 CREATE TABLE `session` ( `session_id` varchar(255) NOT NULL, `session_value` text NOT NULL, `session_time` int(11) NOT NULL, PRIMARY KEY (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; PostgreSQL Pour PostgreSQL, ce sera plutt : 1 2 3 CREATE TABLE session ( session_id character varying(255) NOT NULL, session_value text NOT NULL, PDF brought to you by generated on June 26, 2014 Chapter 14: Comment stocker les sessions dans la base de donnes grce PdoSessionStorage | 55 4 5 6 session_time integer NOT NULL, CONSTRAINT session_pkey PRIMARY KEY (session_id) ); PDF brought to you by generated on June 26, 2014 Chapter 14: Comment stocker les sessions dans la base de donnes grce PdoSessionStorage | 56 Listing 15-1 Listing 15-2 Chapter 15 Comment utiliser le routeur Apache Symfony2, bien que dj trs rapide, fournit galement plusieurs astuces pour augmenter encore la vitesse avec quelques modifications. L'une de ces astuces est de laisser Apache grer les routes directement plutt que d'utiliser Symfony2 pour le faire. Changer les paramtres de la configuration du routeur Pour dumper les routes Apache, vous devez d'abord modifier les paramtres de configuration pour dire Symfony2 d'utiliser le ApacheUrlMatcher plutt que celui par dfaut : 1 2 3 4 # app/config/config_prod.yml parameters: router.options.matcher.cache_class: ~ # Dsactive le cache du routeur router.options.matcher_class: Symfony\Component\Routing\Matcher\ApacheUrlMatcher Notez que cet ApacheUrlMatcher 1 tend UrlMatcher 2 donc mme si vous ne regnrez pas les rgles url_rewrite, tout fonctionnera (parce qu' la fin de ApacheUrlMatcher::match() un appel parent::match() est ralis). Gnrer les rgles mod_rewrite Pour tester que cela fonctionne, crons une route trs basique pour le bundle de dmo : 1 2 3 4 # app/config/routing.yml hello: pattern: /hello/{name} defaults: { _controller: AcmeDemoBundle:Demo:hello } 1. http://api.symfony.com/2.3/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.html 2. http://api.symfony.com/2.3/Symfony/Component/Routing/Matcher/UrlMatcher.html PDF brought to you by generated on June 26, 2014 Chapter 15: Comment utiliser le routeur Apache | 57 Listing 15-3 Listing 15-4 Listing 15-5 Listing 15-6 Maintenant gnrez les rgles url_rewrite : 1 $ php app/console router:dump-apache -e=prod --no-debug Ce qui devrait afficher quelque chose du genre : 1 2 3 4 5 6 7 # skip "real" requests RewriteCond %{REQUEST_FILENAME} -f RewriteRule .* - [QSA,L] # hello RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$ RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello] Vous pouvez maintenant rcrire le web/.htaccess pour utiliser les nouvelles rgles. Avec cet exemple, cela ressemblerait ceci : 1 2 3 4 5 6 7 8 9 10 11 <IfModule mod_rewrite.c> RewriteEngine On # skip "real" requests RewriteCond %{REQUEST_FILENAME} -f RewriteRule .* - [QSA,L] # hello RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$ RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello] </IfModule> La procdure ci-dessus devrait tre effectue chaque fois que vous ajoutez/modifiez une route si vous voulez en tirer pleinement avantage C'est tout ! Nous sommes maintenant prts utiliser les rgles de routage Apache. Modification supplmentaires Pour gagner un peu de temps d'xcution, changez les occurences de Request par ApacheRequest dans le fichier web/app.php: 1 2 3 4 5 6 7 8 9 10 // web/app.php require_once __DIR__.'/../app/bootstrap.php.cache'; require_once __DIR__.'/../app/AppKernel.php'; //require_once __DIR__.'/../app/AppCache.php'; use Symfony\Component\HttpFoundation\ApacheRequest; $kernel = new AppKernel('prod', false); $kernel->loadClassCache(); PDF brought to you by generated on June 26, 2014 Chapter 15: Comment utiliser le routeur Apache | 58 11 12 //$kernel = new AppCache($kernel); $kernel->handle(ApacheRequest::createFromGlobals())->send(); PDF brought to you by generated on June 26, 2014 Chapter 15: Comment utiliser le routeur Apache | 59 Listing 16-1 Chapter 16 Comment crer une commande pour la Console La page Console de la partie Components (Le Composant Console) dcrit comment crer une commande. Cet article du Cookbook aborde les diffrences lorsque vous crez des commandes pour la console avec le framework Symfony2. Enregistrement automatique des commandes Pour que les commandes soient automatiquement disponibles dans Symfony2, crez un rpertoire Command dans votre bundle et crez un fichier php se terminant par Command.php pour chaque commande que vous voulez crer. Par exemple, si vous voulez tendre le bundle AcmeDemoBundle (disponible dans la Standard Edition de Symfony2) pour vous saluer en ligne de commande, crez le fichier GreetCommand.php et insrez y le contenu suivant: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // src/Acme/DemoBundle/Command/GreetCommand.php namespace Acme\DemoBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class GreetCommand extends ContainerAwareCommand { protected function configure() { $this ->setName('demo:greet') ->setDescription('Saluer une personne') ->addArgument('name', InputArgument::OPTIONAL, 'Qui voulez vous saluer??') ->addOption('yell', null, InputOption::VALUE_NONE, 'Si dfinie, la tche PDF brought to you by generated on June 26, 2014 Chapter 16: Comment crer une commande pour la Console | 60 Listing 16-2 Listing 16-3 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 criera en majuscules') ; } protected function execute(InputInterface $input, OutputInterface $output) { $name = $input->getArgument('name'); if ($name) { $text = 'Bonjour '.$name; } else { $text = 'Bonjour'; } if ($input->getOption('yell')) { $text = strtoupper($text); } $output->writeln($text); } } Cette commande sera maintenant automatiquement prte tre excute : 1 $ app/console demo:greet Fabien Tester les commandes Pour tester les commandes utilises dans le cadre du framework, la classe Symfony\Bundle\FrameworkBundle\Console\Application 1 devrait tre utilise au lieu de la classe Symfony\Component\Console\Application 2 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use Symfony\Component\Console\Tester\CommandTester; use Symfony\Bundle\FrameworkBundle\Console\Application; use Acme\DemoBundle\Command\GreetCommand; class ListCommandTest extends \PHPUnit_Framework_TestCase { public function testExecute() { // mockez le Kernel ou crez en un selon vos besoins $application = new Application($kernel); $application->add(new GreetCommand()); $command = $application->find('demo:greet'); $commandTester = new CommandTester($command); $commandTester->execute(array('command' => $command->getName())); $this->assertRegExp('/.../', $commandTester->getDisplay()); // ... } } 1. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Console/Application.html 2. http://api.symfony.com/2.3/Symfony/Component/Console/Application.html PDF brought to you by generated on June 26, 2014 Chapter 16: Comment crer une commande pour la Console | 61 Listing 16-4 Rcuprer des services du Conteneur de services En utilisant ContainerAwareCommand 3 comme classe parente de la commande (au lieu de la classe basique Command 4 ), vous avez accs au conteneur de services. En d'autres termes, vous avez accs tous les services configurs. Par exemple, vous pouvez facilement tendre la tche pour grer les traductions: 1 2 3 4 5 6 7 8 9 10 protected function execute(InputInterface $input, OutputInterface $output) { $name = $input->getArgument('name'); $translator = $this->getContainer()->get('translator'); if ($name) { $output->writeln($translator->trans('Hello %name%!', array('%name%' => $name))); } else { $output->writeln($translator->trans('Hello!')); } } 3. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.html 4. http://api.symfony.com/2.3/Symfony/Component/Console/Command/Command.html PDF brought to you by generated on June 26, 2014 Chapter 16: Comment crer une commande pour la Console | 62 Listing 17-1 Listing 17-2 Listing 17-3 Listing 17-4 Listing 17-5 Chapter 17 Comment utiliser la Console La page Utiliser les commandes de console, les raccourcis et les commandes prconstruites de la documentation des Composants traite des options de la console. Lorsque vous utilisez la console comme partie intgrante du framework full-stack, des options globales supplmentaires sont galement disponibles. Par dfaut, les commandes de la console sont excutes dans l'environnement de dev, vous pourriez vouloir changer cela pour certaines commandes. Par exemple, vous pourriez vouloir excuter certaines commandes dans l'environnement de prod pour des raisons de performance. Le rsultat de certaines commandes sera diffrent selon l'environnement, par exemple, la commande cache:clear ne videra le cache que de l'environnement spcifi. Pour vider le cache de prod, vous devez excuter : 1 $ php app/console cache:clear --env=prod ou son quivalent : 1 $ php app/console cache:clear -e prod En plus de changer l'environnement, vous pouvez galement choisir de dsactiver le mode debug. Cela peut tre utile lorsque vous voulez excuter des commandes dans l'environnement de dev mais viter de dgrader les performances en collectant des donnes de debug : 1 $ php app/console list --no-debug Il existe un shell interactive qui vous permet de taper des commandes sans devoir spcifier php app/ console chaque fois, ce qui est trs utile si vous devez saisir plusieurs commandes. Pour entrer dans le shell, excutez : 1 2 $ php app/console --shell $ php app/console -s Vous pouvez maintenant vous contenter de saisir simplement le nom de la commande : PDF brought to you by generated on June 26, 2014 Chapter 17: Comment utiliser la Console | 63 Listing 17-6 1 Symfony > list Lorsque vous utilisez le shell, vous pouvez choisir d'excuter chaque commande dans un processus distinct : 1 2 $ php app/console --shell --process-isolation $ php app/console -s --process-isolation Lorsque vous faites cela, la sortie ne sera pas colorise et l'interactivit n'est pas supporte, vous devrez donc passer chaque paramtre explicitement. A moins que vous n'utilisiez des processus spars, vider le cache dans le shell n'aura aucun effet sur les commandes que vous excutez. Ceci est d au fait que les fichiers de cache originaux sont toujours utiliss. PDF brought to you by generated on June 26, 2014 Chapter 17: Comment utiliser la Console | 64 Listing 18-1 Chapter 18 Comment gnrer des URLs et envoyer des emails depuis la Console Malheureusement, le contexte de la ligne de commande n'a pas "conscience" de votre VirtualHost ou votre nom de domaine. Cela signifie que si vous gnrez des URLs absolues travers une Commande Console vous finirez sans doute par obtenir quelque chose comme http://localhost/foo/bar, ce qui n'est pas trs utile. Pour corriger cela, vous devez configurer le "request context", qui est une faon lgante de dire que vous avez besoin de configurer votre environnement afin qu'il connaisse quelle URL il devrait utiliser la gnration d'URLs. Il y a deux faons de configurer le request context : au niveau de l'application ou via une commande. Configurer le Request Contexte globalement Pour configurer le Context Request - qui est utilis par le gnrateur d'URL - vous pouvez redfinir les paramtres qu'il utilise comme valeur par dfaut pour changer l'host par dfaut (localhost) et le scheme (http). Vous devez galement configurer le chemain de base si Symfony ne tourne pas la racine de votre serveur. Notez que ceci n'impacte pas les URLs gnres via les requtes web normales, puisqu'elles remplaces celles par dfauts. 1 2 3 4 5 # app/config/parameters.yml parameters: router.request_context.host: example.org router.request_context.scheme: https router.request_context.base_url: my/path PDF brought to you by generated on June 26, 2014 Chapter 18: Comment gnrer des URLs et envoyer des emails depuis la Console | 65 Listing 18-2 Listing 18-3 Configurer le Request Context via la Command Pour le changer uniquement en une commande vous pouvez simplement rcuprer le Request Context depuis le service router et remplaants ses rglages: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/DemoBundle/Command/DemoCommand.php // ... class DemoCommand extends ContainerAwareCommand { protected function execute(InputInterface $input, OutputInterface $output) { $context = $this->getContainer()->get('router')->getContext(); $context->setHost('example.com'); $context->setScheme('https'); $context->setBaseUrl('my/path'); // ... votre code ici } } Utiliser le Memory Spooling Envoyer des e-mails depuis la commande console se fait de la mme manire que dans le cookbook Comment envoyer un Email hormis le fait que le memory spooling est utilis. En utilisant le memory spooling (consultez le cookbook Comment utiliser le Spool d'Email pour plus d'informations), vous devez savoir que c'est parce que Symfony gre la commande console de manire particulire, que les e-mails ne sont pas envoys automatiquement. Vous devez prendre soin de nttoyer file vous mme. Utiliser le code suivant pour envoyer des emails depuis la commande console: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $message = new \Swift_Message(); // ... prparez le message $container = $this->getContainer(); $mailer = $container->get('mailer'); $mailer->send($message); // maintenant nettoyez la file manuellement $spool = $mailer->getTransport()->getSpool(); $transport = $container->get('swiftmailer.transport.real'); $spool->flushQueue($transport); Une autre option est de crer un environnement qui ne serait utilis uniquement que par la commande console et utiliserait une autre mthode de spooling. S'occuper du spooling n'est uniquement ncessaie que si le memory spolling est utilis. Si vous utilisez le file spooling (ou pas de spooling du tout), il n'est pas utile de nettoyer manuellement dans une commande. PDF brought to you by generated on June 26, 2014 Chapter 18: Comment gnrer des URLs et envoyer des emails depuis la Console | 66 Listing 19-1 Chapter 19 Comment activer les logs dans la commande console Le composant Console ne fournit aucune solution out of the box. Normallement, vous lancez les commandes console manuellement et observez la sortie, c'est pourquoi le systme de log n'est pas fourni. Cependant, il y a quelques cas o vous devriez en avoir besoin. Par example, si vous lancez une commande console sans surveillance, comme des cron ou des scripts de dploiements, il serait plus facile d'utiliser les fonctionnalits de log de Symfony, au lieu de configurer d'autres outils pour rassembler la sortie console et la traiter. Ceci peut tre particulirement pratique si vous avez dj quelques paramtres existants pour aggrger et analyser les logs Symfony. Il y a fondamentalement deux cas de log dont vous auriez besoin : Logguer manuellement quelques informations depuis votre commande; Logguer les exceptions non catches. Logguer manuellement depuis la commande console Cette solution est vraiment simple. Lorsque vous crez une commande console dans le framework complet comme dcrit dans "Comment crer une commande pour la Console", votre commande tend ContainerAwareCommand 1 . Cela signifie que vous pouvez simplement accder au service logger standard travers le conteneur de services et utilisez le pour logguer: 1 2 3 4 5 6 7 8 9 // src/Acme/DemoBundle/Command/GreetCommand.php namespace Acme\DemoBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Psr\Log\LoggerInterface; 1. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.html PDF brought to you by generated on June 26, 2014 Chapter 19: Comment activer les logs dans la commande console | 67 Listing 19-2 Listing 19-3 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class GreetCommand extends ContainerAwareCommand { // ... protected function execute(InputInterface $input, OutputInterface $output) { /** @var $logger LoggerInterface */ $logger = $this->getContainer()->get('logger'); $name = $input->getArgument('name'); if ($name) { $text = 'Hello '.$name; } else { $text = 'Hello'; } if ($input->getOption('yell')) { $text = strtoupper($text); $logger->warning('Yelled: '.$text); } else { $logger->info('Greeted: '.$text); } $output->writeln($text); } } Selon l'environnement dans lequel vous lancez votre commande (et votre paramtrage de logging), vous devriez voir les entres loggues dans le fichier app/logs/dev.log ou app/logs/prod.log`. Activer le logging automatique des Exceptions Pour faire que votre application console rcupre les logs des exceptions manques automatiquement pour toutes vos commande, vous pouvez utiliser console events. New in version 2.3: Les vnements console ont t ajouts en Symfony 2.3. Dans un premier temps, configurez un couteur (listener) pour les vnements exception de la console dans le conteneur de service : 1 2 3 4 5 6 7 8 # app/config/services.yml services: kernel.listener.command_dispatch: class: Acme\DemoBundle\EventListener\ConsoleExceptionListener arguments: logger: "@logger" tags: - { name: kernel.event_listener, event: console.exception } Puis implmentez l'couteur (listener): PDF brought to you by generated on June 26, 2014 Chapter 19: Comment activer les logs dans la commande console | 68 Listing 19-4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // src/Acme/DemoBundle/EventListener/ConsoleExceptionListener.php namespace Acme\DemoBundle\EventListener; use Symfony\Component\Console\Event\ConsoleExceptionEvent; use Psr\Log\LoggerInterface; class ConsoleExceptionListener { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function onConsoleException(ConsoleExceptionEvent $event) { $command = $event->getCommand(); $exception = $event->getException(); $message = sprintf( '%s: %s (uncaught exception) at %s line %s while running console command `%s`', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine(), $command->getName() ); $this->logger->error($message); } } Dans le code ci-dessus, lorsque l'une des commandes lance une exception, le listener recevera un vnement. Vous pouvez simplement logguer en passant le service logger via la configuration du service. Votre mthode reoit un objet ConsoleExceptionEvent 2 , qui a une mthode pour rcuprer les informations concernant l'vnement et l'exception. Logguer les statuts "non-0 exit" L'utilisation du logger de la console peut tre pouss plus loin en logguant les statuts "non-0 exit". De cette faon, vous saurez si une commande comporte des erreurs, mme si une aucune exception n'a t leve. Dans un premier temps, configurez un couteur pour l'vnement console.termine dans le conteneur de services : 1 2 3 4 5 6 # app/config/services.yml services: kernel.listener.command_dispatch: class: Acme\DemoBundle\EventListener\ConsoleTerminateListener arguments: logger: "@logger" 2. http://api.symfony.com/2.3/Symfony/Component/Console/Event/ConsoleExceptionEvent.html PDF brought to you by generated on June 26, 2014 Chapter 19: Comment activer les logs dans la commande console | 69 Listing 19-5 7 8 tags: - { name: kernel.event_listener, event: console.terminate } Puis implmentez l'couteur (listener): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // src/Acme/DemoBundle/EventListener/ConsoleExceptionListener.php namespace Acme\DemoBundle\EventListener; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Psr\Log\LoggerInterface; class ConsoleTerminateListener { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function onConsoleTerminate(ConsoleTerminateEvent $event) { $statusCode = $event->getExitCode(); $command = $event->getCommand(); if ($statusCode === 0) { return; } if ($statusCode > 255) { $statusCode = 255; $event->setExitCode($statusCode); } $this->logger->warning(sprintf( 'Command `%s` exited with status code %d', $command->getName(), $statusCode )); } } PDF brought to you by generated on June 26, 2014 Chapter 19: Comment activer les logs dans la commande console | 70 Listing 20-1 Chapter 20 Comment personnaliser les pages d'erreur Lorsqu'une exception quelconque est lance dans Symfony2, cette dernire est capture par la classe Kernel et ventuellement transmise un contrleur spcial, TwigBundle:Exception:show pour qu'il la gre. Ce contrleur, qui fait partie du coeur de TwigBundle, dtermine quelle template d'erreur afficher et le code de statut qui devrait tre dfini pour l'exception donne. Les pages d'erreur peuvent tre personnalises de deux manires diffrentes, dpendant du niveau de contrle que vous souhaitez : 1. Personnalisez les templates d'erreur des diffrentes pages d'erreur (expliqu ci-dessous) ; 2. Remplacez le contrleur d'exception par dfaut TwigBundle::Exception:show par votre propre contrleur et grez le comme vous le dsirez (voir exception_controller dans la rfrence de Twig) ; La personnalisation de la gestion d'exception est en fait bien plus puissante que ce qui est crit dans ces lignes. Un vnement interne, kernel.exception, est lanc et permet d'avoir le contrle complet de la gestion des exceptions. Pour plus d'informations, voir L'vnement kernel.exception. Tous les templates d'erreur se trouvent dans le TwigBundle. Pour surcharger ces templates, utilisez simplement la mthode standard qui permet de surcharger un template qui se trouve dans un bundle. Pour plus d'informations, voir La Surcharge de templates de Bundle. Par exemple, pour surcharger le template d'erreur par dfaut qui est affich l'utilisateur final, crez un nouveau template situ cet emplacement app/Resources/TwigBundle/views/Exception/ error.html.twig : 1 2 3 4 5 6 7 8 9 <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Une erreur est survenue : {{ status_text }}</title> </head> <body> <h1>Oups! Une erreur est survenue</h1> <h2>Le serveur a retourn une erreur "{{ status_code }} {{ status_text }}".</h2> PDF brought to you by generated on June 26, 2014 Chapter 20: Comment personnaliser les pages d'erreur | 71 10 11 </body> </html> Si vous n'tes pas familier avec Twig, ne vous inquitez pas. Twig est un moteur de templating simple, puissant et optionnel qui est intgr dans Symfony2. Pour plus d'informations propos de Twig, voir Crer et utiliser les templates. En plus de la page d'erreur HTML standard, Symfony fournit une page d'erreur par dfaut pour quasiment tous les formats de rponse les plus communs, incluant JSON (error.json.twig), XML (error.xml.twig), et mme Javascript (error.js.twig), pour n'en nommer que quelques uns. Pour surcharger n'importe lequel de ces templates, crez simplement un nouveau fichier avec le mme nom dans le rpertoire app/Resources/TwigBundle/views/Exception. C'est la manire standard de surcharger n'importe quel template qui se trouve dans un bundle. Personnaliser la page 404 et les autres pages d'erreur Vous pouvez aussi personnaliser des templates d'erreur spcifiques en vous basant sur le code de statut HTTP. Par exemple, crez un template app/Resources/TwigBundle/views/Exception/ error404.html.twig pour afficher une page spciale pour les erreurs 404 (page non trouve). Symfony utilise l'algorithme suivant pour dterminer quel template utiliser : Premirement, il cherche un template pour le format et le code de statut donn (ex error404.json.twig) ; S'il n'existe pas, il cherche un template pour le format donn (ex error.json.twig) ; S'il n'existe pas, il se rabat sur le template HTML (ex error.html.twig). Pour voir la liste complte des templates d'erreur par dfaut, jetez un oeil au rpertoire Resources/ views/Exception du TwigBundle. Dans une installation standard de Symfony2, le TwigBundle se trouve cet emplacement : vendor/symfony/symfony/src/Symfony/Bundle/TwigBundle. Souvent, la faon la plus facile de personnaliser une page d'erreur est de la copier depuis le TwigBundle vers le dossier app/Resources/TwigBundle/views/Exception, puis de la modifier. Les pages d'exception aidant au dbuggage qui sont montres au dveloppeur peuvent aussi tre personnalises de la mme manire en crant des templates comme exception.html.twig pour la page d'exception HTML standard ou exception.json.twig pour la page d'exception JSON. PDF brought to you by generated on June 26, 2014 Chapter 20: Comment personnaliser les pages d'erreur | 72 Listing 21-1 Listing 21-2 Chapter 21 Comment dfinir des contrleurs en tant que Services Dans le Book, vous avez appris comment un contrleur peut facilement tre utilis lorsqu'il tend la classe de base Controller 1 . Bien que ceci fonctionne trs bien, les contrleurs peuvent aussi tre dfinis en tant que services. Pour faire rfrence un contrleur qui est dfini en tant que service, utilisez la notation avec deux-points (:). Par exemple, supposons que vous ayez dfini un service nomm my_controller et que vous voulez le transmettre une mthode appele indexAction() l'intrieur du service: 1 $this->forward('my_controller:indexAction', array('foo' => $bar)); Vous devez utiliser la mme notation quand vous dfinissez la valeur de la route _controller : 1 2 3 my_controller: pattern: / defaults: { _controller: my_controller:indexAction } Pour utiliser un contrleur de cette manire, il doit tre dfini dans la configuration du conteneur de services. Pour plus d'informations, voir le chapitre Service Container. Lorsque vous utilisez un contrleur dfini en tant que service, il ne va pas tendre la classe de base Controller dans la plupart des cas. Au lieu de se reposer sur ses mthodes raccourcis , vous allez intragir directement avec les services dont vous avez besoin. Heureusement, cela est gnralement trs facile et la classe de base Controller elle-mme est une formidable source d'inspiration quant comment effectuer de nombreuses tches usuelles. 1. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Controller/Controller.html PDF brought to you by generated on June 26, 2014 Chapter 21: Comment dfinir des contrleurs en tant que Services | 73 Listing 21-3 Spcifier un contrleur en tant que service demande un petit plus de travail. L'avantage premier est que le contrleur en entier ou tout service pass au contrleur peut tre modifi via la configuration du conteneur de services. Cela est spcialement utile lorsque vous dveloppez un bundle open- source ou tout autre bundle qui va tre utilis dans beaucoup de projets diffrents. Donc, mme si vous ne spcifiez pas vos contrleurs en tant que services, vous allez probablement voir ceci effectu dans quelques bundles Symfony2 open-source. Utiliser les annotations de routage Lorsque vous utilisez les annotations pour dfinir le routage dans un contrleur dfini comme service, vous devrez spcifier votre service comme suit: 1 2 3 4 5 6 7 /** * @Route("/blog", service="my_bundle.annot_controller") * @Cache(expires="tomorrow") */ class AnnotController extends Controller { } Dans cet exemple, my_bundle.annot_controller devrait tre l'id de l'instance du AnnotController dfini dans le conteneur de services. Cette partie est documente dans le chapitre @Route et @Method. PDF brought to you by generated on June 26, 2014 Chapter 21: Comment dfinir des contrleurs en tant que Services | 74 Listing 22-1 Chapter 22 Comment optimiser votre environnement pour le debuggage Quand vous travaillez sur un projet Symfony sur votre machine locale, vous devriez utiliser l'environnement dev (correspondant au contrleur frontal app_dev.php). Cet environnement est optimis pour : Donner au dveloppeur des informations rapides et claires si quelque chose ne se droule pas comme prvu ( l'aide de la web debug toolbar, d'exceptions documentes et prsentes clairement, du profiler, ...) ; tre aussi proche que possible l'environnement de production afin de prparer le dploiement du projet. Dsactiver le bootstrap et le cache des classes Pour rendre l'environnement de production aussi rapide que possible, Symfony cre de longs fichiers PHP dans le dossier cache, qui correspondent l'aggrgation des classes PHP dont votre projet a besoin chaque requte. Cependant, ce comportement peut dsorienter votre IDE ou votre debugger. Nous allons vous montrer ici comment modifier le mcanisme de cache afin qu'il permette un dbuggage des classes intgres Symfony. Le contrleur frontal app_dev.php se compose par dfaut du code suivant: 1 2 3 4 5 6 7 8 9 10 // ... require_once __DIR__.'/../app/bootstrap.php.cache'; require_once __DIR__.'/../app/AppKernel.php'; use Symfony\Component\HttpFoundation\Request; $kernel = new AppKernel('dev', true); $kernel->loadClassCache(); $kernel->handle(Request::createFromGlobals())->send(); PDF brought to you by generated on June 26, 2014 Chapter 22: Comment optimiser votre environnement pour le debuggage | 75 Listing 22-2 Listing 22-3 Pour faciliter le travail du debugger, dsactivez le cache des classes PHP en supprimant l'appel loadClassCache() et en replaant les fichiers requis comme ceci: 1 2 3 4 5 6 7 8 9 10 11 // ... // require_once __DIR__.'/../app/bootstrap.php.cache'; require_once __DIR__.'/../app/autoload.php'; require_once __DIR__.'/../app/AppKernel.php'; use Symfony\Component\HttpFoundation\Request; $kernel = new AppKernel('dev', true); // $kernel->loadClassCache(); $kernel->handle(Request::createFromGlobals())->send(); Si vous dsactivez le cache des classes, n'oubliez pas de revenir aux rglages initiaux aprs votre session de dbuggage. Certains IDEs n'apprcient pas que certaines classes soient enregistres diffrents emplacements. Pour prvenir ces problmes, vous pouvez dsactiver la lecture du dossier cache dans votre IDE, ou changer l'extension utilise par Symfony pour ces fichiers: 1 $kernel->loadClassCache('classes', '.php.cache'); PDF brought to you by generated on June 26, 2014 Chapter 22: Comment optimiser votre environnement pour le debuggage | 76 Chapter 23 Dployer une application Symfony2 Le dploiement peut tre une tche complexe et variable en fonction de votre configuration et de vos besoins. Cet article n'essaie pas de rpondre tout, mais plutt d'aborder les besoins les plus rcurrents et d'apporter quelques ides lors du dploiement. Bases du dploiement Symfony2 Les tapes typiques suivre lors du dploiement d'une application Symfony2 sont : 1. Uploader votre code jour sur le serveur de production; 2. Mettre jour vos dpendances Vendor (en gnral, c'est fait via Composer et cela peut tre fait avant l'upload); 3. Lancer les migrations de base de donnes ou toute tche similaire qui apporterait des changements de structure votre base; 4. Vider (et peut tre plus important encore, faire un "warm up") le cache. Un dploiement peut aussi inclure d'autres tapes comme : Tagger une version particulire de votre code dans votre systme de gestion de code; Crer un espace temporaire pour mettre certaines choses jour hors ligne; Lancer les tests pour garantir la stabilit du code et/ou du serveur; Supprimer tout fichier inutile du rpertoire web pour conserver votre environnement de production propre; Vider les systmes de cache externes (comme Memcached 1 ou Redis 2 ). Comment dployer une application Symfony2 Il y a plusieurs manires de dployer une application Symfony2. Commenons par les bases pour entrer dans les dtails ensuite. 1. http://memcached.org/ 2. http://redis.io/ PDF brought to you by generated on June 26, 2014 Chapter 23: Dployer une application Symfony2 | 77 Listing 23-1 Listing 23-2 Transfert de fichier de base La manire la plus basique de dployer une application est de copier les fichiers manuellement via ftp/ scp (ou une mthode similaire). Cela a quelques inconvnients puisque vous ne contrlez pas tout, notamment le processus de mise jour. Cette mthode implique galement de raliser d'autres tches manuellement aprs le transfert de fichiers (voir Tches communes aprs le dploiement). Utiliser un systme de gestion de version Si vous utilisez un systme de gestion de version (ex git ou svn), vous pouvez vous simplifier la vie en faisant en sorte que votre application en production soit une copie de votre dpt. Ainsi, lorsque vous tes prt mettre jour votre code, il suffit juste de rcuprer les dernires modifications de votre dpt. Cela rend les mises jour de vos fichiers plus facile, mais vous devrez tout de mme vous occuper manuellement d'autres tapes (voir Tches communes aprs le dploiement). Utiliser des scripts et d'autres outils Il existe des outils de qualit pour faciliter le dploiement. Il y a mme certains outils qui ont spcialement t taills pour les besoins de Symfony2, et d'autres qui s'assurent que tout se passe bien avant, pendant et aprs le dploiement. Lisez Les outils pour une liste des outils qui peuvent vous aider dployer. Tches communes aprs le dploiement Aprs avoir dploy votre code source, il y a un certain nombre de choses faire : A) Configurer votre fichier app/config/parameters.ini Ce fichier devrait tre personnalis sur chaque systme. La mthode que vous utilisez pour dployer votre code source de doit pas dployer ce fichier. Vous devriez plutt le dfinir manuellement (ou via un processus) sur votre serveur. B) Mettre jour les vendors Vos vendors peuvent tre mis jour avant de transfrer votre code source (mettez jour votre rpertoire vendor/ puis transfrez le avec le reste de votre code source) ou aprs sur le serveur. Peu importe ce que vous choisissez, mettez jour vos vendors comme vous le faites d'habitude : 1 $ php composer.phar install --optimize-autoloader L'option --optimize-autoloader rend l'autoloader de Composer plus performant en construisant une map . C) Videz votre cache Symfony Assurez vous de vider (et faire un warm up) de votre cache : 1 $ php app/console cache:clear --env=prod --no-debug PDF brought to you by generated on June 26, 2014 Chapter 23: Dployer une application Symfony2 | 78 Listing 23-3 D) Dumpez vos ressources Assetic Si vous utilisez Assetic, vous devrez aussi dumpez vos ressources : 1 $ php app/console assetic:dump --env=prod --no-debug E) Et bien d'autres ! Il y a encore bien d'autres choses que vous devrez peut tre faire selon votre configuration : Lancer vos migrations de base de donnes Vider votre cache APC Lancer assets:install (dj dans composer.phar install) Ajouter/diter des tches CRON Mettre vos ressources sur un CDN ... Cycle de vie de l'application : intgration continue, qualit, ... Alors que cet article couvre les aspects techniques du dploiement, le cycle de vie complet du code depuis le dveloppement jusqu'au serveur de production peut contenir bien d'autres tapes (dploiement en prproduction, qualit, lancement des tests, ...). L'utilisation de la prproduction, des tests, de l 'assurance qualit, de l'intgration continue, des migrations de base de donnes et la capacit de retour arrire en cas d'chec sont fortement recommands. Il existes des outils simples ou plus complexes qui vous permettent de simplifier le dploiement. N'oubliez pas que dployer votre application implique galement de mettre jour vos dpendances (gnralement avec Composer), mettre jour votre base de donnes, vider votre cache et de raliser potentiellement d'autres chose comme mettre vos ressources sur un CDN (voir Tches communes aprs le dploiement). Les outils Capifony 3 : Cet outil fournit un ensemble d'outils spcialiss bass sur Capistrano et taills spcifiquement pour les projets symfony et Symfony2. sf2debpkg 4 : Cet outil aide construire un paquet natif Debian pour vos projets Symfony2. Magallanes 5 : Cet outil de dploiement semblable Capistrano est construit en PHP et est peut tre plus facile tendre pour les dveloppeurs PHP qui ont des besoins spcifiques. Bundles: 3. http://capifony.org/ 4. https://github.com/liip/sf2debpkg 5. https://github.com/andres-montanez/Magallanes PDF brought to you by generated on June 26, 2014 Chapter 23: Dployer une application Symfony2 | 79 Il existe plusieurs bundles qui proposent des fonctionnalits lis au dploiement 6 directement dans votre console Symfony2. Scripts de base: Vous pouvez bien sur utiliser le shell, Ant 7 , ou n'importe quel autre outil de script pour dployer vos projets. Platform as a Service Providers: Paas est une manire relativement nouvelle de dployer votre application. Typiquement, une Paas utilisera un unique fichier de configuration la racine de votre projet pour dterminer comment construire un environnement la vole qui supporte votre logiciel. PagodaBox 8 possde un excellent support de Symfony2. Vous voulez en savoir plus ? Discutez avec la communaut sur le canal IRC Symfony 9 #symfony (sur freenode) pour plus d'informations. 6. http://knpbundles.com/search?q=deploy 7. http://blog.sznapka.pl/deploying-symfony2-applications-with-ant 8. https://github.com/jmather/pagoda-symfony-sonata-distribution/blob/master/Boxfile 9. http://webchat.freenode.net/?channels=symfony PDF brought to you by generated on June 26, 2014 Chapter 23: Dployer une application Symfony2 | 80 Listing 24-1 Chapter 24 Comment grer les uploads de fichier avec Doctrine Grer les uploads de fichier avec les entits Doctrine n'est en rien diffrent de grer n'importe quel autre upload. En d'autres termes, vous tes libre de dplacer le fichier via votre contrleur aprs avoir gr la soumission du formulaire. Pour voir des exemples, rfrez-vous la page de rfrence du type fichier. Si vous le choisissez, vous pouvez aussi intgrer l'upload de fichier dans le cycle de vie de votre entit (c-a-d cration, mise jour et suppression). Dans ce cas, lorsque votre entit est cre, mise jour et supprime de Doctrine, les processus d'upload et de suppression du fichier se feront de manire automatique (sans avoir besoin de faire quoi que ce soit dans votre contrleur). Pour que cela fonctionne, vous allez avoir besoin de prendre en compte un certain nombre de dtails qui vont tre couverts dans cet article du Cookbook. Installation basique Tout d'abord, crez une classe Entit Doctrine simple avec laquelle nous allons travailler: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/DemoBundle/Entity/Document.php namespace Acme\DemoBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity */ class Document { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 81 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 */ public $id; /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank */ public $name; /** * @ORM\Column(type="string", length=255, nullable=true) */ public $path; public function getAbsolutePath() { return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path; } public function getWebPath() { return null === $this->path ? null : $this->getUploadDir().'/'.$this->path; } protected function getUploadRootDir() { // le chemin absolu du rpertoire o les documents uploads doivent tre sauvegards return __DIR__.'/../../../../web/'.$this->getUploadDir(); } protected function getUploadDir() { // on se dbarrasse de __DIR__ afin de ne pas avoir de problme lorsqu'on affiche // le document/image dans la vue. return 'uploads/documents'; } } L'entit Document a un nom et est associe un fichier. La proprit path stocke le chemin relatif du fichier et est persiste dans la base de donnes. La mthode getAbsolutePath() est un moyen pratique de retourner le chemin absolu du fichier alors que la mthode getWebPath() permet de retourner le chemin web, qui lui peut tre utilis dans un template pour ajouter un lien vers le fichier upload. Si vous ne l'avez pas dj fait, vous devriez probablement lire la documentation du type fichier en premier afin de comprendre comment le processus de base de l'upload fonctionne. Si vous utilisez les annotations pour spcifier vos rgles de validation (comme montr dans cet exemple), assurez-vous d'avoir activ la validation via les annotations (voir configuration de la validation). Pour grer l'upload de fichier dans le formulaire, utilisez un champ virtuel file. Par exemple, si vous construisez votre formulaire directement dans un contrleur, cela ressemblerait quelque chose comme a: PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 82 Listing 24-2 Listing 24-3 Listing 24-4 1 2 3 4 5 6 7 8 9 10 11 public function uploadAction() { // ... $form = $this->createFormBuilder($document) ->add('name') ->add('file') ->getForm(); // ... } Ensuite, crez cette proprit dans votre classe Document et ajoutez quelques rgles de validation: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/DemoBundle/Entity/Document.php // ... class Document { /** * @Assert\File(maxSize="6000000") */ public $file; // ... } Comme vous utilisez la contrainte File, Symfony2 va automatiquement deviner que le champ du formulaire est un champ d'upload de fichier. C'est pourquoi vous n'avez pas eu le spcifier explicitement lors de la cration du formulaire ci-dessus (->add('file')). Le contrleur suivant vous montre comment grer le processus en entier: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use Acme\DemoBundle\Entity\Document; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; // ... /** * @Template() */ public function uploadAction() { $document = new Document(); $form = $this->createFormBuilder($document) ->add('name') ->add('file') ->getForm() ; if ($this->getRequest()->isMethod('POST')) { $form->bind($this->getRequest()); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($document); PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 83 Listing 24-5 Listing 24-6 Listing 24-7 23 24 25 26 27 28 29 30 $em->flush(); $this->redirect($this->generateUrl(...)); } } return array('form' => $form->createView()); } Lorsque vous crivez le template, n'oubliez pas de spcifier l'attribut enctype : <h1>Upload File</h1> <form action="#" method="post" {{ form_enctype(form) }}> {{ form_widget(form) }} <input type="submit" value="Upload Document" /> </form> Le contrleur prcdent va automatiquement persister l'entit Document avec le nom soumis, mais elle ne va rien faire propos du fichier et la proprit path sera vide. Une manire facile de grer l'upload de fichier est de le dplacer juste avant que l'entit soit persiste et ainsi spcifier la proprit path en consquence. Commencez par appeler une nouvelle mthode upload() sur la classe Document que vous allez crer dans un moment pour grer l'upload de fichier: 1 2 3 4 5 6 7 8 9 10 if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $document->upload(); $em->persist($document); $em->flush(); $this->redirect(...); } La mthode upload() va tirer parti de l'objet UploadedFile 1 , qui correspond ce qui est retourn aprs qu'un champ file ait t soumis: 1 2 3 4 5 6 7 8 9 10 11 12 13 public function upload() { // la proprit file peut tre vide si le champ n'est pas requis if (null === $this->file) { return; } // utilisez le nom de fichier original ici mais // vous devriez l'assainir pour au moins viter // quelconques problmes de scurit // la mthode move prend comme arguments le rpertoire cible et // le nom de fichier cible o le fichier doit tre dplac 1. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/File/UploadedFile.html PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 84 Listing 24-8 Listing 24-9 14 15 16 17 18 19 20 21 22 $this->file->move($this->getUploadRootDir(), $this->file->getClientOriginalName()); // dfinit la proprit path comme tant le nom de fichier o vous // avez stock le fichier $this->path = $this->file->getClientOriginalName(); // nettoie la proprit file comme vous n'en aurez plus besoin $this->file = null; } Utiliser les callbacks du cycle de vie (lifecycle) Mme si cette implmentation fonctionne, elle souffre d'un dfaut majeur : que se passe-t-il s'il y a un problme lorsque l'entit est persiste ? Le fichier serait dj dplac vers son emplacement final mme si la proprit path de l'entit n'a pas t persiste correctement. Pour viter ces problmes, vous devriez changer l'implmentation afin que les oprations sur la base de donnes et le dplacement du fichier deviennent atomiques : s'il y a un problme en persistant l'entit ou si le fichier ne peut pas tre dplac, alors rien ne devrait se passer. Pour faire cela, vous devez dplacer le fichier aussitt que Doctrine persiste l'entit dans la base de donns. Ceci peut tre accompli en s'interfrant dans le cycle de vie de l'entit via un callback: 1 2 3 4 5 6 7 /** * @ORM\Entity * @ORM\HasLifecycleCallbacks */ class Document { } Ensuite, rfactorisez la classe Document pour tirer parti de ces callbacks: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use Symfony\Component\HttpFoundation\File\UploadedFile; /** * @ORM\Entity * @ORM\HasLifecycleCallbacks */ class Document { /** * @ORM\PrePersist() * @ORM\PreUpdate() */ public function preUpload() { if (null !== $this->file) { // faites ce que vous voulez pour gnrer un nom unique $this->path = sha1(uniqid(mt_rand(), true)).'.'.$this->file->guessExtension(); } } /** * @ORM\PostPersist() PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 85 Listing 24-10 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 * @ORM\PostUpdate() */ public function upload() { if (null === $this->file) { return; } // s'il y a une erreur lors du dplacement du fichier, une exception // va automatiquement tre lance par la mthode move(). Cela va empcher // proprement l'entit d'tre persiste dans la base de donnes si // erreur il y a $this->file->move($this->getUploadRootDir(), $this->path); unset($this->file); } /** * @ORM\PostRemove() */ public function removeUpload() { if ($file = $this->getAbsolutePath()) { unlink($file); } } } La classe effectue maintenant tout ce dont vous avez besoin : elle gnre un nom de fichier unique avant de le persister, dplace le fichier aprs avoir persist l'entit, et efface le fichier si l'entit est supprime. Maintenant que le dplacement du fichier est automatiquement pris en charge par l'entit, l'appel de la mthode $document->upload() devrait tre supprim du contrleur: 1 2 3 4 5 6 7 8 if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($document); $em->flush(); $this->redirect(...); } Les vnements de callback @ORM\PrePersist() et @ORM\PostPersist() sont dclenchs avant et aprs que l'entit soit persiste dans la base de donnes. D'autre part, les vnements de callback @ORM\PreUpdate() et @ORM\PostUpdate() sont appels lorsque l'entit est mise jour. Les callbacks PreUpdate et PostUpdate sont dclenchs seulement s'il y a un changement dans l'un des champs de l'entit tant persiste. Cela signifie que, par dfaut, si vous modifiez uniquement la proprit $file, ces vnements ne seront pas dclenchs, comme la proprit elle-mme n'est pas directement persiste via Doctrine. Une solution pourrait tre d'utiliser un champ updated qui soit persist dans Doctrine, et de le modifier manuellement lorsque le fichier est chang. PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 86 Listing 24-11 Utiliser l'id en tant que nom de fichier Si vous voulez utiliser l'id comme nom de fichier, l'implmentation est lgrement diffrente car vous devez sauvegarder l'extension dans la proprit path, la place du nom de fichier actuel: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 use Symfony\Component\HttpFoundation\File\UploadedFile; /** * @ORM\Entity * @ORM\HasLifecycleCallbacks */ class Document { // proprit utilis temporairement pour la suppression private $filenameForRemove; /** * @ORM\PrePersist() * @ORM\PreUpdate() */ public function preUpload() { if (null !== $this->file) { $this->path = $this->file->guessExtension(); } } /** * @ORM\PostPersist() * @ORM\PostUpdate() */ public function upload() { if (null === $this->file) { return; } // vous devez lancer une exception ici si le fichier ne peut pas // tre dplac afin que l'entit ne soit pas persiste dans la // base de donnes comme le fait la mthode move() de UploadedFile $this->file->move($this->getUploadRootDir(), $this->id.'.'.$this->file->guessExtension()); unset($this->file); } /** * @ORM\PreRemove() */ public function storeFilenameForRemove() { $this->filenameForRemove = $this->getAbsolutePath(); } /** * @ORM\PostRemove() */ public function removeUpload() { PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 87 55 56 57 58 59 60 61 62 63 if ($this->filenameForRemove) { unlink($this->filenameForRemove); } } public function getAbsolutePath() { return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path; } } Vous noterez que dans ce cas, vous devez effectuer un peu plus de travail pour supprimer le fichier. Avant qu'il soit supprim, vous devez stocker le chemin du fichier (puisqu'il dpend de l'id). Ensuite, une fois que l'objet est bien compltement supprim de la base de donnes, vous pouvez supprimer le fichier en toute scurit (dans PostRemove). PDF brought to you by generated on June 26, 2014 Chapter 24: Comment grer les uploads de fichier avec Doctrine | 88 Chapter 25 Comment utiliser les extensions Doctrine: Timestampable, Sluggable, Translatable, etc. Doctrine2 est trs flexible, et la communaut a dj cr une srie d'extensions Doctrine trs pratiques afin de vous aider avec les tches usuelles lies aux entits. Une bibliothque en particulier - la bibliothque DoctrineExtensions 1 - fournit l'intgration de fonctionnalits pour les comportements (Behaviors) Sluggable 2 , Translatable 3 , Timestampable 4 , Loggable 5 , Tree 6 et Sortable 7 L'utilisation de chacune de ces extensions est explique dans son dpt. Toutefois, pour installer/activer chaque extension, vous devez enregistrer et activer un Ecouteur d'vnement (Event Listener). Pour faire cela, vous avez deux possibilits : 1. Utiliser le bundle StofDoctrineExtensionsBundle 8 , qui intgre la bibliothque ci-dessus. 2. Implmenter ces services directement en suivant la documentation pour l'intgration dans Symfony2 : Installer les extensions Gedmo Doctrine2 dans Symfony2 9 1. https://github.com/l3pp4rd/DoctrineExtensions 2. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sluggable.md 3. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md 4. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/timestampable.md 5. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/loggable.md 6. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/tree.md 7. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sortable.md 8. https://github.com/stof/StofDoctrineExtensionsBundle 9. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/symfony2.md PDF brought to you by generated on June 26, 2014 Chapter 25: Comment utiliser les extensions Doctrine: Timestampable, Sluggable, Translatable, etc. | 89 Listing 26-1 Chapter 26 Comment enregistrer des listeners ( couteurs en franais) et des souscripteurs d'vnement Doctrine est fourni avec un riche systme d'vnement qui lance des vnements chaque fois - ou presque - que quelque chose se passe dans le systme. Pour vous, cela signifie que vous pouvez crer arbitrairement des services et dire Doctrine de notifier ces objets chaque fois qu'une certaine action (par exemple : prePersist) a lieu dans Doctrine. Cela pourrait tre utile par exemple de crer un index de recherche indpendant chaque fois qu'un objet est sauvegard dans votre base de donnes. Doctrine dfinit deux types d'objets qui peuvent couter des vnements Doctrine : les listeners et les souscripteurs d'vnement. Les deux sont trs similaires, mais les listeners sont un peu plus simples. Pour plus d'informations, voir Le systme d'vnements 1 sur le site de Doctrine. Configurer le listener/souscripteur d'vnement Pour spcifier un service d'agir comme un listener d'vnements ou comme un souscripteur, vous devez simplement le tagger avec un nom appropri. Selon votre cas, vous pouvez placer un listener dans chaque connexion DBAL et gestionnaire d'entit ORM ou juste dans une connexion DBAL spcifique et tous les gestionnaires d'entit qui utilisent cette connexion. 1 2 3 4 5 6 7 8 doctrine: dbal: default_connection: default connections: default: driver: pdo_sqlite memory: true 1. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html PDF brought to you by generated on June 26, 2014 Chapter 26: Comment enregistrer des listeners ( couteurs en franais) et des souscripteurs d'vnement | 90 Listing 26-2 9 10 11 12 13 14 15 16 17 18 19 20 21 services: my.listener: class: Acme\SearchBundle\Listener\SearchIndexer tags: - { name: doctrine.event_listener, event: postPersist } my.listener2: class: Acme\SearchBundle\Listener\SearchIndexer2 tags: - { name: doctrine.event_listener, event: postPersist, connection: default } my.subscriber: class: Acme\SearchBundle\Listener\SearchIndexerSubscriber tags: - { name: doctrine.event_subscriber, connection: default } Crer la Classe du Listener Dans l'exemple prcdent, un service my.listener a t configur en tant que listener Doctrine sur l'vnement postPersist. Cette classe derrire ce service doit avoir une mthode postPersist, qui va tre appele lorsque l'vnement est lanc: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // src/Acme/SearchBundle/Listener/SearchIndexer.php namespace Acme\SearchBundle\Listener; use Doctrine\ORM\Event\LifecycleEventArgs; use Acme\StoreBundle\Entity\Product; class SearchIndexer { public function postPersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); $entityManager = $args->getEntityManager(); // peut-tre voulez-vous seulement agir sur une entit Product if ($entity instanceof Product) { // faites quelque chose avec l'entit Product } } } Dans chaque vnement, vous avez accs un objet LifecycleEventArgs, qui vous donne accs l'objet entit de l'vnement ainsi qu'au gestionnaire d'entits lui-mme. Une chose importante noter est qu'un listener va couter toutes les entits de votre application. Donc, si vous ne voulez grer qu'un type spcifique d'entit (par exemple : une entit Product mais pas une entit BlogPost), vous devez vrifier le nom de la classe de votre entit dans votre mthode (comme montr ci-dessus). PDF brought to you by generated on June 26, 2014 Chapter 26: Comment enregistrer des listeners ( couteurs en franais) et des souscripteurs d'vnement | 91 Listing 27-1 Chapter 27 Comment utiliser la couche DBAL de Doctrine Cet article traite de la couche DBAL de Doctrine. Typiquement, vous allez travailler avec le haut niveau de la couche ORM de Doctrine, qui utilise le DBAL en arrire-plan pour effectivement communiquer avec la base de donnes. Pour en lire plus propos de l'ORM Doctrine, voir Doctrine et les bases de donnes . La couche d'abstraction de la base de donnes (DBAL pour DataBase Abstraction Layer) de Doctrine 1 se situe au plus haut niveau de PDO 2 et offre une API intuitive et flexible pour communiquer avec les bases de donnes relationnelles les plus populaires. En d'autres termes, la bibliothque DBAL rend facile l'excution de requtes et autres actions sur la base de donnes. Lisez la Documentation DBAL 3 officielle de Doctrine pour apprendre tous les dtails et capacits de la bibliothque DBAL de Doctrine. Pour dmarrer, configurez les paramtres de connexion de la base de donnes : 1 2 3 4 5 6 7 8 # app/config/config.yml doctrine: dbal: driver: pdo_mysql dbname: Symfony2 user: root password: null charset: UTF8 Pour une liste complte des options de configuration de DBAL, voir Configuration du DBAL Doctrine. Vous pouvez alors accder la connexion DBAL de Doctrine en utilisant le service database_connection : 1. http://www.doctrine-project.org 2. http://www.php.net/pdo 3. http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/index.html PDF brought to you by generated on June 26, 2014 Chapter 27: Comment utiliser la couche DBAL de Doctrine | 92 Listing 27-2 Listing 27-3 Listing 27-4 1 2 3 4 5 6 7 8 9 10 class UserController extends Controller { public function indexAction() { $conn = $this->get('database_connection'); $users = $conn->fetchAll('SELECT * FROM users'); // ... } } Dclarer des Types de Correspondance Personnaliss Vous pouvez dclarer des types de correspondance personnaliss via la configuration de Symfony. Ils seront ajouts toutes les connexions configures. Pour plus d'informations sur les types de correspondances personnaliss, lisez la section types de correspondances personnaliss 4 de la documentation de Doctrine. 1 2 3 4 5 6 # app/config/config.yml doctrine: dbal: types: custom_first: Acme\HelloBundle\Type\CustomFirst custom_second: Acme\HelloBundle\Type\CustomSecond Dclarer des Types de Correspondance Personnaliss via le SchemaTool Le SchemaTool est utilis pour inspecter la base de donnes afin d'en comparer le schma. Pour effectuer cette tche, il a besoin de connatre quel type de correspondance utiliser pour chaque type de base de donnes. En dclarer de nouveaux peut tre effectu grce la configuration. Faisons correspondre le type ENUM (non-support par DBAL par dfaut) un type string : 1 2 3 4 5 6 7 8 # app/config/config.yml doctrine: dbal: connections: default: // Autres paramtres de connexion mapping_types: enum: string 4. http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types PDF brought to you by generated on June 26, 2014 Chapter 27: Comment utiliser la couche DBAL de Doctrine | 93 Listing 28-1 Chapter 28 Comment gnrer des Entits partir d'une base de donnes existante Lorsque vous commencez travailler sur un tout nouveau projet qui utilise une base de donnes, deux situations diffrentes peuvent arriver. Dans la plupart des cas, le modle de base de donnes est conu et construit de zro. Dans d'autres cas, cependant, vous commencerez avec un modle de base de donnes existant et probablement inchangeable. Heureusement, Doctrine est fourni avec une srie d'outils vous aidant gnrer les classes de modle partir de votre base de donnes existante. Comme la documentation des outils Doctrine 1 le dit, faire du reverse engineering est un processus qu'on n'effectue qu'une seule fois lorsqu'on dmarre un projet. Doctrine est capable de convertir environ 70-80% des informations de correspondance ncessaires en se basant sur les champs, les index et les contraintes de cls trangres. En revanche, Doctrine ne peut pas identifier les associations inverses, les types d'inhritance, les entits avec cls trangres en tant que cls primaires ou les oprations smantiques sur des associations telles que la cascade ou les vnements de cycle de vie. Ainsi, du travail additionnel sur les entits gnres sera ncessaire par la suite pour finir la conception de chacune afin de satisfaire les spcificits du modle de votre domaine. Ce tutoriel suppose que vous utilisez une application de blog simple avec les deux tables suivantes : blog_post et blog_comment. Une entre comment est lie une entre post grce une contrainte de cl trangre. 1 2 3 4 5 6 7 8 CREATE TABLE `blog_post` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL, `content` longtext COLLATE utf8_unicode_ci NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 1. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/tools.html#reverse-engineering PDF brought to you by generated on June 26, 2014 Chapter 28: Comment gnrer des Entits partir d'une base de donnes existante | 94 Listing 28-2 Listing 28-3 9 10 11 12 13 14 15 16 17 18 CREATE TABLE `blog_comment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `post_id` bigint(20) NOT NULL, `author` varchar(20) COLLATE utf8_unicode_ci NOT NULL, `content` longtext COLLATE utf8_unicode_ci NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`), KEY `blog_comment_post_id_idx` (`post_id`), CONSTRAINT `blog_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_post` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; Avant d'aller plus loin, assurez-vous que vos paramtres de connexion la base de donnes sont correctement dfinis dans le fichier app/config/parameters.yml (ou quelconque endroit que ce soit o votre configuration de base de donnes est conserve) et que vous avez initialis un bundle qui va contenir votre future classe entit. Dans ce tutorial, nous supposerons qu'un AcmeBlogBundle existe et qu'il se situe dans le dossier src/Acme/BlogBundle. La premire tape permettant de construire les classes entit depuis une base de donnes existante est de demander Doctrine d'introspecter cette dernire et de gnrer les fichiers de mta-donnes correspondants. Les fichiers de mta-donnes dcrivent la classe entit gnrer en se basant sur les champs des tables. 1 $ php app/console doctrine:mapping:convert xml ./src/Acme/BlogBundle/Resources/config/ doctrine/metadata/orm --from-database --force Cette outil de ligne de commande demande Doctrine d'introspecter la base de donnes et de gnrer les fichiers XML de mta-donnes dans le dossier src/Acme/BlogBundle/Resources/config/doctrine/ metadata/orm de votre bundle. Il est aussi possible de gnrer les mta-donnes au format YAML en changeant le premier argument pour yml. Le fichier de mta-donnes BlogPost.dcm.xml gnr ressemble ce qui suit : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="utf-8"?> <doctrine-mapping> <entity name="BlogPost" table="blog_post"> <change-tracking-policy>DEFERRED_IMPLICIT</change-tracking-policy> <id name="id" type="bigint" column="id"> <generator strategy="IDENTITY"/> </id> <field name="title" type="string" column="title" length="100"/> <field name="content" type="text" column="content"/> <field name="isPublished" type="boolean" column="is_published"/> <field name="createdAt" type="datetime" column="created_at"/> <field name="updatedAt" type="datetime" column="updated_at"/> <field name="slug" type="string" column="slug" length="255"/> <lifecycle-callbacks/> </entity> </doctrine-mapping> PDF brought to you by generated on June 26, 2014 Chapter 28: Comment gnrer des Entits partir d'une base de donnes existante | 95 Listing 28-4 Listing 28-5 Si vous avez des relations oneToMany entre vos entits, vous devrez diter les fichiers xml ou yml gnrs pour ajouter une section sur les entits spcifiques afin de dfinir les attributs inversedBy et mappedBy de la relation oneToMany. Une fois que les fichiers de mta-donnes sont gnrs, vous pouvez demander Doctrine d'importer le schma et de construire les classes entit qui lui sont lies en excutant les deux commandes suivantes. 1 2 $ php app/console doctrine:mapping:import AcmeBlogBundle annotation $ php app/console doctrine:generate:entities AcmeBlogBundle La premire commande gnre les classes entit avec des annotations de correspondance, mais vous pouvez bien sr changer l'argument annotation pour tre xml ou yml. La classe entit nouvellement cre ressemble ce qui suit : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php // src/Acme/BlogBundle/Entity/BlogComment.php namespace Acme\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Acme\BlogBundle\Entity\BlogComment * * @ORM\Table(name="blog_comment") * @ORM\Entity */ class BlogComment { /** * @var bigint $id * * @ORM\Column(name="id", type="bigint", nullable=false) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @var string $author * * @ORM\Column(name="author", type="string", length=100, nullable=false) */ private $author; /** * @var text $content * * @ORM\Column(name="content", type="text", nullable=false) */ private $content; /** * @var datetime $createdAt * * @ORM\Column(name="created_at", type="datetime", nullable=false) */ PDF brought to you by generated on June 26, 2014 Chapter 28: Comment gnrer des Entits partir d'une base de donnes existante | 96 44 45 46 47 48 49 50 51 52 53 private $createdAt; /** * @var BlogPost * * @ORM\ManyToOne(targetEntity="BlogPost") * @ORM\JoinColumn(name="post_id", referencedColumnName="id") */ private $post; } Comme vous pouvez le voir, Doctrine convertit tous les champs de la table en proprits prives et annotes de la classe. La chose la plus impressionnante est qu'il identifie aussi la relation avec la classe entit BlogPost bas sur la contrainte de cl trangre. De ce fait, vous pouvez trouver une proprit prive $post correspondant une entit BlogPost dans la classe entit BlogComment. La dernire commande a gnr tous les getters et setters pour les proprits de vos deux classes entit BlogPost et BlogComment. Les entits gnres sont maintenant prtes tre utilises. Amusez- vous! PDF brought to you by generated on June 26, 2014 Chapter 28: Comment gnrer des Entits partir d'une base de donnes existante | 97 Listing 29-1 Chapter 29 Comment travailler avec plusieurs gestionnaires d'entits et connexions Vous pouvez utiliser plusieurs gestionnaires d'entits ou plusieurs connexions dans une application Symfony2. Cela est ncessaire si vous utilisez diffrentes bases de donnes ou mme des vendors avec des ensembles d'entits entirement diffrents. En d'autres termes, un gestionnaire d'entits qui se connecte une base de donnes va grer quelques entits alors qu'un autre gestionnaire d'entits qui se connecte une autre base de donnes pourrait grer le reste. Utiliser plusieurs gestionnaires d'entits est assez facile, mais plus avanc et gnralement non requis. Soyez sr que vous ncessitiez plusieurs gestionnaires d'entits avant d'ajouter cette couche de complexit. Le code de configuration suivant montre comment vous pouvez configurer deux gestionnaires d'entits : doctrine: dbal: default_connection: default connections: default: driver: %database_driver% host: %database_host% port: %database_port% dbname: %database_name% user: %database_user% password: %database_password% charset: UTF8 customer: driver: %database_driver2% host: %database_host2% port: %database_port2% dbname: %database_name2% user: %database_user2% password: %database_password2% charset: UTF8 PDF brought to you by generated on June 26, 2014 Chapter 29: Comment travailler avec plusieurs gestionnaires d'entits et connexions | 98 Listing 29-2 Listing 29-3 Listing 29-4 orm: default_entity_manager: default entity_managers: default: connection: default mappings: AcmeDemoBundle: ~ AcmeStoreBundle: ~ customer: connection: customer mappings: AcmeCustomerBundle: ~ Dans ce cas, vous avez dfini deux gestionnaires d'entits et les avez appel default et customer. Le gestionnaire d'entits default gre les entits des bundles AcmeDemoBundle et AcmeStoreBundle, alors que le gestionnaire d'entits customer gre les entits du bundle AcmeCustomerBundle.Vous avez galement dfini deux connexions, une pour chaque gestionnaire d'entit. Lorsque vous travaillez avec plusieurs connexions ou plusieurs gestionnaires d'entits, vous devriez tre explicite quant la configuration que vous voulez. Si vous omettez le nom de la connexion ou du gestionnaire d'entit quand vous mettez jour votre schema, le gestionnaire par dfaut (c-a-d default) sera utilis. Crer votre base de donnes quand vous utilisez plusieurs connexion : 1 2 3 4 5 # N'utilise que la connexion default $ php app/console doctrine:database:create # N'utilise que la connexion customer $ php app/console doctrine:database:create --connection=customer Mettre jour votre schma quand vous utilisez plusieurs gestionnaires d'entit : 1 2 3 4 5 # Utilise le gestionnaire default $ php app/console doctrine:schema:update --force # Utilise le gestionnaire customer $ php app/console doctrine:schema:update --force --em=customer Si vous omettez le nom du gestionnaire d'entit quand vous le demandez, le gestionnaire d'entits par dfaut (c'est--dire default) est retourn: 1 2 3 4 5 6 7 8 9 10 11 class UserController extends Controller { public function indexAction() { // les deux retournent le gestionnaire d'entits default $em = $this->get('doctrine')->getManager(); $em = $this->get('doctrine')->getManager('default'); $customerEm = $this->get('doctrine')->getManager('customer'); } } PDF brought to you by generated on June 26, 2014 Chapter 29: Comment travailler avec plusieurs gestionnaires d'entits et connexions | 99 Listing 29-5 Vous pouvez maintenant utiliser Doctrine comme vous le faisiez avant - en utilisant le gestionnaire d'entits default pour persister et aller chercher les entits qu'il gre et le gestionnaire d'entits customer pour persister et aller chercher ses entits. La mme chose s'applique aux appels de repository: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class UserController extends Controller { public function indexAction() { // Retourne un repository gr par le gestionnaire default $products = $this->get('doctrine') ->getRepository('AcmeStoreBundle:Product') ->findAll(); // Manire explicite de traiter avec le gestionnaire default $products = $this->get('doctrine') ->getRepository('AcmeStoreBundle:Product', 'default') ->findAll(); // Retourne un repository gr par le gestionnaire customer $customers = $this->get('doctrine') ->getRepository('AcmeCustomerBundle:Customer', 'customer') ->findAll(); } } PDF brought to you by generated on June 26, 2014 Chapter 29: Comment travailler avec plusieurs gestionnaires d'entits et connexions | 100 Listing 30-1 Chapter 30 Comment dfinir des fonctions DQL personnalises Doctrine vous permet de spcifier des fonctions DQL personnalises. Pour plus d'informations ce sujet, lisez l'article du cookbook de Doctrine Fonctions DQL dfinies par l'utilisateur 1 . Dans Symfony, vous pouvez dfinir vos fonctions DQL personnalises comme suit : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # app/config/config.yml doctrine: orm: # ... entity_managers: default: # ... dql: string_functions: test_string: Acme\HelloBundle\DQL\StringFunction second_string: Acme\HelloBundle\DQL\SecondStringFunction numeric_functions: test_numeric: Acme\HelloBundle\DQL\NumericFunction datetime_functions: test_datetime: Acme\HelloBundle\DQL\DatetimeFunction 1. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html PDF brought to you by generated on June 26, 2014 Chapter 30: Comment dfinir des fonctions DQL personnalises | 101 Chapter 31 Comment dfinir des Relations avec des Classes Abstraites et des Interfaces New in version 2.1: Le ResolveTargetEntityListener est une nouveaut de Doctrine 2.2, qui a t packag pour la premire fois avec Symfony 2.1. L'un des buts des bundles est de crer des ensembles distincts de fonctionnalits qui n'ont pas beaucoup (ou pas du tout) de dpendances, vous permettant d'utiliser cette fonctionnalit dans d'autres applications sans inclure d'lments superflus. Doctrine 2.2 inclut un nouvel utilitaire appel le ResolveTargetEntityListener, qui fonctionne en interceptant certains appels dans Doctrine et en r-crivant des paramtres targetEntity dans vos mta- donnes de correspondance durant l'excution. Cela signifie que depuis votre bundle, vous tes capable d'utiliser une interface ou une classe abstraite dans vos correspondances et que vous pouvez vous attendre une correspondance correcte avec une entit concrte au moment de l'excution. Cette fonctionnalit vous permet de dfinir des relations entre diffrentes entits sans en faire des dpendances crites en dur . Contexte/dcor Supposons que vous ayez un InvoiceBundle qui fournit une fonctionnalit de facturation ( invoicing en anglais) et un CustomerBundle qui contient les outils de gestion de client. Vous souhaitez garder ces deux entits spares, car elles peuvent tre utilises dans d'autres systmes l'une sans l'autre ; mais pour votre application, vous voulez les utiliser ensemble. Dans ce cas, vous avez une entit Invoice ayant une relation avec un objet qui n'existe pas, une InvoiceSubjectInterface. Le but est de rcuprer le ResolveTargetEntityListener pour remplacer toute mention de l'interface par un objet rel qui implmente cette interface. PDF brought to you by generated on June 26, 2014 Chapter 31: Comment dfinir des Relations avec des Classes Abstraites et des Interfaces | 102 Listing 31-1 Listing 31-2 Listing 31-3 Mise en place Utilisons les entits basiques suivantes (qui sont incompltes pour plus de brivet) pour expliquer comment mettre en place et utiliser le RTEL. Une entit Customer : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // src/Acme/AppBundle/Entity/Customer.php namespace Acme\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Acme\CustomerBundle\Entity\Customer as BaseCustomer; use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; /** * @ORM\Entity * @ORM\Table(name="customer") */ class Customer extends BaseCustomer implements InvoiceSubjectInterface { // Dans notre exemple, toutes les mthodes dfinies dans // l' InvoiceSubjectInterface sont dj implmentes dans le BaseCustomer } Une entit Invoice : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // src/Acme/InvoiceBundle/Entity/Invoice.php namespace Acme\InvoiceBundle\Entity; use Doctrine\ORM\Mapping AS ORM; use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; /** * Reprsente une Invoice . * * @ORM\Entity * @ORM\Table(name="invoice") */ class Invoice { /** * @ORM\ManyToOne(targetEntity="Acme\InvoiceBundle\Model\InvoiceSubjectInterface") * @var InvoiceSubjectInterface */ protected $subject; } Une InvoiceSubjectInterface : 1 2 3 4 5 6 7 // src/Acme/InvoiceBundle/Model/InvoiceSubjectInterface.php namespace Acme\InvoiceBundle\Model; /** * Une interface que le sujet de la facture devrait implmenter. * Dans la plupart des circonstances, il ne devrait y avoir qu'un unique objet PDF brought to you by generated on June 26, 2014 Chapter 31: Comment dfinir des Relations avec des Classes Abstraites et des Interfaces | 103 Listing 31-4 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * qui implmente cette interface puisque le ResolveTargetEntityListener peut * changer seulement la cible d'un objet unique. */ interface InvoiceSubjectInterface { // Liste toutes les mthodes additionnelles dont votre // InvoiceBundle aura besoin pour accder au sujet afin // que vous soyez sr que vous avez accs ces mthodes. /** * @return string */ public function getName(); } Ensuite, vous devez configurer le listener , qui informe le DoctrineBundle de votre remplacement : 1 2 3 4 5 6 7 # app/config/config.yml doctrine: # .... orm: # .... resolve_target_entities: Acme\InvoiceBundle\Model\InvoiceSubjectInterface: Acme\AppBundle\Entity\Customer Rflexions finales Avec le ResolveTargetEntityListener, vous tes capable de dcoupler vos bundles, en les gardant utilisables par eux-mmes, mais en tant toujours capable de dfinir des relations entre diffrents objets. En utilisant cette mthode, vos bundles vont finir par tre plus faciles maintenir indpendamment. PDF brought to you by generated on June 26, 2014 Chapter 31: Comment dfinir des Relations avec des Classes Abstraites et des Interfaces | 104 Listing 32-1 Chapter 32 Comment implmenter un simple formulaire de cration de compte Certains formulaires ont des champs en plus, dont les valeurs n'ont pas besoin d'tre stockes en base de donnes. Par exemple, vous pourriez vouloir crer un formulaire de cration de compte avec des champs en plus (comme par exemple une checkbox Accepter les conditions ) et imbriquer le formulaire qui contient les informations relatives au compte. Un modle simple : User Vous avez une entit User simple associe la base de donnes: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // src/Acme/AccountBundle/Entity/User.php namespace Acme\AccountBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** * @ORM\Entity * @UniqueEntity(fields="email", message="Email already taken") */ class User { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", length=255) PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 105 Listing 32-2 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 * @Assert\NotBlank() * @Assert\Email() */ protected $email; /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank() */ protected $plainPassword; public function getId() { return $this->id; } public function getEmail() { return $this->email; } public function setEmail($email) { $this->email = $email; } public function getPlainPassword() { return $this->plainPassword; } public function setPlainPassword($password) { $this->plainPassword = $password; } } Cette entit User contient trois champs et deux d'entre eux (email et plainPassword) doivent tre affichs dans le formulaire. La proprit email doit tre unique dans la base de donnes, ce qui est ralis par l'ajout d'une validation au dbut de la classe. Si vous voulez intgrer cet utilisateur dans le systme de scurit, vous devez implmenter la UserInterface du composant Security. Crer un formulaire pour le modle Ensuite, crez le formulaire pour le modle User: 1 2 3 4 5 // src/Acme/AccountBundle/Form/Type/UserType.php namespace Acme\AccountBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 106 Listing 32-3 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 use Symfony\Component\OptionsResolver\OptionsResolverInterface; class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('email', 'email'); $builder->add('plainPassword', 'repeated', array( 'first_name' => 'password', 'second_name' => 'confirm', 'type' => 'password', )); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\AccountBundle\Entity\User' )); } public function getName() { return 'user'; } } Il n'y a que deux champs : email et plainPassword (dupliqu pour confirmer le mot de passe saisi). L'option data_class spcifie au formulaire le nom de la classe associe (c-a-d l'entit User). Pour en savoir plus sur le composant Formulaire, lisez Formulaires. Imbriquer le formulaire User dans le formulaire de cration de compte Le formulaire que vous utiliserez pour la page de cration de compte n'est pas le mme que le formulaire qui est utilis pour modifier simplement l'objet User (c-a-d UserType). Le formulaire de cration de compte contiendra quelques champs supplmentaires, comme Accepter les conditions , dont les valeurs ne seront pas stockes en base de donnes. Commencez par crer une simple classe qui reprsente la cration de compte ( registration en anglais): 1 2 3 4 5 6 7 8 9 10 11 // src/Acme/AccountBundle/Form/Model/Registration.php namespace Acme\AccountBundle\Form\Model; use Symfony\Component\Validator\Constraints as Assert; use Acme\AccountBundle\Entity\User; class Registration { /** * @Assert\Type(type="Acme\AccountBundle\Entity\User") PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 107 Listing 32-4 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 */ protected $user; /** * @Assert\NotBlank() * @Assert\True() */ protected $termsAccepted; public function setUser(User $user) { $this->user = $user; } public function getUser() { return $this->user; } public function getTermsAccepted() { return $this->termsAccepted; } public function setTermsAccepted($termsAccepted) { $this->termsAccepted = (Boolean) $termsAccepted; } } Ensuite, crez le formulaire pour ce modle Registration: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // src/Acme/AccountBundle/Form/Type/RegistrationType.php namespace Acme\AccountBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class RegistrationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('user', new UserType()); $builder->add('terms', 'checkbox', array('property_path' => 'termsAccepted')); } public function getName() { return 'registration'; } } Vous n'avez pas besoin d'utiliser de mthode spciale pour imbriquer le formulaire UserType. Un formulaire est aussi un champ, donc vous pouvez l'ajouter comme n'importe quel champ, avec l'objectif que la proprit Registration.user contienne une instance de la classe User. PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 108 Listing 32-5 Listing 32-6 Listing 32-7 Grer la soumission du formulaire Ensuite, vous aurez besoin d'un contrleur pour prendre en charge le formulaire. Commencez par crer un simple contrleur pour afficher le formulaire de cration de compte: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // src/Acme/AccountBundle/Controller/AccountController.php namespace Acme\AccountBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; use Acme\AccountBundle\Form\Type\RegistrationType; use Acme\AccountBundle\Form\Model\Registration; class AccountController extends Controller { public function registerAction() { $form = $this->createForm(new RegistrationType(), new Registration()); return $this->render('AcmeAccountBundle:Account:register.html.twig', array('form' => $form->createView())); } } et son template : 1 2 3 4 5 6 {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #} <form action="{{ path('create')}}" method="post" {{ form_enctype(form) }}> {{ form_widget(form) }} <input type="submit" /> </form> Enfin, crez le contrleur qui prendra en charge la soumission du formulaire. Il se chargera de la validation, et enregistrera les donnes dans la base de donnes: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public function createAction() { $em = $this->getDoctrine()->getEntityManager(); $form = $this->createForm(new RegistrationType(), new Registration()); $form->handleRequest($this->getRequest()); if ($form->isValid()) { $registration = $form->getData(); $em->persist($registration->getUser()); $em->flush(); return $this->redirect(...); } return $this->render('AcmeAccountBundle:Account:register.html.twig', array('form' => $form->createView())); } PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 109 C'est tout ! Maintenant, votre formulaire valide et vous permet d'enregistrer un objet User dans la base de donnes. La checkbox supplmentaire terms du modle Registration est utilise durant la validation, mais n'est plus utilise ensuite lors de l'enregistrement de l'utilisateur en base de donnes. PDF brought to you by generated on June 26, 2014 Chapter 32: Comment implmenter un simple formulaire de cration de compte | 110 Listing 33-1 Listing 33-2 Chapter 33 Comment envoyer un Email Envoyer des emails est une tche classique pour n'importe quelle application web et une qui possde des complications et piges potentiels. Plutt que de rinventer la roue, une solution pour envoyer des emails est d'utiliser le SwiftmailerBundle, qui tire partie de la puissance de la bibliothque Swiftmailer 1 . N'oubliez pas d'activer le bundle dans votre Kernel avant de l'utiliser: 1 2 3 4 5 6 7 8 9 public function registerBundles() { $bundles = array( ..., new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), ); // ... } Configuration Avant d'utiliser Swiftmailer, soyez sr d'inclure sa configuration. Le seul paramtre obligatoire de la configuration est transport : 1 2 3 4 5 6 7 8 # app/config/config.yml swiftmailer: transport: smtp encryption: ssl auth_mode: login host: smtp.gmail.com username: your_username password: your_password 1. http://swiftmailer.org/ PDF brought to you by generated on June 26, 2014 Chapter 33: Comment envoyer un Email | 111 Listing 33-3 La plus grande partie de la configuration de Swiftmailer traite de comment les messages eux-mmes devraient tre envoys. Les attributs de configuration suivants sont disponibles : transport (smtp, mail, sendmail, ou gmail) username password host port encryption (tls, or ssl) auth_mode (plain, login, or cram-md5) spool type (comment mettre les messages dans une queue, seulement file est support pour le moment) path (o stocker les messages) delivery_address (une adresse email o envoyer TOUS les emails) disable_delivery (dfinir true pour dsactiver entirement l'envoi des emails) Envoyer des Emails La bibliothque Swiftmailer fonctionne grce la cration, la configuration et enfin l'envoi d'objets Swift_Message. Le mailer est responsable de l'envoi rl du message et est accessible via le service mailer. Globalement, envoyer un email est assez simple: 1 2 3 4 5 6 7 8 9 10 11 12 public function indexAction($name) { $message = \Swift_Message::newInstance() ->setSubject('Hello Email') ->setFrom('[email protected]') ->setTo('[email protected]') ->setBody($this->renderView('HelloBundle:Hello:email.txt.twig', array('name' => $name))) ; $this->get('mailer')->send($message); return $this->render(...); } Pour garder les choses dcouples, le corps de l'email a t stock dans un template et est rendu grce la mthode renderView(). L'objet $message supporte beaucoup plus d'options, comme inclure des pices jointes, ajouter du contenu HTML, et bien plus encore. Heureusement, Swiftmailer couvre le thme de la Cration de Messages 2 de manire trs dtaille dans sa documentation. Plusieurs articles du cookbook lis l'envoi d'emails dans Symfony2 sont disponibles : Comment utiliser Gmail pour envoyer des Emails Comment travailler avec les Emails pendant le Dveloppement Comment utiliser le Spool d'Email 2. http://swiftmailer.org/docs/messages.html PDF brought to you by generated on June 26, 2014 Chapter 33: Comment envoyer un Email | 112 Listing 34-1 Chapter 34 Comment utiliser Gmail pour envoyer des Emails Durant le dveloppement, au lieu d'utiliser un serveur SMTP ordinaire pour envoyer des emails, vous pourriez trouver qu'utiliser Gmail soit plus facile ou plus pratique. Le bundle Swiftmailer rend cela vraiment facile. Au lieu d'utiliser votre compte Gmail courant, il est bien sr recommand que vous criez un compte spcial. Dans le fichier de configuration de dveloppement, changez le paramtre transport pour qu'il contienne la valeur gmail et dfinissez les paramtres username et password avec les informations d'accs de Google : 1 2 3 4 5 # app/config/config_dev.yml swiftmailer: transport: gmail username: your_gmail_username password: your_gmail_password Vous avez termin ! Le transport gmail est simplement un raccourci qui utilise le transport smtp et dfinit encryption, auth_mode et host afin que cela fonctionne avec Gmail. PDF brought to you by generated on June 26, 2014 Chapter 34: Comment utiliser Gmail pour envoyer des Emails | 113 Listing 35-1 Listing 35-2 Chapter 35 Comment travailler avec les Emails pendant le Dveloppement Lorsque vous dveloppez une application qui envoie des emails, vous voudrez souvent que cette dernire n'en envoie pas au destinataire spcifi pendant le dveloppement. Si vous utilisez le SwiftmailerBundle avec Symfony2, vous pouvez facilement raliser cela travers les paramtres de configuration sans avoir faire de quelconques changements dans votre code. Il y a deux choix principaux possibles lorsqu'on parle de gestion d'emails durant le dveloppement : (a) dsactiver compltement l'envoi d'emails ou (b) envoyer tous les emails une adresse spcifique. Dsactiver l'Envoi Vous pouvez dsactiver l'envoi d'emails en dfinissant l'option disable_delivery true. C'est ce qui est fait par dfaut dans l'environnement test dans la distribution Standard. Si vous faites cela dans la configuration spcifique test, alors les emails ne seront pas envoys lorsque vous excuterez les tests, mais continueront d'tre envoys dans les environnements prod et dev : 1 2 3 # app/config/config_test.yml swiftmailer: disable_delivery: true Si vous voulez aussi dsactiver l'envoi dans l'environnement dev, ajoutez tout simplement la mme configuration dans le fichier config_dev.yml. Envoyer une Adresse Spcifique Vous pouvez aussi choisir d'avoir tous les emails envoys une adresse spcifique la place de l'adresse rellement spcifie lors de l'envoi du message. Ceci peut tre accompli via l'option delivery_address : PDF brought to you by generated on June 26, 2014 Chapter 35: Comment travailler avec les Emails pendant le Dveloppement | 114 Listing 35-3 Listing 35-4 1 2 3 # app/config/config_dev.yml swiftmailer: delivery_address: [email protected] Maintenant, supposons que vous envoyiez un email [email protected]. 1 2 3 4 5 6 7 8 9 10 11 12 public function indexAction($name) { $message = \Swift_Message::newInstance() ->setSubject('Hello Email') ->setFrom('[email protected]') ->setTo('[email protected]') ->setBody($this->renderView('HelloBundle:Hello:email.txt.twig', array('name' => $name))) ; $this->get('mailer')->send($message); return $this->render(...); } Dans l'environnement dev, l'email sera envoy [email protected]. Swiftmailer ajoutera un en-tte supplmentaire l'email, X-Swift-To, contenant l'adresse remplace, afin que vous puissiez toujours voir qui il aurait t envoy. En plus des adresses to, cela va aussi stopper les emails envoys n'importe quelle adresse des champs CC et BCC. Swiftmailer ajoutera des en-ttes additionnels l'email contenant les adresses surcharges. Ces en-ttes sont respectivement X-Swift-Cc et X-Swift-Bcc pour les adresses de CC et BCC. Voir les informations depuis la Barre d'Outils de Dbuggage Web Vous pouvez voir tout email envoy durant une unique rponse lorsque vous tes dans l'environnement dev via la Barre d'Outils de Dbuggage. L'icne d'email dans la barre d'outils montrera combien d'emails ont t envoys. Si vous cliquez dessus, un rapport s'ouvrira montrant les dtails des emails envoys. Si vous envoyez un email et puis redirigez immdiatement vers une autre page, la barre d'outils de dbuggage n'affichera pas d'icne d'email ni de rapport sur la page d'aprs. A la place, vous pouvez dfinir l'option intercept_redirects comme tant true dans le fichier config_dev.yml, ce qui va forcer les redirections s'arrter et vous permettre d'ouvrir le rapport avec les dtails des emails envoys. Sinon, vous pouvez ouvrir le profiler aprs la redirection et rechercher par l'URL soumise et utilise lors de la requte prcdente (par exemple : /contact/handle). La fonctionnalit de recherche du profiler vous permet de charger les informations du profiler pour toutes les requtes passes. 1 2 3 # app/config/config_dev.yml web_profiler: intercept_redirects: true PDF brought to you by generated on June 26, 2014 Chapter 35: Comment travailler avec les Emails pendant le Dveloppement | 115 Listing 36-1 Listing 36-2 Listing 36-3 Chapter 36 Comment utiliser le Spool d'Email Quand vous utilisez le SwiftmailerBundle pour envoyer un email depuis une application Symfony2, il va par dfaut l'envoyer immdiatement. Vous pourriez, cependant, vouloir viter d'avoir un hit de performance entre le Swiftmailer et le transport de l'email, qui pourrait avoir pour consquence que l'utilisateur ait attendre que la page suivante se charge pendant que l'email est envoy. Cela peut tre vit en choisissant d'envoyer les emails en mode spool au lieu de les envoyer directement. Cela signifie que Swiftmailer n'essaie pas d'envoyer l'email mais la place sauvegarde le message quelque part comme par exemple dans un fichier. Un autre processus peut alors lire depuis le spool et prendre en charge l'envoi des emails contenus dans ce dernier. Pour le moment, seul le spooling via un fichier est support par Swiftmailer. Afin d'utiliser le spool , utilisez la configuration suivante : 1 2 3 4 5 6 # app/config/config.yml swiftmailer: # ... spool: type: file path: /path/to/spool Si vous voulez stocker le spool quelque part dans votre rpertoire de projet, rappelez-vous que vous pouvez utiliser le paramtre %kernel.root_dir% pour rfrencer la racine du projet : 1 path: "%kernel.root_dir%/spool" Maintenant, quand votre application envoie un email, il ne sera en fait pas envoy mais ajout au spool la place. L'envoi des messages depuis le spool est effectu sparment. Il y a une commande de la console pour envoyer les messages qui sont dans le spool : 1 $ php app/console swiftmailer:spool:send --env=prod Cette commande possde une option pour limiter le nombre de messages devant tre envoys : PDF brought to you by generated on June 26, 2014 Chapter 36: Comment utiliser le Spool d'Email | 116 Listing 36-4 Listing 36-5 1 $ php app/console swiftmailer:spool:send --message-limit=10 --env=prod Vous pouvez aussi dfinir la limite de temps en secondes : 1 $ php app/console swiftmailer:spool:send --time-limit=10 --env=prod Bien sr, vous n'avez pas besoin de lancer cette tche manuellement. A la place, la commande de la console devrait tre lance par une tche cron ou une tche planifie et excute intervalle rgulier. PDF brought to you by generated on June 26, 2014 Chapter 36: Comment utiliser le Spool d'Email | 117 Listing 37-1 Chapter 37 Comment mettre en place des filtres avant et aprs un processus donn Il est trs commun, dans le dveloppement d'application web, d'avoir besoin qu'un bout de logique soit excut juste avant ou juste aprs vos actions de contrleur agissant comme des filtres ou des hooks . Dans Symfony1, cela tait effectu avec les mthodes preExecute et postExecute ; la plupart des principaux frameworks ont des mthodes similaires mais il n'y a rien de semblable dans Symfony2. La bonne nouvelle est qu'il y a une bien meilleure manire d'interfrer le processus Requte -> Rponse en utilisant le composant EventDispatcher. Exemple de validation de jeton Imaginez que vous devez dvelopper une API dans laquelle certains contrleurs sont publics mais d'autres ont un accs restreint qui est rserv un ou plusieurs clients. Pour ces fonctionnalits prives, vous pourriez fournir un jeton vos clients afin qu'ils s'identifient eux-mmes. Donc, avant d'excuter votre action de contrleur, vous devez vrifier si l'action est restreinte ou pas. Si elle est restreinte, vous devez valider le jeton fourni. Veuillez noter que, pour garder cet article suffisament simple, les jetons vont tre dfinis dans la configuration et aucune mise en place de base de donnes ni de fournisseur d'authentification via le composant de Scurit ne vont tre utiliss. Crer un filtre avant un vnement kernel.controller Premirement, stockez un jeton basique en configuration en utilisant le fichier config.yml et la cl parameters : PDF brought to you by generated on June 26, 2014 Chapter 37: Comment mettre en place des filtres avant et aprs un processus donn | 118 Listing 37-2 Listing 37-3 Listing 37-4 1 2 3 4 5 # app/config/config.yml parameters: tokens: client1: pass1 client2: pass2 Les contrleurs de Tag devant tre vrifis Un listener de kernel.controller est notifi chaque requte juste avant que le contrleur ne soit excut. D'abord, vous avez besoin de savoir si le contrleur qui correspond la requte a besoin d'une validation de token. Une faon propre et facile est de crer une interface vide et de faire en sorte que les contrleurs l'implmentent: 1 2 3 4 5 6 namespace Acme\DemoBundle\Controller; interface TokenAuthenticatedController { // ... } Un contrleur qui implmente cette interface ressemble simplement cela: 1 2 3 4 5 6 7 8 9 10 11 12 13 namespace Acme\DemoBundle\Controller; use Acme\DemoBundle\Controller\TokenAuthenticatedController; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class FooController extends Controller implements TokenAuthenticatedController { // une action qui a besoin d'une authentification public function barAction() { // ... } } Crer un Listener d'vnement Ensuite, vous allez avoir besoin de crer un listener d'vnement, qui va contenir la logique que vous souhaitez excuter avant vos contrleurs. Si vous n'tes pas familier avec les listeners d'vnement, vous pouvez en apprendre plus sur eux en lisant Comment crer un listener ( couteur en franais) d'vnement: 1 2 3 4 5 6 7 8 9 // src/Acme/DemoBundle/EventListener/TokenListener.php namespace Acme\DemoBundle\EventListener; use Acme\DemoBundle\Controller\TokenAuthenticatedController; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; class TokenListener { PDF brought to you by generated on June 26, 2014 Chapter 37: Comment mettre en place des filtres avant et aprs un processus donn | 119 Listing 37-5 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private $tokens; public function __construct($tokens) { $this->tokens = $tokens; } public function onKernelController(FilterControllerEvent $event) { $controller = $event->getController(); /* * $controller peut tre une classe ou une closure. Ce n'est pas * courant dans Symfony2 mais a peut arriver. * Si c'est une classe, elle est au format array */ if (!is_array($controller)) { return; } if ($controller[0] instanceof TokenAuthenticatedController) { $token = $event->getRequest()->query->get('token'); if (!in_array($token, $this->tokens)) { throw new AccessDeniedHttpException('Cette action ncessite un jeton valide!'); } } } } Dclarez le Listener Finalement, dclarez votre listener comme un service et taggez-le en tant que listener d'vnement. En coutant le kernel.controller, vous dites Symfony que vous voulez que votre listener soit appel juste avant qu'un contrleur quelconque soit excut : # app/config/config.yml (or inside your services.yml) services: demo.tokens.action_listener: class: Acme\DemoBundle\EventListener\TokenListener arguments: [ %tokens% ] tags: - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } Avec cette configuration, votre mthode onKernelController de TokenListener sera excute chaque requte. Si le contrleur qui doit tre excut implmente TokenAuthenticatedController, l'authentification par jeton est applique. Cela vous permet d'avoir le filtre avant que vous vouliez sur tous les contrleurs. Crer un filtre aprs un vnement kernel.response En plus d'avoir un hook qui est excut avant notre contrleur, vous pouvez galement ajouter un hook qui sera excut aprs votre contrleur. Pour cet exemple, imaginez que vous voulez ajouter un hash PDF brought to you by generated on June 26, 2014 Chapter 37: Comment mettre en place des filtres avant et aprs un processus donn | 120 Listing 37-6 Listing 37-7 Listing 37-8 sha1 (avec un salage - ou salt - qui utilise le jeton) chaque rponse qui a pass notre authentification par jeton. Un autre vnement du noyau de Symfony, appel kernel.response, est notifi chaque requte, mais aprs que le contrleur a retourn un objet Response. Crer un couteur aprs est aussi simple que de crer une classe couteur et de l'enregistrer en tant que service sur cet vnement. Par exemple, prenez le TokenListener de l'exemple prcdent et enregistrez d'abord le jeton d'authentification dans les attributs de la requte. Cela indiquera que cette requte a subi une demande d'authentification par jeton: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function onKernelController(FilterControllerEvent $event) { // ... if ($controller[0] instanceof TokenAuthenticatedController) { $token = $event->getRequest()->query->get('token'); if (!in_array($token, $this->tokens)) { throw new AccessDeniedHttpException('Cette action ncessite un jeton valide!'); } // marque la requte aprs authentification $event->getRequest()->attributes->set('auth_token', $token); } } Maintenant, ajoutons une autre mthode cette classe, onKernelResponse, qui vrifiera que l'objet requte est marqu et, si c'est le cas, qui dfinira un en-tte personnalis pour la rponse: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // ajoutez la nouvelle instruction use en haut de votre fichier use Symfony\Component\HttpKernel\Event\FilterResponseEvent; public function onKernelResponse(FilterResponseEvent $event) { // Vrifie que onKernelController est une requte authentifie if (!$token = $event->getRequest()->attributes->get('auth_token')) { return; } $response = $event->getResponse(); // cre un hash et le dfinit comme en-tte de la rponse $hash = sha1($response->getContent().$token); $response->headers->set('X-CONTENT-HASH', $hash); } Enfin, un second tag est ncessaire dans la dfinition de service pour notifier Symfony que l'vnement onKernelResponse doit tre notifi pour l'vnement kernel.response : # app/config/config.yml (or inside your services.yml) services: demo.tokens.action_listener: class: Acme\DemoBundle\EventListener\TokenListener arguments: [ %tokens% ] tags: - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } PDF brought to you by generated on June 26, 2014 Chapter 37: Comment mettre en place des filtres avant et aprs un processus donn | 121 C'est tout ! Le TokenListener est maintenant notifi avant chaque contrleur qui est excut (onKernelController) et aprs chaque rponse retourne par un contrleur (onKernelResponse). En faisant des contrleurs spcifiques qui implmentent l'interface TokenAuthenticatedController, nos couteurs savent quels contrleurs traiter. Et en stockant une valeur dans les attributs de la requte, la mthode onKernelResponse sait quand ajouter notre nouvel en-tte. Amusez-vous ! PDF brought to you by generated on June 26, 2014 Chapter 37: Comment mettre en place des filtres avant et aprs un processus donn | 122 Listing 38-1 Listing 38-2 Chapter 38 Comment tendre une Classe sans utiliser l'Hritage Pour permettre plusieurs classes d'ajouter des mthodes une autre, vous pouvez dfinir la mthode magique __call() dans la classe que vous voulez tendre comme cela : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Foo { // ... public function __call($method, $arguments) { // cre un vnement nomm 'foo.method_is_not_found' $event = new HandleUndefinedMethodEvent($this, $method, $arguments); $this->dispatcher->dispatch('foo.method_is_not_found', $event); // aucun listener n'a t capable d'analyser l'vnement ? La mthode // n'existe pas if (!$event->isProcessed()) { throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method)); } // retourne la valeur retourne par le listener return $event->getReturnValue(); } } Cela utilise un vnement HandleUndefinedMethodEvent spcial qui devrait aussi tre cr. C'est en fait une classe gnrique qui pourrait tre rutilise chaque fois que vous avez besoin de ce pattern d'extension de classe : 1 2 3 use Symfony\Component\EventDispatcher\Event; class HandleUndefinedMethodEvent extends Event PDF brought to you by generated on June 26, 2014 Chapter 38: Comment tendre une Classe sans utiliser l'Hritage | 123 Listing 38-3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 { protected $subject; protected $method; protected $arguments; protected $returnValue; protected $isProcessed = false; public function __construct($subject, $method, $arguments) { $this->subject = $subject; $this->method = $method; $this->arguments = $arguments; } public function getSubject() { return $this->subject; } public function getMethod() { return $this->method; } public function getArguments() { return $this->arguments; } /** * Dfinit la valeur retourner et stoppe les autres listeners allant * tre notifis */ public function setReturnValue($val) { $this->returnValue = $val; $this->isProcessed = true; $this->stopPropagation(); } public function getReturnValue($val) { return $this->returnValue; } public function isProcessed() { return $this->isProcessed; } } Ensuite, crez une classe qui va couter l'vnement foo.method_is_not_found et ajoutez la mthode bar() : 1 2 3 4 class Bar { public function onFooMethodIsNotFound(HandleUndefinedMethodEvent $event) { PDF brought to you by generated on June 26, 2014 Chapter 38: Comment tendre une Classe sans utiliser l'Hritage | 124 Listing 38-4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // nous voulons rpondre seulement aux appels de la mthode 'bar' if ('bar' != $event->getMethod()) { // autorise un autre listener prendre en charge cette mthode // inconnue return; } // le sujet de l'objet (l'instance foo) $foo = $event->getSubject(); // les arguments de la mthode bar $arguments = $event->getArguments(); // faites quelque chose // ... // dfinit la valeur retourne $event->setReturnValue($someValue); } } Finalement, ajoutez la nouvelle mthode bar la classe Foo en dclarant une instance de Bar avec l'vnement foo.method_is_not_found : 1 2 $bar = new Bar(); $dispatcher->addListener('foo.method_is_not_found', array($bar, 'onFooMethodIsNotFound')); PDF brought to you by generated on June 26, 2014 Chapter 38: Comment tendre une Classe sans utiliser l'Hritage | 125 Listing 39-1 Chapter 39 Comment personnaliser le Comportement d'une Mthode sans utiliser l'Hritage Faire quelque chose avant ou aprs l'Appel d'une Mthode Si vous souhaitez faire quelque chose juste avant, ou juste aprs qu'une mthode a t appele, vous pouvez dispatcher un vnement respectivement au dbut ou la fin d'une mthode: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Foo { // ... public function send($foo, $bar) { // faites quelque chose avant le dbut de la mthode $event = new FilterBeforeSendEvent($foo, $bar); $this->dispatcher->dispatch('foo.pre_send', $event); // rcuprez $foo et $bar depuis l'vnement, ils pourraient // avoir t modifis $foo = $event->getFoo(); $bar = $event->getBar(); // l'implmentation relle est ici $ret = ...; // faites quelque chose aprs la fin de la mthode $event = new FilterSendReturnValue($ret); $this->dispatcher->dispatch('foo.post_send', $event); return $event->getReturnValue(); } } PDF brought to you by generated on June 26, 2014 Chapter 39: Comment personnaliser le Comportement d'une Mthode sans utiliser l'Hritage | 126 Listing 39-2 Dans cet exemple, deux vnements sont lancs : foo.pre_send, avant que la mthode soit excute, et foo.post_send aprs que la mthode est excute. Chacun utilise une classe Event personnalise pour communiquer des informations aux listeners des deux vnements. Ces classes d'vnements devraient tre cres par vous-mme et devraient permettre, dans cet exemple, aux variables $foo, $bar et $ret d'tre rcupres et dfinies par les listeners. Par exemple, supposons que FilterSendReturnValue possde une mthode setReturnValue, un listener pourrait alors ressembler ceci : 1 2 3 4 5 6 7 public function onFooPostSend(FilterSendReturnValue $event) { $ret = $event->getReturnValue(); // modifie la valeur originale de ``$ret`` $event->setReturnValue($ret); } PDF brought to you by generated on June 26, 2014 Chapter 39: Comment personnaliser le Comportement d'une Mthode sans utiliser l'Hritage | 127 Listing 40-1 Listing 40-2 Listing 40-3 Chapter 40 Comment personnaliser le rendu de formulaire Symfony vous propose un large choix de mthodes pour personnaliser la manire dont un formulaire est affich. Dans ce guide, vous apprendrez comment personnaliser autant que possible chaque partie de votre formulaire sans effort, et ceci que vous utilisiez Twig ou PHP comme moteur de template. Bases de l'affichage de formulaire Rappelons que le libell, les ventuelles erreurs et le widget HTML d'un champ de formulaire peuvent tre facilement affichs en utilisant la fonction Twig form_row ou la mthode d'aide ( helper en anglais) PHP row : 1 {{ form_row(form.age) }} Vous pouvez galement afficher chacune de ces trois parties d'un champ individuellement : 1 2 3 4 5 <div> {{ form_label(form.age) }} {{ form_errors(form.age) }} {{ form_widget(form.age) }} </div> Dans les deux cas, le libell du formulaire, les erreurs et le widget HTML sont affichs en utilisant un ensemble de balises livr d'office avec Symfony. Par exemple, les deux templates ci-dessus afficheront : 1 2 3 4 5 6 7 <div> <label for="form_age">Age</label> <ul> <li>This field is required</li> </ul> <input type="number" id="form_age" name="form[age]" /> </div> Pour tester rapidement un formulaire, vous pouvez afficher l'ensemble du formulaire en une seule ligne : PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 128 Listing 40-4 Listing 40-5 Listing 40-6 Listing 40-7 Listing 40-8 1 {{ form_widget(form) }} La suite de ce document explique comment le rendu de chaque partie du formulaire peut tre modifi diffrents niveaux. Pour plus d'informations sur l'affichage de formulaires en gnral, lisez Afficher un Formulaire dans un Template. Que sont les thmes de formulaire ? Symfony utilise des fragments de formulaires - des parties de template qui affichent juste une partie du formulaire - pour afficher chaque partie du formulaire : libells de champs, erreurs, champs texte input, balises select, etc. Ces fragments sont dfinis comme blocs dans Twig et comme fichiers de templates avec PHP. Un thme n'est rien de plus qu'un ensemble de fragments que vous pouvez utiliser pour afficher un formulaire. En d'autres termes, si vous voulez personnaliser l'affichage d'une partie d'un formulaire, vous pouvez importer un thme qui contient une personnalisation des fragments de formulaire concerns. Symfony est fourni avec un thme par dfaut (form_div_layout.html.twig 1 pour Twig et FrameworkBundle:Form pour PHP) qui dfinit chaque fragment ncessaire l'affichage des diffrentes parties d'un formulaire. Dans la section suivante, vous apprendrez comment personnaliser un thme en surchargeant un ou l'ensemble de ses fragments. Par exemple, lorsque le widget d'un type de champ integer est affich, un champ input number est gnr. 1 {{ form_widget(form.age) }} affiche : 1 <input type="number" id="form_age" name="form[age]" required="required" value="33" /> En interne, Symfony utilise le fragment integer_widget pour afficher le champ. C'est parce que le type de champ est integer et que vous voulez afficher son widget (par opposition son libell ou ses erreurs). Par dfaut, avec Twig, le bloc integer_widget du template form_div_layout.html.twig 2 serait choisi. En PHP, cela serait plutt le fichier integer_widget.html.php situ dans le dossier FrameworkBundle/ Resources/views/Form. L'implmentation par dfaut du fragment integer_widget ressemble ceci : 1 2 3 4 5 {# form_div_layout.html.twig #} {% block integer_widget %} {% set type = type|default('number') %} {{ block('form_widget_simple') }} {% endblock integer_widget %} Comme vous pouvez le voir, ce fragment affiche un autre fragment (form_widget_simple) : 1. https://github.com/symfony/symfony/blob/2.1/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig 2. https://github.com/symfony/symfony/blob/2.1/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 129 1 2 3 4 5 {# form_div_layout.html.twig #} {% block form_widget_simple %} {% set type = type|default('text') %} <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/> {% endblock form_widget_simple %} L'ide est qu'un fragment dtermine le code HTML gnr pour chaque partie du formulaire. Pour personnaliser l'affichage d'un formulaire, vous devez juste identifier et surcharger le bon fragment. Un ensemble de personnalisations de fragments d'un formulaire est appel thme de formulaire. Lorsque vous affichez un formulaire, pouvez choisir quel(s) thme(s) de formulaire appliquer. Dans Twig, un thme est un unique fichier de template et les fragments sont des blocs dfinis dans ce fichier. En PHP, un thme est un rpertoire et les fragments sont des fichiers de template individuels dans ce rpertoire. Savoir quel bloc personnaliser Dans cet exemple, le nom du fragment personnalis est integer_widget parce que vous voulez surcharger le widget HTML pour tous les types de champ integer. Si vous voulez personnaliser les champs textarea , vous devrez personnaliser textarea_widget. Comme vous le voyez, un nom de fragment est une combinaison du type de champ et de la partie du formulaire qui doit tre affiche (ex widget, label, errors, row). En consquence, pour personnaliser la manire dont les erreurs sont affiches pour les champs input text uniquement, vous devrez personnaliser le fragment text_errors. Pourtant, bien souvent, vous voudrez personnaliser l'affichage des erreurs pour tous les champs. Vous pouvez faire cela en personnalisant le fragment form_errors. Cette mthode tire avantage de l'hritage de type de champs. Plus prcisment, puisque le type text tend le type field, le composant formulaire cherchera d'abord le fragment spcifique au type (par exemple : text_errors) avant de se rabattre sur le nom du fragment parent si le spcifique n'existe pas (par exemple : form_errors). Pour plus d'informations sur ce sujet, lisez Nommage de Fragment de Formulaire. Thmes de formulaire Pour apprcier la puissance des thmes de formulaire, supposons que vous vouliez encadrer chaque champ number par un div. La cl pour faire cela est de personnaliser le fragment integer_widget. Thmes de formulaire avec Twig Lorsque vous personnalisez un bloc de champ de formulaire Twig, vous avez deux choix possibles quant la localisation du bloc personnalis : Mthode Avantages Inconvnients Dans le mme template que le formulaire Rapide et facile Ne peut pas tre rutilis PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 130 Listing 40-9 Listing 40-10 Mthode Avantages Inconvnients Dans un template spar Peut tre rutilis par d'autres templates Ncessite la cration d'un template Ces deux mthodes ont les mmes effets mais ne sont pas aussi avantageuses l'une que l'autre suivant les situations. Mthode 1: Dans le mme template que le formulaire La manire la plus facile de personnaliser le bloc integer_widget est de le modifier directement dans le template qui affiche le formulaire. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {% extends '::base.html.twig' %} {% form_theme form _self %} {% block integer_widget %} <div class="integer_widget"> {% set type = type|default('number') %} {{ block('form_widget_simple') }} </div> {% endblock %} {% block content %} {# affiche le formulaire #} {{ form_row(form.age) }} {% endblock %} En utilisant la balise spciale {% form_theme form _self %}, Twig cherchera tout bloc surcharg dans le mme template. En supposant que le champ form.age soit du type de champ integer, lorsque le widget sera affich, le bloc personnalis integer_widget sera utilis. L'inconvnient de cette mthode est que les blocs de formulaire personnaliss ne peuvent pas tre rutiliss pour afficher d'autres formulaires dans d'autres templates. En d'autres termes, cette mthode est spcialement utile pour faire des changements applicables un formulaire spcifique de votre application. Si vous voulez rutiliser vos personnalisations pour certains (ou tous les) autres formulaires, lisez la section suivante. Mthode 2: Dans un template spar Vous pouvez galement choisir de mettre le bloc de formulaire personnalis integer_widget dans un template spar. Le code et le rsultat final seront les mmes, mais vous pourrez alors rutiliser cette personnalisation de formulaire dans d'autres templates : 1 2 3 4 5 6 7 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} {% block integer_widget %} <div class="integer_widget"> {% set type = type|default('number') %} {{ block('form_widget_simple') }} </div> {% endblock %} Maintenant que vous avez cr le bloc de formulaire personnalis, vous devez dire Symfony de l'utiliser. Dans le template o vous affichez votre formulaire, indiquez Symfony d'utiliser votre template via la balise form_theme : PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 131 Listing 40-11 Listing 40-12 Listing 40-13 Listing 40-14 1 2 3 {% form_theme form 'AcmeDemoBundle:Form:fields.html.twig' %} {{ form_widget(form.age) }} Pour afficher le widget form.age, Symfony utilisera le bloc de formulaire integer_widget du nouveau template et la balise input sera encadre par l'lment div comme vous l'avez spcifi dans le bloc personnalis. Thme de formulaire en PHP Si vous utilisez PHP comme moteur de template, la seule mthode pour personnaliser un fragment est de crer un nouveau fichier de template, ce qui est quivalent la seconde mthode utilise par Twig. Le fichier de template doit tre nomm en fonction du fragment. Vous devez crer un fichier integer_widget.html.php pour personnaliser le fragment integer_widget. 1 2 3 4 <!-- src/Acme/DemoBundle/Resources/views/Form/integer_widget.html.php --> <div class="integer_widget"> <?php echo $view['form']->block($form, 'form_widget_simple', array('type' => isset($type) ? $type : "number")) ?> </div> Maintenant que vous avez cr le template de formulaire personnalis, vous devez dire Symfony de l'utiliser. Dans le template o vous affichez votre formulaire, indiquez Symfony d'utiliser le thme via la mthode setTheme : 1 2 3 <?php $view['form']->setTheme($form, array('AcmeDemoBundle:Form')) ;?> <?php $view['form']->widget($form['age']) ?> Pour afficher le widget form.age, Symfony utilisera le template personnalis integer_widget.html.php et la balise input sera encadre par l'lment div. Faire rfrence aux blocs par dfaut d'un formulaire (spcifique Twig) Jusqu' prsent, pour surcharger un bloc de formulaire particulier, la meilleure mthode consistait copier le bloc par dfaut depuis form_div_layout.html.twig 3 , le copier dans un nouveau template, et le personnaliser. Dans la plupart des cas, vous pouvez viter cela en rfrenant le bloc de base lorsque vous le personnalisez. C'est facile faire, mais cela peut varier sensiblement si vos personnalisations de bloc sont dans le mme template que le formulaire ou pas. Faire rfrence aux blocs depuis le mme template que le formulaire Importer les blocs en ajoutant la balise use dans le template o vous affichez le formulaire : 1 {% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %} 3. https://github.com/symfony/symfony/blob/2.1/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 132 Listing 40-15 Listing 40-16 Listing 40-17 A partir de maintenant, lorsque les blocs sont imports depuis form_div_layout.html.twig 4 , le bloc integer_widget sera renomm en base_integer_widget. Cela signifie que lorsque vous redfinissez le bloc integer_widget, vous pouvez faire rfrence l'implmentation par dfaut via base_integer_widget : 1 2 3 4 5 {% block integer_widget %} <div class="integer_widget"> {{ block('base_integer_widget') }} </div> {% endblock %} Faire rfrence aux blocs par dfaut depuis un template externe Si vos personnalisations de formulaire se trouvent dans un template externe, vous pouvez faire rfrence au bloc par dfaut en utilisant la fonction Twig parent() : 1 2 3 4 5 6 7 8 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} {% extends 'form_div_layout.html.twig' %} {% block integer_widget %} <div class="integer_widget"> {{ parent() }} </div> {% endblock %} Il n'est pas possible de faire rfrence au bloc par dfaut si vous utilisez PHP comme moteur de template. Vous devrez alors copier manuellement le code du bloc par dfaut dans votre nouveau fichier de template. Faire des personnalisations au niveau de l'application Si vous aimeriez qu'une personnalisation de formulaire soit globale votre application, vous pouvez faire cela en ralisant cette personnalisation dans un template externe, et en l'important dans la configuration de votre application : Twig En utilisant la configuration suivante, toute personnalisation de blocs de formulaire qui se trouve dans le template AcmeDemoBundle:Form:fields.html.twig sera utilise quand un formulaire sera affich. 1 2 3 4 5 6 7 # app/config/config.yml twig: form: resources: - 'AcmeDemoBundle:Form:fields.html.twig' # ... 4. https://github.com/symfony/symfony/blob/2.1/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 133 Listing 40-18 Listing 40-19 Listing 40-20 Listing 40-21 Listing 40-22 Par dfaut, Twig utilise un layout base de div pour afficher les formulaires. Cependant, certaines personnes prfreront utiliser un layout base de tableau. Utilisez la ressource form_table_layout.html.twig pour utiliser un tel layout : 1 2 3 4 5 # app/config/config.yml twig: form: resources: ['form_table_layout.html.twig'] # ... Si vous ne voulez appliquer ce changement que dans un seul template, ajoutez la ligne suivante dans votre fichier de template plutt que d'ajouter le template comme ressource : 1 {% form_theme form 'form_table_layout.html.twig' %} Notez que la variable form dans le code ci-dessus est la vue du formulaire que vous avez passe votre template. PHP En utilisant la configuration suivante, tout fragment de formulaire personnalis se situant dans le rpertoire src/Acme/DemoBundle/Resources/views/Form sera utilis lorsque le formulaire sera affich. 1 2 3 4 5 6 7 # app/config/config.yml framework: templating: form: resources: - 'AcmeDemoBundle:Form' # ... Par dfaut, le moteur de template PHP utilise un layout base de div pour afficher les formulaires. Nanmoins, certains prfreront afficher leurs formulaires dans un layout base de tableau. Utilisez la ressource FrameworkBundle:FormTable pour utiliser un tel layout : 1 2 3 4 5 6 # app/config/config.yml framework: templating: form: resources: - 'FrameworkBundle:FormTable' Si vous ne voulez appliquer vos changements que dans un seul template, ajoutez la ligne suivante dans votre fichier de template plutt que d'ajouter le template comme ressource : 1 <?php $view['form']->setTheme($form, array('FrameworkBundle:FormTable')); ?> Notez que la variable form dans le code ci-dessus est la vue du formulaire que vous avez passe votre template. Comment personnaliser un champ individuel Jusqu'ici, vous avez vu les diffrentes manires de personnaliser le widget gnr pour tous les types de champ texte. Vous pouvez galement personnaliser un champ individuel. Par exemple, supposons que PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 134 Listing 40-23 Listing 40-24 vous ayez deux champs texte (nom et prnom) mais que vous vouliez seulement personnaliser l'un de ces deux champs. Cela peut tre fait en personnalisant un fragment dont le nom est une combinaison de l'attribut id du champ et de la partie du champ concern (libell, widget ou erreur). Par exemple : 1 2 3 4 5 6 7 8 9 {% form_theme form _self %} {% block _product_name_widget %} <div class="text_widget"> {{ block('form_widget_simple') }} </div> {% endblock %} {{ form_widget(form.name) }} Ici, le fragment _product_name_widget dfinit le template utiliser pour le widget du champ dont l'id est product_name (et dont le nom est product[name]). La partie product du champ est le nom du formulaire qui peut tre dfini manuellement ou automatiquement gnr en se basant sur le nom du type de formulaire (par exemple : ProductType donnera product). Si vous n'tes pas sr du nom de votre formulaire, regardez le code source gnr de votre formulaire. Vous pouvez aussi surcharger le code d'un champ entier en utilisant la mme mthode : 1 2 3 4 5 6 7 8 9 10 {# _product_name_row.html.twig #} {% form_theme form _self %} {% block _product_name_row %} <div class="name_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div> {% endblock %} Autres personnalisations courantes Jusqu' prsent, ce document vous a expliqu diffrentes manires de personnaliser une unique partie de l'affichage d'un formulaire. L'ide est de personnaliser un fragment spcifique qui correspond une partie du formulaire que vous voulez contrler (lisez nommer les blocs de formulaire). Dans les sections suivantes, vous verrez comment effectuer plusieurs personnalisations de formulaires communes. Pour appliquer ces personnalisations, utilisez l'une des mthodes dcrites dans la section Thmes de formulaire. Personnaliser l'affichage des erreurs Le composant formulaire ne prend en charge que la manire dont les erreurs de validation sont affiches, et non les messages d'erreurs eux-mmes. Les messages d'erreurs sont dtermins par les contraintes de validation que vous appliquez vos objets. Pour plus d'informations, lisez le chapitre sur la validation. PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 135 Listing 40-25 Listing 40-26 Listing 40-27 Listing 40-28 Il y a plusieurs manires diffrentes de personnaliser l'affichage des erreurs lorsqu'un formulaire soumis n'est pas valide. Les messages d'erreur d'un champ sont affichs lorsque vous utilisez le helper form_errors : 1 {{ form_errors(form.age) }} Par dfaut, les erreurs sont affiches dans une liste non-ordonne : 1 2 3 <ul> <li>Ce champ est obligatoire</li> </ul> Pour surcharger l'affichage des erreurs pour tous les champs, il vous suffit de copier, coller et personnaliser le fragment form_errors. {# fields_errors.html.twig #} {% block form_errors %} {% spaceless %} {% if errors|length > 0 %} <ul class="error_list"> {% for error in errors %} <li>{{ error.messagePluralization is null ? error.messageTemplate|trans(error.messageParameters, 'validators') : error.messageTemplate|transchoice(error.messagePluralization, error.messageParameters, 'validators') }}</li> {% endfor %} </ul> {% endif %} {% endspaceless %} {% endblock form_errors %} Lisez Thmes de formulaire pour savoir comment appliquer ces personnalisations. Vous pouvez aussi personnaliser l'affichage des erreurs pour un seul type de champ spcifique. Par exemple, certaines erreurs qui sont plus globales (c'est--dire qui ne sont pas spcifiques un seul champ) sont affiches sparment, souvent en haut de votre formulaire : 1 {{ form_errors(form) }} Pour personnaliser uniquement le rendu de ces erreurs, suivez les mmes directives que ci-dessus, sauf que vous devez maintenant appeler le bloc form_errors (Twig) ou le fichier form_errors.html.php (PHP). Maintenant, lorsque les erreurs du type form seront affiches, votre fragment personnalis sera utilis au lieu du fragment par dfaut form_errors. Personnaliser le Form Row Quand vous pouvez le faire, la manire la plus simple d'afficher un champ de formulaire est la fonction form_row, qui affiche le libell, les erreurs et le widget HTML d'un champ. Pour personnaliser le code gnr utilis pour afficher tous les champs de formulaire, surchargez le fragment form_row. Par exemple, supposons que vous vouliez ajouter une classe l'lment div qui entoure chaque bloc : PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 136 Listing 40-29 Listing 40-30 Listing 40-31 Listing 40-32 1 2 3 4 5 6 7 8 {# form_row.html.twig #} {% block form_row %} <div class="form_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div> {% endblock form_row %} Lisez Thmes de formulaire pour savoir comment appliquer cette personnalisation. Ajouter une astrisque obligatoire sur les libells de champs Si vous voulez marquer tous les champs obligatoires par une astrisque (*), vous pouvez le faire en personnalisant le fragment form_label. Avec Twig, si vous faites cette personnalisation dans le mme template que votre formulaire, modifiez le tag use et ajoutez ce qui suit : 1 2 3 4 5 6 7 8 9 {% use 'form_div_layout.html.twig' with form_label as base_form_label %} {% block form_label %} {{ block('base_form_label') }} {% if required %} <span class="required" title="Ce champ est obligatoire">*</span> {% endif %} {% endblock %} Avec Twig, si vous faites les changements dans un template spar, ajoutez ce qui suit : 1 2 3 4 5 6 7 8 9 {% extends 'form_div_layout.html.twig' %} {% block form_label %} {{ parent() }} {% if required %} <span class="required" title="Ce champ est obligatoire">*</span> {% endif %} {% endblock %} Si vous utilisez PHP comme moteur de template, vous devrez copier le contenu depuis le template original : 1 2 3 4 5 6 7 8 <!-- form_label.html.php --> <!-- contenu original --> <?php if ($required) { $label_attr['class'] = trim((isset($label_attr['class']) ? $label_attr['class'] : '').' required'); } ?> <?php if (!$compound) { $label_attr['for'] = $id; } ?> <?php if (!$label) { $label = $view['form']->humanize($name); } ?> <label <?php foreach ($label_attr as $k => $v) { printf('%s="%s" ', $view->escape($k), PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 137 Listing 40-33 Listing 40-34 Listing 40-35 9 10 11 12 13 $view->escape($v)); } ?>><?php echo $view->escape($view['translator']->trans($label, array(), $translation_domain)) ?></label> <!-- personnalisation --> <?php if ($required) : ?> <span class="required" title="Ce champ est obligatoire">*</span> <?php endif ?> Lisez Thmes de formulaire pour savoir comment appliquer cette personnalisation. Ajouter des messages d' aide Vous pouvez aussi personnaliser vos widgets de formulaire avec un message d' aide ( help en anglais) facultatif. Avec Twig, si vous faites les changements dans le mme template que votre formulaire, modifiez le tag use et ajoutez ce qui suit : 1 2 3 4 5 6 7 8 9 {% use 'form_div_layout.html.twig' with form_widget_simple as base_form_widget_simple %} {% block form_widget_simple %} {{ block('base_form_widget_simple') }} {% if help is defined %} <span class="help">{{ help }}</span> {% endif %} {% endblock %} Avec Twig, si vous faites les changements dans un template spar, procdez comme suit : 1 2 3 4 5 6 7 8 9 {% extends 'form_div_layout.html.twig' %} {% block form_widget_simple %} {{ parent() }} {% if help is defined %} <span class="help">{{ help }}</span> {% endif %} {% endblock %} Si vous utilisez PHP comme moteur de template, vous devrez copier le contenu depuis le template original : 1 2 3 4 5 6 7 8 <!-- form_widget_simple.html.php --> <!-- Contenu original --> <input type="<?php echo isset($type) ? $view->escape($type) : 'text' ?>" <?php if (!empty($value)): ?>value="<?php echo $view->escape($value) ?>"<?php endif ?> <?php echo $view['form']->block($form, 'widget_attributes') ?> /> PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 138 Listing 40-36 Listing 40-37 9 10 11 12 13 <!-- Personnalisation --> <?php if (isset($help)) : ?> <span class="help"><?php echo $view->escape($help) ?></span> <?php endif ?> Pour afficher un message d'aide en dessous du champ, passez le dans une variable help : 1 {{ form_widget(form.title, {'help': 'foobar'}) }} Lisez Thmes de formulaire pour savoir comment appliquer ces personnalisations. Utiliser les Variables La plupart des fonctions disponibles pour afficher les diffrents parties d'un formulaire (ex widget, label, erreurs, etc) vous permettent galement de raliser certaines personnalisations directement. Jetez un oeil l'exemple suivant : 1 2 {# affiche un widget, mais y ajoute la classe "foo" #} {{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }} Le tableau pass comme second argument contient des variables de formulaire. Pour plus de dtails sur ce concept de Twig, lisez Un peu plus sur les Variables de formulaire. PDF brought to you by generated on June 26, 2014 Chapter 40: Comment personnaliser le rendu de formulaire | 139 Listing 41-1 Chapter 41 Comment utiliser les Convertisseurs de Donnes Vous allez souvent prouver le besoin de transformer les donnes entres par l'utilisateur dans un formulaire en quelque chose d'autre qui sera utilis dans votre programme. Vous pourriez effectuer ceci manuellement dans votre contrleur, mais que se passe-t-il si vous voulez rutiliser ce formulaire spcifique diffrents endroits ? Supposons que vous ayez une relation one-to-one de Task ( Tche en franais) vers Issue ( Problme en franais), par exemple : une tche possde de manire optionnelle un problme qui lui est li. Ajouter un lment listbox contenant tous les problmes possibles peut ventuellement engendrer une trs longue liste dans laquelle il est impossible de trouver quoi que ce soit. Vous pourriez vouloir utiliser plutt un champ texte dans lequel l'utilisateur pourra simplement taper le numro du problme recherch. Vous pouvez essayer de faire cela dans votre contrleur mais ce n'est pas la meilleure solution. Ce serait beaucoup mieux si votre problme pouvait automatiquement tre converti en un objet Issue. C'est maintenant que le Convertisseur de Donnes entre en jeu. Crer le convertisseur (Transformer) Crez d'abord la classe IssueToNumberTransformer, elle sera charge de convertir un numro de problme en un objet Issue et inversement: 1 2 3 4 5 6 7 8 9 10 // src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php namespace Acme\TaskBundle\Form\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; use Doctrine\Common\Persistence\ObjectManager; use Acme\TaskBundle\Entity\Issue; class IssueToNumberTransformer implements DataTransformerInterface { PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 140 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 /** * @var ObjectManager */ private $om; /** * @param ObjectManager $om */ public function __construct(ObjectManager $om) { $this->om = $om; } /** * Transforms an object (issue) to a string (number). * * @param Issue|null $issue * @return string */ public function transform($issue) { if (null === $issue) { return ""; } return $issue->getNumber(); } /** * Transforms a string (number) to an object (issue). * * @param string $number * @return Issue|null * @throws TransformationFailedException if object (issue) is not found. */ public function reverseTransform($number) { if (!$number) { return null; } $issue = $this->om ->getRepository('AcmeTaskBundle:Issue') ->findOneBy(array('number' => $number)) ; if (null === $issue) { throw new TransformationFailedException(sprintf( 'Le problme avec le numro "%s" ne peut pas tre trouv!', $number )); } return $issue; } } PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 141 Listing 41-2 Si vous voulez qu'un nouveau problme soit cr quand un numro inconnu est saisi, vous pouvez l'instancier plutt que de lancer l'exception TransformationFailedException. Utiliser le Convertisseur Maintenant que le convertisseur est construit, il vous suffit juste de l'ajouter votre champ Issue dans un formulaire. Vous pouvez galement utiliser les convertisseurs sans crer de nouveau type de champ de formulaire personnalis en appelant addModelTransformer (ou addViewTransformer, lisez Convertisseurs de modle et de vue) sur n'importe quel constructeur de champ: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 use Symfony\Component\Form\FormBuilderInterface; use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // ... // cela suppose que le gestionnaire d'entit a t pass en option $entityManager = $options['em']; $transformer = new IssueToNumberTransformer($entityManager); // ajoute un champ texte normal, mais y ajoute aussi votre convertisseur $builder->add( $builder->create('issue', 'text') ->addModelTransformer($transformer) ); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Task', )); $resolver->setRequired(array( 'em', )); $resolver->setAllowedTypes(array( 'em' => 'Doctrine\Common\Persistence\ObjectManager', )); // ... } // ... } PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 142 Listing 41-3 Listing 41-4 Cet exemple requiert que vous ayez pass le gestionnaire d'entit en option lors de la cration du formulaire. Plus tard, vous apprendrez comment vous pourriez crer un type de champ issue personnalis pour viter de devoir faire cela dans votre contrleur: 1 2 3 $taskForm = $this->createForm(new TaskType(), $task, array( 'em' => $this->getDoctrine()->getManager(), )); Cool, vous avez fini ! Votre utilisateur sera maintenant capable de saisir un numro de problme dans le champ texte, et il sera converti en un object Issue qui reprsente ce problme. Cela signifie que, aprs avoir russi associer (bind) les donnes, le framework Formulaire passera un vritable objet Issue la mthode Task::setIssue() plutt que son numro. Si le problme ne peut pas tre retrouv partir de son numro, une erreur sera cre pour ce champ et le message de cette erreur peut tre contrl grce l'option de champ invalid_message. Veuillez noter qu'ajouter le convertisseur requiert une syntaxe un peu plus complexe lorsque vous ajoutez un champ. L'exemple suivant est incorrect car le convertisseur serait appliqu au formulaire entier au lieu d'tre juste appliqu au champ: 1 2 3 4 // C'EST INCORRECT, LE CONVERTISSEUR SERA APPLIQUE A TOUT LE FORMULAIRE // regardez l'exemple ci-dessus pour connatre la bonne syntaxe $builder->add('issue', 'text') ->addModelTransformer($transformer); Convertisseurs de modle et de vue New in version 2.1: Les noms et mthodes des convertisseurs ont t changs dans Symfony 2.1. prependNormTransformer devient addModelTransformer et appendClientTransformer devient addViewTransformer. Dans l'exemple ci-dessus, le convertisseur a t utilis comme convertisseur de modle . En fait, il y a deux types diffrents de convertisseurs, et trois types diffrents de donnes. Dans tout formulaire, les trois types de donnes sont : 1) Donnes modle - C'est une donne dans le format utilis dans votre application (ex, un objet Issue). Si vous appelez Form::getData ou Form::setData, vous traitez avec la donne modle . 2) Donnes normalise - C'est la version normalise de votre donne, et c'est bien souvent la mme que votre donne modle (mais pas dans cet exemple). Elle n'est en gnral pas utilise directement. 3) Donne vue - C'est le format qui est utilis pour remplir les champs eux-mmes. C'est aussi le format dans lequel l'utilisateur soumettra ses donnes. Quand vous appelez les mthodes Form::bind($data), la variable $data est dans le format de donnes vue . Les deux diffrents types de convertisseurs vous aident faire des conversions entre ces types de donnes : Convertisseurs modle: transform: donne modle => donne normalise reverseTransform: donne normalise => donne modle Convertisseurs vue: transform: donne normalise => donne vue reverseTransform: donne vue => donne normalise PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 143 Listing 41-5 Le convertisseur que vous utiliserez dpendra de votre situation. Pour utiliser le convertisseur vue, appelez addViewTransformer. Alors pourquoi utiliser le convertisseur modle ? Dans cet exemple, le champ est un champ texte, et un champ texte devrait toujours tre dans un format simple, scalaire dans l'un des formats normalis ou vue . Pour cette raison, le convertisseur le plus appropri tait le convertisseur modle (qui convertit un format normalis, le numro du problme, en un format modle, l'objet Issue, et inversement). La diffrence entre les convertisseurs est subtile et vous devriez toujours penser ce que la donne normalise d'un champ devrait tre. Par exemple, la donne normalise d'un champ texte est une chane de caractres, mais c'est un objet DateTime pour un champ date. Utiliser les convertisseurs dans un type de champ personnalis Dans l'exemple ci-dessus, vous aviez appliqu le convertisseur sur un champ texte normal. C'tait facile mais cela a deux inconvnients : 1) Vous devez toujours vous souvenir d'appliquer le convertisseur lorsque vous ajoutez des champs pour saisir des numros de problme 2) Vous devez vous soucier de passer l'option em quand vous crez le formulaire qui utilise le convertisseur. Pour ces raisons, vous pourriez choisir de crer un type de champ personnalis. Premirement, crez la classe du type de champ personnalis: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // src/Acme/TaskBundle/Form/Type/IssueSelectorType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class IssueSelectorType extends AbstractType { /** * @var ObjectManager */ private $om; /** * @param ObjectManager $om */ public function __construct(ObjectManager $om) { $this->om = $om; } public function buildForm(FormBuilderInterface $builder, array $options) { $transformer = new IssueToNumberTransformer($this->om); $builder->addModelTransformer($transformer); PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 144 Listing 41-6 Listing 41-7 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'invalid_message' => 'The selected issue does not exist', )); } public function getParent() { return 'text'; } public function getName() { return 'issue_selector'; } } Ensuite, enregistrez votre type comme service et taggez le avec form.type pour qu'il soit reconnu comme type de champ personnalis : 1 2 3 4 5 6 services: acme_demo.type.issue_selector: class: Acme\TaskBundle\Form\Type\IssueSelectorType arguments: ["@doctrine.orm.entity_manager"] tags: - { name: form.type, alias: issue_selector } Maintenant, lorsque vous aurez besoin d'utiliser votre type de champ spcial issue_selector, ce sera trs facile: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // src/Acme/TaskBundle/Form/Type/TaskType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('task') ->add('dueDate', null, array('widget' => 'single_text')); ->add('issue', 'issue_selector'); } public function getName() { return 'task'; } } PDF brought to you by generated on June 26, 2014 Chapter 41: Comment utiliser les Convertisseurs de Donnes | 145 Listing 42-1 Chapter 42 Comment modifier dynamiquement les formulaires en utilisant les vnements Souvent, un formulaire ne pourra pas tre cr de faon statique. Dans cet article, vous apprendrez personnaliser vos formulaires selon trois cas courants: 1. Personnaliser un formulaire en se basant sur les donnes Exemple: vous avez un formulaire "Product" et vous devez modifier/ajouter/supprimer un champ selon les donnes de l 'objet avec lequel vous travaillez. 2. Comment grer des formulaires en se basant sur les donnes utilisateur Exemple: vous crez un formulaire "Message" et vous devez construire une liste droulante qui ne contient que les utilisateurs qui sont amis avec l'utilisateur actuellement authentifi. 3. Gnration dynamique pour formulaire soumis Exemple: dans un formulaire d'inscription, vous avez un champ "pays" et un champ "tat" que vous voulez alimenter selon la valeur du champ "pays". Personnaliser un formulaire en se basant sur les donnes Avant de se lancer directement dans la gnration dynamique de formulaire, commenons par rappeler ce quoi ressemble une classe de formulaire basique: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/DemoBundle/Form/Type/ProductType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name'); PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 146 Listing 42-2 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $builder->add('price'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\DemoBundle\Entity\Product' )); } public function getName() { return 'product'; } } Si la section de code ci-dessus ne vous est pas familire, vous avez probablement besoin de lire d'abord le chapitre sur les Formulaires avant d'aller plus loin. Supposons un instant que ce formulaire utilise une classe imaginaire Product qui possde uniquement deux proprits ( name et price ). Le formulaire gnr partir de cette classe sera identique que ce soit pour la cration d'un Produit ou pour l'dition d'un Produit existant (par exemple : un Produit rcupr depuis la base de donnes). Supposons maintenant que vous ne souhaitiez pas que l'utilisateur puisse changer la valeur de name une fois que l'objet a t cr. Pour faire cela, vous pouvez utiliser le Rpartiteur d'vnements de Symfony pour analyser les donnes de l'objet et modifier le formulaire en se basant sur les donnes de l'objet Product. Dans cet article, vous allez apprendre comment ajouter ce niveau de flexibilit vos formulaires. Ajouter un couteur d'vnement une classe de formulaire Donc, au lieu d'ajouter directement ce widget name`, la responsabilit de crer ce champ en particulier est laisse un couteur d'vnement: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // src/Acme/DemoBundle/Form/Type/ProductType.php namespace Acme\DemoBundle\Form\Type; // ... use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('price'); $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) { // ... adding the name field if needed }); } // ... } PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 147 Listing 42-3 Listing 42-4 Le but est de crer un champ name uniquement si l'objet Product sous-jacent est nouveau (par exemple : n'a pas t persist dans la base de donnes). Partant de ce principe, l'couteur d'vnement pourrait ressembler quelque chose comme a: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // ... public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){ $product = $event->getData(); $form = $event->getForm(); // vrifie si l'objet Product est "nouveau" // Si aucune donne n'est passe au formulaire, la donne est "null". // Ce doit tre considr comme un nouveau "Product" if (!$product || null === $product->getId()) { $form->add('name', 'text'); } }); } New in version 2.2: La possibilit de passer une chaine de caractres FormInterface::add 1 est une nouveaut de Symfony 2.2. Vous pouvez bien sr utiliser n'importe quel type de callback au lieu d'une closure, par exemple une mthode de l'objet ProductType` pour une meilleur lisibilit: 1 2 3 4 5 6 7 8 9 10 11 12 13 // ... class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData')); } public function onPreSetData(FormEvent $event){ // ... } } La ligne FormEvents::PRE_SET_DATA est convertie en form.pre_set_data. La classe FormEvents 2 a un but organisationnel. C'est un endroit centralis o vous trouverez la liste des diffrents vnements de formulaire disponibles. Vous pouvez voir la liste complte des vnement de formulaire dans la classe FormEvents 3 . 1. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#add() 2. http://api.symfony.com/2.3/Symfony/Component/Form/FormEvents.html 3. http://api.symfony.com/2.3/Symfony/Component/Form/FormEvents.html PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 148 Listing 42-5 Listing 42-6 Ajouter un souscripteur d'vnement sur un formulaire Pour une meilleur rutilisabilit ou s'il y a trop de logique dans votre couteur d'vnement, vous pouvez galement dplacer la logique qui cre le champ name dans un souscripteur d'vnements: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // src/Acme/DemoBundle/Form/Type/ProductType.php namespace Acme\DemoBundle\Form\Type; // ... use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber; class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('price'); $builder->addEventSubscriber(new AddNameFieldSubscriber()); } // ... } Maintenant, la logique qui cre le champ name est situe dans sa propre classe de souscripteur: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php namespace Acme\DemoBundle\Form\EventListener; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class AddNameFieldSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { // Dit au dispatcher que vous voulez couter l'vnement // form.pre_set_data et que la mthode preSetData doit tre appele return array(FormEvents::PRE_SET_DATA => 'preSetData'); } public function preSetData(FormEvent $event) { $product = $event->getData(); $form = $event->getForm(); if (!$product || null === $product->getId()) { $form->add('name', 'text'); } } } Comment grer des formulaires en se basant sur les donnes utilisateur Parfois, vous voulez qu'un formulaire soit gnr dynamiquement en se basant sur les donnes du formulaire mais aussi sur quelque chose d'autre - par exemple des donnes de l'utilisateur connect. Supposons que vous travaillez sur un site social o un utilisateur ne peut envoyer des messages qu' des PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 149 Listing 42-7 Listing 42-8 personnes marques comme amies sur ce site. Dans ce cas, la liste des personnes qui l'utilisateur peut envoyer des messages ne doit contenir que des amis. Crer le type de formulaire En utilisant un couteur d'vnements, votre formulaire pourrait ressembler ceci: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // src/Acme/DemoBundle/Form/Type/FriendMessageFormType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvent; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class FriendMessageFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('subject', 'text') ->add('body', 'textarea') ; $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){ // ... Ajouter une liste de choix d'amis de l'utilisateur connect }); } public function getName() { return 'acme_friend_message'; } public function setDefaultOptions(OptionsResolverInterface $resolver) { } } Le problme est maintenant de rcuprer l'utilisateur actuel et de crer un champ select qui ne contient que des amis de l'utilisateur. Heureusement, il est assez facile d'injecter un service dans le formulaire. Cela peut tre fait dans le constructeur: 1 2 3 4 5 6 private $securityContext; public function __construct(SecurityContext $securityContext) { $this->securityContext = $securityContext; } PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 150 Listing 42-9 Vous devez vous demander, maintenant que vous avez accs l'utilisateur (au travers du security context), pourquoi ne pas utiliser directement buildForm et viter de passer par un couteur d'vnement? C'est parce que le faire dans la mthode buildForm signifierait que tout le type de formulaire sera modifi et non pas juste l'instance de formulaire que vous voulez. Ce n'est gnralement pas trs grave, mais techniquement, un seul type de formulaire pourrait tre utilis dans une seule requte pour crer plusieurs formulaires ou champs Personnaliser le type de formulaire Maintenant que vous avez toutes les bases, vous pouvez tirer avantage du SecurityContext et remplir votre couteur: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 // src/Acme/DemoBundle/FormType/FriendMessageFormType.php use Symfony\Component\Security\Core\SecurityContext; use Doctrine\ORM\EntityRepository; // ... class FriendMessageFormType extends AbstractType { private $securityContext; public function __construct(SecurityContext $securityContext) { $this->securityContext = $securityContext; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('subject', 'text') ->add('body', 'textarea') ; // rcupre le user et vrifie rapidement qu'il existe bien $user = $this->securityContext->getToken()->getUser(); if (!$user) { throw new \LogicException( 'Le FriendMessageFormType ne peut pas tre utilis sans utilisateur connect!' ); } $builder->addEventListener( FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($user) { $form = $event->getForm(); $formOptions = array( 'class' => 'Acme\DemoBundle\Entity\User', 'property' => 'fullName', 'query_builder' => function(EntityRepository $er) use ($user) { // construit une requte personnalise // retourne $er->createQueryBuilder('u')->addOrderBy('fullName', 'DESC'); // ou appelle une mthode d'un repository qui retourne un query PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 151 Listing 42-10 Listing 42-11 46 47 48 49 50 51 52 53 54 55 56 57 builder // l'instance $er est une instance de UserRepository // retourne $er->createOrderByFullNameQueryBuilder(); }, ); // cre le champ, cela quivaut $builder->add() // nom du champ, type de champ, donne, options $form->add('friend', 'entity', $formOptions); } ); } // ... } Les options multiple et expanded valent false par dfaut car le type de champ est entity. Utiliser le formulaire Votre formulaire est maintenant prt tre utilis et il y a deux manires possibles de l'utiliser dans un contrleur : 1. le crer manuellement et y passer le security context; ou 2. le dfinir comme service. a) Crer le formulaire manuellement C'est trs simple, et probablement la meilleur approche moins que vous n'utilisiez votre nouveau type de champ plusieurs endroits ou que vous l'imbriquez dans d'autres formulaires: 1 2 3 4 5 6 7 8 9 10 11 12 class FriendMessageController extends Controller { public function newAction(Request $request) { $securityContext = $this->container->get('security.context'); $form = $this->createForm( new FriendMessageFormType($securityContext) ); // ... } } b) Definir le formulaire comme service Pour dfinir votre formulaire comme service, crez simplement un service classique que vous taggerez avec form.type. 1 2 # app/config/config.yml services: PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 152 Listing 42-12 Listing 42-13 Listing 42-14 Listing 42-15 3 4 5 6 7 acme.form.friend_message: class: Acme\DemoBundle\Form\Type\FriendMessageFormType arguments: ["@security.context"] tags: - { name: form.type, alias: acme_friend_message } Si vous dsirez le crer dans un contrleur ou dans n'importe quel autre service qui a accs la formfactory, alors faites: 1 2 3 4 5 6 7 8 9 10 11 use Symfony\Component\DependencyInjection\ContainerAware; class FriendMessageController extends ContainerAware { public function newAction(Request $request) { $form = $this->get('form.factory')->create('acme_friend_message'); // ... } } Si vous tendez la classe Symfony\Bundle\FrameworkBundle\Controller\Controller, appelez simplement: 1 $form = $this->createForm('acme_friend_message'); Vous pouvez galement l'imbriquer facilement dans un autre formulaire: 1 2 3 4 5 // dans une classe "form type" public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('message', 'acme_friend_message'); } Gnration dynamique pour formulaire soumis Un autre cas qui peut survenir est que vous voulez personnaliser votre formulaire selon des donnes soumises par l'utilisateur. Par exemple, imaginez que vous ayez un formulaire d'inscription pour des rassemblements sportifs. Certains vnements vous permettront de spcifier votre position prfre sur le terrain. Ce serait par exemple une liste de choix. Cependant, les choix possibles dpendront de chaque sport. Le football aura des attaquants, des dfenseurs, un gardien, ... mais le baseball aura un lanceur et pas de gardien. Vous devrez corriger les options pour que la validation soit bonne. Le rassemblement est au formulaire comme une entit. Nous pouvons donc accder chaque sport comme ceci: 1 2 3 4 5 6 7 // src/Acme/DemoBundle/Form/Type/SportMeetupType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; // ... PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 153 Listing 42-16 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class SportMeetupType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('sport', 'entity', array(...)) ; $builder->addEventListener( FormEvents::PRE_SET_DATA, function(FormEvent $event) { $form = $event->getForm(); // ce sera votre entit, c-a-d SportMeetup $data = $event->getData(); $positions = $data->getSport()->getAvailablePositions(); $form->add('position', 'entity', array('choices' => $positions)); } ); } } Lorsque vous construisez ce formulaire pour l'afficher l'utilisateur la premire fois, alors cet exemple fonctionne parfaitement. Cependant, les choses se compliqueront lorsque vous grerez la soumission. Ceci est d au fait que l'vnement PRE_SET_DATA nous donne la donne avec laquelle vous commencez (un objet SportMeetup vide), et non pas la donnes soumise. Dans un formulaire, vous pouvez couter les vnements suivants: PRE_SET_DATA POST_SET_DATA PRE_SUBMIT SUBMIT POST_SUBMIT New in version 2.3: Les vnements PRE_SUBMIT, SUBMIT et POST_SUBMIT ont t ajouts dans Symfony 2.3. Avant, ils taient nomms PRE_BIND, BIND et POST_BIND. New in version 2.2.6: Le comportement de l'vnement POST_SUBMIT a chang dans la version 2.2.6. Ci-dessous, un exemple d'utilisation. La solution est d'ajouter un couteur POST_SUBMIT sur le champ dont votre nouveau champ dpend. Si vous ajoutez un couteur POST_SUBMIT sur un champ enfant (ex sport), et ajoutez un nouvel enfant au formulaire parent, le composant Form dtectera automatiquement le nouveau champ et lui associera les donnes soumises par le client. Le type de formulaire ressemble maintenant ceci: PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 154 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 // src/Acme/DemoBundle/Form/Type/SportMeetupType.php namespace Acme\DemoBundle\Form\Type; // ... use Acme\DemoBundle\Entity\Sport; use Symfony\Component\Form\FormInterface; class SportMeetupType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('sport', 'entity', array(...)) ; $formModifier = function(FormInterface $form, Sport $sport) { $positions = $sport->getAvailablePositions(); $form->add('position', 'entity', array('choices' => $positions)); }; $builder->addEventListener( FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($formModifier) { // ce sera votre entit, c-a-d SportMeetup $data = $event->getData(); $formModifier($event->getForm(), $data->getSport()); } ); $builder->get('sport')->addEventListener( FormEvents::POST_SUBMIT, function(FormEvent $event) use ($formModifier) { // Il est important de rcuprer ici $event->getForm()->getData(), // car $event->getData() vous renverra la donnes initiale (vide) $sport = $event->getForm()->getData(); // puisque nous avons ajout l'couteur l'enfant, il faudra passer // le parent aux fonctions de callback! $formModifier($event->getForm()->getParent(), $sport); } ); } } Vous pouvez constater que vous devez couter ces deux vnements et avoir diffrentes fonctions de callback juste parce que dans deux scnarios diffrents, les donnes que vous pouvez utiliser sont disponibles dans diffrents vnements. A part cela, les couteurs ralisent exactement la mme chose dans un formulaire donn. Mais il manque encore la mise jour du formulaire ct client aprs que la slection du sport a t faite. Cela devrait tre fait grce un appel AJAX dans votre application. Dans ce controller, vous pourrez soumettre votre formulaire, mais au lieu de le traiter, simplement utiliser le formulaire soumis pour afficher les champs mis jour. La rponse de l'appel AJAX pourra alors tre utilise pour mettre jour la vue. PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 155 Listing 42-17 Supprimer la validation de formulaire Pour supprimer la validation, vous pouvez utiliser l'vnement POST_SUBMIT et empcher le ValidationListener 4 d'tre appel. Vous pouvez tre amen faire cela si vous dfinissez group_validation false car, mme dans ce cas, certaines vrifications sont tout de mme faites. Par exemple, un fichier upload sera quand mme vrifi pour voir s'il est trop volumineux, et un formulaire vrifiera galement si des champs supplmentaires ont t soumis. Pour dsactiver tout cela, utilisez un couteur: 1 2 3 4 5 6 7 8 9 10 11 use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addEventListener(FormEvents::POST_SUBMIT, function($event) { $event->stopPropagation(); }, 900); // Dfinissez toujours une priorit plus grande que le ValidationListener // ... } En faisant cela, vous risquez de dsactiver plus que la validation du formulaire, puisque POST_SUBMIT peut avoir d'autres couteurs. 4. http://api.symfony.com/2.3/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.html PDF brought to you by generated on June 26, 2014 Chapter 42: Comment modifier dynamiquement les formulaires en utilisant les vnements | 156 Listing 43-1 Chapter 43 Comment imbriquer une Collection de Formulaires Dans cet article, vous allez apprendre crer un formulaire qui imbrique une collection de plusieurs autres formulaires. Cela pourrait tre utile, par exemple, si vous avez une classe Task et que vous voulez diter/crer/supprimer diffrents objets Tag lis cette Task , tout cela dans le mme formulaire. Dans cet article, nous supposons que vous utilisez Doctrine en tant qu'outil de stockage pour votre base de donnes. Mais si vous n'utilisez pas Doctrine (par exemple : Propel ou une simple connexion la base de donnes), tout reste trs similaire. Seules quelques sections de ce tutoriel s'occupent de la persistence . Si vous utilisez Doctrine, vous aurez besoin d'ajouter les mta-donnes de Doctrine, incluant la relation ManyToMany sur la proprit tags de Task. Pour commencer, supposons que chaque Task appartienne plusieurs objets Tags. Crons tout d'abord une simple classe Task: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // src/Acme/TaskBundle/Entity/Task.php namespace Acme\TaskBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; class Task { protected $description; protected $tags; public function __construct() { $this->tags = new ArrayCollection(); } PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 157 Listing 43-2 Listing 43-3 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public function getDescription() { return $this->description; } public function setDescription($description) { $this->description = $description; } public function getTags() { return $this->tags; } public function setTags(ArrayCollection $tags) { $this->tags = $tags; } } ArrayCollection est spcifique Doctrine et revient utiliser un array (mais cela doit tre une ArrayCollection si vous utilisez Doctrine). Maintenant, crez une classe Tag. Comme vous l'avez vu ci-dessus, une Task peut avoir plusieurs objets Tag: 1 2 3 4 5 6 7 // src/Acme/TaskBundle/Entity/Tag.php namespace Acme\TaskBundle\Entity; class Tag { public $name; } La proprit name est public ici, mais elle pourrait tout aussi bien tre protected ou private (ce qui impliquerait d'avoir des mthodes getName et setName). Venons-en maintenant aux formulaires. Crez une classe formulaire afin qu'un objet Tag puisse tre modifi par l'utilisateur: 1 2 3 4 5 6 7 8 9 10 // src/Acme/TaskBundle/Form/Type/TagType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TagType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 158 Listing 43-4 Listing 43-5 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { $builder->add('name'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Tag', )); } public function getName() { return 'tag'; } } Avec cela, vous avez tout ce qu'il faut pour afficher un formulaire pour le tag lui-mme. Mais comme le but final est de permettre la modification des tags d'une Task directement depuis le formulaire de la task lui-mme, crez un formulaire pour la classe Task. Notez que vous imbriquez une collection de formulaires TagType en utilisant le type de champ collection: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // src/Acme/TaskBundle/Form/Type/TaskType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description'); $builder->add('tags', 'collection', array('type' => new TagType())); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Task', )); } public function getName() { return 'task'; } } Dans votre contrleur, vous allez maintenant initialiser une nouvelle instance de TaskType: 1 2 3 4 // src/Acme/TaskBundle/Controller/TaskController.php namespace Acme\TaskBundle\Controller; use Acme\TaskBundle\Entity\Task; PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 159 Listing 43-6 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 use Acme\TaskBundle\Entity\Tag; use Acme\TaskBundle\Form\Type\TaskType; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class TaskController extends Controller { public function newAction(Request $request) { $task = new Task(); // code de test - le code ci-dessous est simplement l pour que la // Task ait quelques tags, sinon, l'exemple ne serait pas intressant $tag1 = new Tag(); $tag1->name = 'tag1'; $task->getTags()->add($tag1); $tag2 = new Tag(); $tag2->name = 'tag2'; $task->getTags()->add($tag2); // fin du code de test $form = $this->createForm(new TaskType(), $task); // analyse le formulaire quand on reoit une requte POST if ($request->isMethod('POST')) { $form->bind($request); if ($form->isValid()) { // ici vous pouvez par exemple sauvegarder la Task et ses objets Tag } } return $this->render('AcmeTaskBundle:Task:new.html.twig', array( 'form' => $form->createView(), )); } } Le template correspondant est maintenant capable d'afficher le champ description pour le formulaire de la tche ainsi que les formulaires TagType pour n'importe quels tags qui sont lis cet objet Task. Dans le contrleur ci-dessus, j'ai ajout du code de test afin que vous puissiez voir cela en action (puisqu'un objet Task possde zro tag lorsqu'il est cr pour la premire fois). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #} {# ... #} <form action="..." method="POST" {{ form_enctype(form) }}> {# affiche l'unique champ de la tche : description #} {{ form_row(form.description) }} <h3>Tags</h3> <ul class="tags"> {# itre sur chaque tag existant et affiche son unique champ : name #} {% for tag in form.tags %} <li>{{ form_row(tag.name) }}</li> {% endfor %} </ul> PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 160 Listing 43-7 17 18 19 {{ form_rest(form) }} {# ... #} </form> Lorsque l'utilisateur soumet le formulaire, les donnes soumises pour les champs Tags sont utilises pour construire une collection ArrayCollection d'objets Tag, qui est ensuite affecte au champ tag de l'instance Task. La collection Tags est naturellement accessible via $task->getTags() et peut tre persiste dans la base de donnes ou utilise de la manire que vous voulez. Jusqu'ici, tout cela fonctionne bien, mais cela ne vous permet pas d'ajouter de nouveaux tags ou de supprimer des tags existants de manire dynamique. Donc, bien qu'diter des tags existants fonctionnera parfaitement, votre utilisateur ne pourra pour le moment pas en ajouter de nouveaux. Dans cet exemple, vous n'imbriquez qu'une seule collection, mais vous n'tes pas limit cela. Vous pouvez aussi intgrer des collections imbriques avec autant de sous-niveaux que vous souhaitez. Mais si vous utilisez Xdebug dans votre environnement de dveloppement, vous pourriez recevoir une erreur telle Maximum function nesting level of '100' reached, aborting!. Cela est d au paramtre PHP xdebug.max_nesting_level, qui est dfini avec une valeur de 100 par dfaut. Cette directive limite la rcursion 100 appels, ce qui ne pourrait pas tre assez pour afficher le formulaire dans le template si vous affichez le formulaire en entier en une seule fois (par exemple : form_widget(form)). Pour parer cela, vous pouvez dfinir cette directive avec une valeur plus haute (soit dans le fichier ini PHP ou l'aide de ini_set 1 , par exemple dans app/autoload.php) ou bien afficher chaque champ du formulaire manuellement en utilisant form_row. Autoriser de nouveaux tags avec le prototypage Autoriser l'utilisateur ajouter de nouveaux tags signifie que vous allez avoir besoin d'utiliser un peu de Javascript. Plus tt, vous avez ajout deux tags votre formulaire dans le contrleur. Maintenant, vous devez permettre l'utilisateur d'ajouter autant de tags qu'il souhaite directement depuis le navigateur. Quelques lignes de Javascript sont ncessaires pour effectuer cela. La premire chose que vous devez faire est de spcifier la collection du formulaire qu'elle va recevoir un nombre inconnu de tags. Jusqu'ici, vous avez ajout deux tags et le formulaire s'attend en recevoir exactement deux, sinon une erreur sera leve : This form should not contain extra fields ce qui signifie que le formulaire ne peut contenir de champs supplmentaires. Pour rendre cela flexible, vous ajoutez l'option allow_add votre champ collection: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/TaskBundle/Form/Type/TaskType.php // ... use Symfony\Component\Form\FormBuilderInterface; public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description'); $builder->add('tags', 'collection', array( 'type' => new TagType(), 1. http://php.net/manual/en/function.ini-set.php PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 161 Listing 43-8 Listing 43-9 Listing 43-10 Listing 43-11 13 14 15 16 'allow_add' => true, 'by_reference' => false, )); } Notez que 'by_reference' => false a galement t ajout. Normalement, le framework de formulaire modifierait les tags d'un objet Task sans jamais appeler setTags. En dfinissant by_reference false, setTags sera appele. Vous comprendrez plus tard pourquoi cela est important. En plus de dire au champ d'accepter n'importe quel nombre d'objets soumis, l'option allow_add met une variable prototype votre disposition. Ce prototype est un petit template qui contient tout le code HTML ncessaire pour afficher un nouveau formulaire tag . Pour l'utiliser, faites le changement suivant dans votre formulaire : 1 2 3 <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}"> ... </ul> Si vous affichez votre sous-formulaire tags en entier et en une seule fois (par exemple : form_row(form.tags)), alors le prototype est automatiquement disponible dans le div extrieur avec l'attribut data-prototype, similaire ce que vous voyez ci-dessus. form.tags.vars.prototype est un lment de formulaire qui ressemble l'lment individuel form_widget(tag) l'intrieur de votre boucle for. Cela signifie que vous pouvez appeler form_widget, form_row, ou form_label sur ce prototype. Vous pourriez mme choisir de n'afficher qu'un seul de ses champs (par exemple : le champ name) : 1 {{ form_widget(form.tags.vars.prototype.name)|e }} Sur la page affiche, le rsultat ressemblera quelque chose comme ceci : 1 <ul class="tags" data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</ label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>"> Le but de cette section sera d'utiliser Javascript pour lire cet attribut et ajouter dynamiquement un nouveau tag lorsque l'utilisateur clique sur un lien Ajouter un tag . Pour garder les choses simples, cet exemple utilise jQuery et suppose que vous l'avez dj inclus quelque part dans votre page. Ajoutez une balise script quelque part dans votre page afin que vous puissiez commencer crire un peu de Javascript. Tout d'abord, ajoutez un lien en bas de votre liste de tags via Javascript. Ensuite, liez l'vnement click de ce lien afin que vous puissiez ajouter un formulaire tag (addTagForm sera expliqu plus tard) : 1 2 3 // Rcupre le div qui contient la collection de tags var collectionHolder = $('ul.tags'); PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 162 Listing 43-12 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // ajoute un lien add a tag var $addTagLink = $('<a href="#" class="add_tag_link">Ajouter un tag</a>'); var $newLinkLi = $('<li></li>').append($addTagLink); jQuery(document).ready(function() { // ajoute l'ancre ajouter un tag et li la balise ul collectionHolder.append($newLinkLi); $addTagLink.on('click', function(e) { // empche le lien de crer un # dans l'URL e.preventDefault(); // ajoute un nouveau formulaire tag (voir le prochain bloc de code) addTagForm(collectionHolder, $newLinkLi); }); }); Le travail de la fonction addTagForm sera d'utiliser l'attribut data-prototype pour ajouter dynamiquement un nouveau formulaire lorsqu'on clique sur ce lien. Le code HTML de data-prototype contient la balise texte avec un nom du genre task[tags][__name__][name] et un id du genre task_tags___name___name. La chane de caractres __name__ est une variable de substitution ( placeholder en anglais) que vous remplacerez avec un nombre unique et incrmental (par exemple : task[tags][3][name]). New in version 2.1: La variable de substitution a t change dans Symfony 2.1. Au lieu de $$name$$, elle se nomme dornavant __name__. Le code ncessaire pour faire fonctionner tout cela peut varier, mais en voici un exemple : 1 2 3 4 5 6 7 8 9 10 11 12 function addTagForm(collectionHolder, $newLinkLi) { // Rcupre l'lment ayant l'attribut data-prototype comme expliqu plus tt var prototype = collectionHolder.attr('data-prototype'); // Remplace '__name__' dans le HTML du prototype par un nombre bas sur // la longueur de la collection courante var newForm = prototype.replace(/__name__/g, collectionHolder.children().length); // Affiche le formulaire dans la page dans un li, avant le lien "ajouter un tag" var $newFormLi = $('<li></li>').append(newForm); $newLinkLi.before($newFormLi); } Maintenant, chaque fois qu'un utilisateur cliquera sur le lien Ajouter un tag, un nouveau sous- formulaire apparatra sur la page. Lors de la soumission, tout nouveau formulaire de tag sera converti en un nouvel objet Tag et ajout la proprit tags de l'objet Task. PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 163 Listing 43-13 Listing 43-14 Listing 43-15 Doctrine: Relations de Cascade et sauvegarde du ct Inverse Afin que les nouveaux tags soient sauvegards dans Doctrine, vous devez prendre en compte certains lments. Tout d'abord, moins que vous n'itriez sur tous les nouveaux objets Tag et appelez $em->persist($tag) sur chacun d'entre eux, vous allez recevoir une erreur de la part de Doctrine : A new entity was found through the relationship 'AcmeTaskBundleEntityTask#tags' that was not configured to cascade persist operations for entity... Pour rparer cela, vous pourriez choisir d'effectuer automatiquement l'opration de persistence en mode cascade de l'objet Task pour tout les tags lis. Pour faire ceci, ajoutez l'option cascade votre mta-donne ManyToMany : 1 2 3 4 5 6 7 8 // src/Acme/TaskBundle/Entity/Task.php // ... /** * @ORM\OneToMany(targetEntity="Tag", cascade={"persist"}) */ protected $tags; Un second problme potentiel peut toucher les relations de Doctrine en ce qui concerne Le ct Propritaire et le ct Inverse 2 . Dans cet exemple, si le ct propritaire dans la relation est Task , alors la persistence va fonctionner sans problme comme les tags sont ajouts correctement la Task . Cependant, si le ct propritaire est Tag , alors vous aurez besoin de coder un peu plus afin de vous assurer que le bon ct de la relation est correctement modifi. L'astuce est de s'assurer qu'une unique Task est dfinie pour chaque Tag . Une manire facile de faire cela est d'ajouter un bout de logique supplmentaire la mthode setTags(), qui est appele par le framework formulaire puisque la valeur de by_reference est dfinie comme false: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/TaskBundle/Entity/Task.php // ... public function setTags(ArrayCollection $tags) { foreach ($tags as $tag) { $tag->addTask($this); } $this->tags = $tags; } Dans le Tag, assurez-vous simplement d'avoir une mthode addTask: 1 2 3 4 5 6 7 8 9 10 // src/Acme/TaskBundle/Entity/Tag.php // ... public function addTask(Task $task) { if (!$this->tasks->contains($task)) { $this->tasks->add($task); } } PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 164 Listing 43-16 Listing 43-17 Si vous avez une relation OneToMany, alors l'astuce reste similaire, except que vous pouvez simplement appeler la mthode setTask depuis la mthode setTags. Autoriser des tags tre supprims La prochaine tape est d'autoriser la suppression d'un lment particulier de la collection. La solution est similaire celle qui permet d'autoriser l'ajout de tags. Commencez par ajouter l'option allow_delete dans le Type de formulaire: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // src/Acme/TaskBundle/Form/Type/TaskType.php // ... use Symfony\Component\Form\FormBuilderInterface; public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description'); $builder->add('tags', 'collection', array( 'type' => new TagType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, )); } Modifications des Templates L'option allow_delete a une consquence : si un lment d'une collection n'est pas envoy lors de la soumission, les donnes lies cet lment sont supprimes de la collection sur le serveur. La solution est donc de supprimer l'lment formulaire du DOM. Premirement, ajoutez un lien Supprimer ce tag dans chaque formulaire de tag : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 jQuery(document).ready(function() { // ajoute un lien de suppression tous les lments li de // formulaires de tag existants collectionHolder.find('li').each(function() { addTagFormDeleteLink($(this)); }); // ... le reste du bloc vu plus haut }); function addTagForm() { // ... // ajoute un lien de suppression au nouveau formulaire addTagFormDeleteLink($newFormLi); } 2. http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 165 Listing 43-18 La fonction addTagFormDeleteLink va ressembler quelque chose comme ceci : 1 2 3 4 5 6 7 8 9 10 11 12 function addTagFormDeleteLink($tagFormLi) { var $removeFormA = $('<a href="#">Supprimer ce tag</a>'); $tagFormLi.append($removeFormA); $removeFormA.on('click', function(e) { // empche le lien de crer un # dans l'URL e.preventDefault(); // supprime l'lment li pour le formulaire de tag $tagFormLi.remove(); }); } Lorsqu'un formulaire de tag est supprim du DOM et soumis, l'objet Tag supprim ne sera pas inclus dans la collection passe setTags. Selon votre couche de persistence, cela pourrait ou ne pourrait pas tre suffisant pour effectivement supprimer la relation entre le Tag supprim et l'objet Task. PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 166 Listing 43-19 Doctrine: Assurer la persistence dans la base de donnes Quand vous supprimez des objets de cette manire, vous pourriez avoir travailler un peu plus afin de vous assurer que la relation entre la Task et le Tag supprim soit correctement enleve. Dans Doctrine, vous avez deux cts dans une relation : le ct propritaire et le ct inverse. Normalement, dans ce cas, vous aurez une relation ManyToMany et les tags supprims disparatront et seront persists correctement (ajouter des tags fonctionne aussi sans efforts supplmentaires). Mais si vous avez une relation OneToMany ou une ManyToMany avec un mappedBy sur l'entit Task (signifiant qu'une Task est le ct inverse ), vous devrez effectuer plus de choses afin que les tags supprims soient persists correctement. Dans ce cas, vous pouvez modifier le contrleur afin qu'il efface la relation pour les tags supprims. Ceci suppose que vous ayez une editAction qui gre la mise jour de votre Task : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 // src/Acme/TaskBundle/Controller/TaskController.php use Doctrine\Common\Collections\ArrayCollection; // ... public function editAction($id, Request $request) { $em = $this->getDoctrine()->getManager(); $task = $em->getRepository('AcmeTaskBundle:Task')->find($id); if (!$task) { throw $this->createNotFoundException('Aucune tche trouve pour cet id : '.$id); } $originalTags = new ArrayCollection(); // Cre un tableau contenant les objets Tag courants de la // base de donnes foreach ($task->getTags() as $tag) { $originalTags->add($tag); } $editForm = $this->createForm(new TaskType(), $task); if ($request->isMethod('POST')) { $editForm->bind($this->getRequest()); if ($editForm->isValid()) { // supprime la relation entre le tag et la Task foreach ($originalTags as $tag) { if ($task->getTags()->contains($tag) == false) { // supprime la Task du Tag $tag->getTasks()->removeElement($task); // si c'tait une relation ManyToOne, vous pourriez supprimer la // relation comme ceci // $tag->setTask(null); $em->persist($tag); // si vous souhaitiez supprimer totalement le Tag, vous pourriez PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 167 46 47 48 49 50 51 52 53 54 55 56 57 58 59 // aussi faire comme cela // $em->remove($tag); } } $em->persist($task); $em->flush(); // redirige vers quelconque page d'dition return $this->redirect($this->generateUrl('task_edit', array('id' => $id))); } } // affiche un template de formulaire quelconque } Comme vous pouvez le voir, ajouter et supprimer les lments correctement peut ne pas tre trivial. A moins que vous n'ayez une relation ManyToMany o Task est le ct propritaire , vous devrez ajouter du code supplmentaire pour vous assurer que la relation soit correctement mise jour (que ce soit pour l'ajout de nouveaux tags ou pour la suppression de tags existants) pour chacun des objets Tag. PDF brought to you by generated on June 26, 2014 Chapter 43: Comment imbriquer une Collection de Formulaires | 168 Listing 44-1 Chapter 44 Comment Crer un Type de Champ de Formulaire Personnalis Symfony est livr avec un ensemble de types de champ essentiels disponible pour construire des formulaires. Cependant, il y a des situations o vous voudrez crer un type de champ de formulaire personnalis pour un besoin spcifique. Cet article part du principe que vous avez besoin d'une dfinition de champ qui s'occupe du sexe/genre d'une personne, base sur le champ existant choice . Cette section explique comment le champ est dfini, comment vous pouvez personnaliser son affichage et, finalement, comment vous pouvez le dclarer afin de pouvoir l'utiliser dans votre application. Dfinir le Type de Champ Afin de crer le type de champ personnalis, vous devez crer tout d'abord la classe reprsentant le champ. Dans cette situation, la classe s'occupant du type de champ sera nomme GenderType et le fichier sera stock dans le rpertoire par dfaut pour les champs de formulaire, c'est--dire <BundleName>\Form\Type. Assurez-vous que le champ tend AbstractType 1 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/Acme/DemoBundle/Form/Type/GenderType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class GenderType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'choices' => array( 'm' => 'Male', 'f' => 'Female', 1. http://api.symfony.com/2.3/Symfony/Component/Form/AbstractType.html PDF brought to you by generated on June 26, 2014 Chapter 44: Comment Crer un Type de Champ de Formulaire Personnalis | 169 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ) )); } public function getParent() { return 'choice'; } public function getName() { return 'gender'; } } L'endroit o est stock ce fichier n'est pas important - le rpertoire Form\Type est seulement une convention. Ici, la valeur retourne par la fonction getParent indique que vous tendez le type de champ choice. Cela signifie que, par dfaut, vous hritez de toute la logique et du rendu de l'affichage de ce type de champ. Pour avoir un aperu de cette logique, jetez un oeil la classe ChoiceType 2 . Il y a trois mthodes qui sont particulirement importantes : buildForm() - Chaque type de champ possde une mthode buildForm, qui est l'endroit o vous configurez et construisez n'importe quel(s) champ(s). Notez que c'est la mme mthode que vous utilisez pour initialiser vos formulaires, et cela fonctionne de la mme manire ici. buildView() - Cette mthode est utilise pour dfinir quelconques variables supplmentaires dont vous aurez besoin lors du rendu de votre champ dans un template. Par exemple, dans ChoiceType 3 , une variable multiple est dfinie et utilise dans le template pour dfinir (ou ne pas dfinir) l'attribut multiple pour le champ select. Voir Crer un Template pour le Champ pour plus de dtails. setDefaultOptions() - Cette mthode dfinit des options pour votre formulaire qui peuvent tre utilises dans buildForm() et buildView(). Il y a beaucoup d'options communes tous les champs (lisez Type de champ Form), mais vous pouvez crer ici n'importe quelle autre dont vous pourriez avoir besoin. Si vous crez un champ qui se compose de beaucoup de champs, alors assurez-vous de dfinir votre type parent en tant que form ou quelque chose qui tend form. Aussi, si vous avez besoin de modifier la vue ( view en anglais) de n'importe lequel de vos types enfants depuis votre type parent, utilisez la mthode finishView(). La mthode getName() retourne un identifiant qui devrait tre unique dans votre application. Ce dernier est utilis dans diffrents endroits, comme par exemple lorsque vous personnalisez la manire dont votre formulaire sera rendu. Le but de ce champ tait d'tendre le type choice afin d'activer la slection du sexe/genre. Cela est accompli en dfinissant choices par une liste de genres possibles. 2. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php 3. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php PDF brought to you by generated on June 26, 2014 Chapter 44: Comment Crer un Type de Champ de Formulaire Personnalis | 170 Listing 44-2 Listing 44-3 Listing 44-4 Crer un Template pour le Champ Chaque type de champ est rendu par un fragment de template, qui est dtermin en partie par la valeur retourne par votre mthode getName(). Pour plus d'informations, voir Que sont les thmes de formulaire ?. Dans ce cas, comme le champ parent est choice, vous n'avez pas besoin de faire quoi que ce soit car le type de champ personnalis sera automatiquement affich comme un type choice. Mais pour le bien fond de cet exemple, supposons que quand votre champ est tendu (i.e. boutons radio ou checkbox, la place d'un champ select ), vous souhaitez toujours l'afficher dans un lment ul. Dans le template de votre thme de formulaire (voir le lien ci-dessus pour plus de dtails), crez un bloc gender_widget pour le grer : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} {% block gender_widget %} {% spaceless %} {% if expanded %} <ul {{ block('widget_container_attributes') }}> {% for child in form %} <li> {{ form_widget(child) }} {{ form_label(child) }} </li> {% endfor %} </ul> {% else %} {# just let the choice widget render the select tag #} {{ block('choice_widget') }} {% endif %} {% endspaceless %} {% endblock %} Assurez-vous que c'est le bon prfixe du widget qui est utilis. Dans cet exemple, le nom devrait tre gender_widget, si l'on se fie la valeur retourne par getName. De plus, le fichier de configuration principal devrait pointer vers le template du formulaire personnalis afin qu'il soit utilis lors de l'affichage de tous les formulaires. 1 2 3 4 5 # app/config/config.yml twig: form: resources: - 'AcmeDemoBundle:Form:fields.html.twig' Utiliser le Type de Champ Vous pouvez ds lors utiliser votre type de champ personnalis en crant tout simplement une nouvelle instance du type dans l'un de vos formulaires: 1 2 3 4 // src/Acme/DemoBundle/Form/Type/AuthorType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; PDF brought to you by generated on June 26, 2014 Chapter 44: Comment Crer un Type de Champ de Formulaire Personnalis | 171 Listing 44-5 Listing 44-6 Listing 44-7 5 6 7 8 9 10 11 12 13 14 15 use Symfony\Component\Form\FormBuilderInterface; class AuthorType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('gender_code', new GenderType(), array( 'empty_value' => 'Choose a gender', )); } } Mais cela fonctionne uniquement car le GenderType() est trs simple. Que se passerait-il si les diffrents genres taient stocks dans un fichier de configuration ou dans une base de donnes ? La prochaine section explique comment des types de champ plus complexes peuvent rsoudre ce problme. Crer votre Type de Champ en tant que Service Jusqu'ici, cet article a suppos que vous aviez un type de champ personnalis trs simple. Mais si vous avez besoin d'accder la configuration, une connexion la base de donnes, ou n'importe quel autre service, alors vous allez vouloir dclarer votre type personnalis en tant que service. Par exemple, supposons que vous stockiez les paramtres du sexe/genre dans une configuration : 1 2 3 4 5 # app/config/config.yml parameters: genders: m: Male f: Female Pour utiliser ce paramtre, dfinissez votre type de champ personnalis en tant que service, en injectant la valeur du paramtre genders en tant que premier argument de la fonction __construct (qui doit tre cre) : 1 2 3 4 5 6 7 8 # src/Acme/DemoBundle/Resources/config/services.yml services: acme_demo.form.type.gender: class: Acme\DemoBundle\Form\Type\GenderType arguments: - "%genders%" tags: - { name: form.type, alias: gender } Assurez-vous que le fichier des services est import. Voir Importer la Configuration avec imports pour plus de dtails. Assurez vous que l'attribut alias du tag corresponde la valeur retourne par la mthode getName dfinie plus tt. Vous allez voir l'importance de cela dans un moment quand vous utiliserez le type de champ personnalis. Mais tout d'abord, ajoutez une mthode __construct GenderType, qui reoit la configuration du sexe/genre: PDF brought to you by generated on June 26, 2014 Chapter 44: Comment Crer un Type de Champ de Formulaire Personnalis | 172 Listing 44-8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // src/Acme/DemoBundle/Form/Type/GenderType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\OptionsResolver\OptionsResolverInterface; // ... // ... class GenderType extends AbstractType { private $genderChoices; public function __construct(array $genderChoices) { $this->genderChoices = $genderChoices; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'choices' => $this->genderChoices, )); } // ... } Super ! Le GenderType est maintenant rempli par les paramtres de la configuration et dclar en tant que service. De plus, parce que vous avez utilis l'alias form.type dans sa configuration, utiliser le champ est maintenant beaucoup plus facile: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // src/Acme/DemoBundle/Form/Type/AuthorType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; // ... class AuthorType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('gender_code', 'gender', array( 'empty_value' => 'Choose a gender', )); } } Notez qu' la place d'instancier une nouvelle instance, vous pouvez simplement y faire rfrence grce l'alias utilis dans la configuration de votre service, gender. Amusez-vous ! PDF brought to you by generated on June 26, 2014 Chapter 44: Comment Crer un Type de Champ de Formulaire Personnalis | 173 Chapter 45 Comment crer une extension de type de formulaire Les types de formulaire personnaliss sont supers si vous avez besoin de types de champ qui font quelque chose de spcifique, comme un slecteur de civilit, ou un champ pour saisir la TVA. Mais parfois, vous n'avez pas vraiment besoin d'ajouter de nouveaux types de champ, vous voulez en fait ajouter de nouvelles fonctionnalits sur des types existant. C'est ici que les extensions de type entrent en jeu. Les extensions de type de formulaire ont deux utilisations principales : 1. Vous voulez ajouter une fonctionnalit gnrique sur plusieurs types (comme ajouter un texte d' aide sur tout les types de champ); 2. Vous voulez ajouter une fonctionnalit spcifique sur un type (comme ajouter une fonctionnalit tlchargement sur un type de champ file ). Dans ces deux cas, vous pourrez atteindre votre objectif en personnalisant l'affichage du formulaire, ou en personnalisant les types de champ. Mais utiliser les extensions de type de formulaire peut tre plus propre (en limitant la part de logique mtier dans les templates) et plus flexible (vous pouvez ajouter plusieurs extensions de type un seul type de formulaire)/ Les extensions de type de formulaire peuvent accomplir bien plus que ce que peuvent faire des types de champ personnaliss, mais au lieu d'tre eux-mmes des types de champ, ils se branchent sur des types existants. Imaginez que vous devez grer une entit Media, et que chaque mdia est associ un fichier. Votre formulaire Media utilise un type file, mais lorsque vous ditez l'entit, vous voulez avoir un aperu automatique de l'image affich ct du champ. Vous pourriez bien videmment faire cela en personnalisation la manire dont est affich le champ dans le template, mais les extensions de type de champ vous permettent de le faire sans rpter le code. PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 174 Listing 45-1 Listing 45-2 Dfinir l'extension de type de formulaire Votre premire tche est de crer la classe d'extension de type de formulaire. Appelons-la ImageTypeExtension. Par convention, les extensions de formulaire se trouvent habituellement dans le rpertoire Form\Extension de l'un de vos bundles. Lorsque vous crez une extension de type de formulaire, vous pouvez soit implmenter l'interface FormTypeExtensionInterface 1 , soit tendre la classe AbstractTypeExtension 2 . Dans la plupart des cas, il est plus simple d'tendre la classe abstraite: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php namespace Acme\DemoBundle\Form\Extension; use Symfony\Component\Form\AbstractTypeExtension; class ImageTypeExtension extends AbstractTypeExtension { /** * Retourne le nom du type de champ qui est tendu * * @return string Le nom du type qui est tendu */ public function getExtendedType() { return 'file'; } } La seule mthode que vous devez implmenter est la fonction getExtendedType. Elle est utilise pour spcifier le nom du type de formulaire qui est tendu par votre extension. La valeur que vous retournez dans la mthode getExtendedType correspond la valeur retourne par la mthode getName de la classe de type de formulaire que vous dsirez tendre. En plus de la fonction getExtendedType, vous allez probablement vouloir surcharger l'une des mthodes suivantes : buildForm() buildView() setDefaultOptions() finishView() Pour plus d'informations sur ce que ces mthodes font, vous pouvez lire l'article du Cookbook Crer des types de champ personnaliss. Enregistrer vos extensions de type de formulaire comme service La prochaine tape est d'indiquer Symfony que vous avez cr une extension. Tout ce que vous devez faire pour cela est de la dclarer comme service en utilisant le tag form.type_extension : 1. http://api.symfony.com/2.3/Symfony/Component/Form/FormTypeExtensionInterface.html 2. http://api.symfony.com/2.3/Symfony/Component/Form/AbstractTypeExtension.html PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 175 Listing 45-3 1 2 3 4 5 services: acme_demo_bundle.image_type_extension: class: Acme\DemoBundle\Form\Extension\ImageTypeExtension tags: - { name: form.type_extension, alias: file } La cl alias du tag est le type de champ sur lequel appliquer votre extension. Dans cet exemple, comme vous voulez tendre le type de champ file, vous utilisez file comme alias. Ajouter la logique mtier l'extension Le but de votre extension est d'afficher de jolies images ct des champs d'upload de fichier (quand le modle associ contient des images). Pour atteindre cet objectif, supposons que vous utilisez une approche similaire celle dcrite dans Comment grer l'upload de fichiers avec Doctrine: vous avez un modle Mdia avec une proprit file (qui correspond au champ file) et une proprit path (qui correspond au chemin de l'image dans la base de donnes): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // src/Acme/DemoBundle/Entity/Media.php namespace Acme\DemoBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; class Media { // ... /** * @var string Le chemin stock en base de donnes */ private $path; /** * @var \Symfony\Component\HttpFoundation\File\UploadedFile * @Assert\File(maxSize="2M") */ public $file; // ... /** * Retourne l'url de l'image * * @return null|string */ public function getWebPath() { // ... $webPath est l'url complte de l'image, qui est utilise dans le template return $webPath; } Votre classe d'extension de type de formulaire devra faire deux choses pour tendre le type de formulaire file : 1. Surcharger la mthode setDefaultOptions pour ajouter une option image_path; 2. Surcharger les mthodes buildForm et buildView pour passer l'url de l'image la vue. PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 176 Listing 45-4 La logique est la suivante : lorsque vous ajoutez un champ de formulaire du type file, vous pourrez alors spcifier une nouvelle option : image_path. Cette option indiquera au champ de fichier comment rcuprer l'url de l'image actuelle pour l'afficher dans la vue: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php namespace Acme\DemoBundle\Form\Extension; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class ImageTypeExtension extends AbstractTypeExtension { /** * Retourne le nom du type de champ qui est tendu * * @return string Le nom du type tendu */ public function getExtendedType() { return 'file'; } /** * Ajoute l'option image_path * * @param \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setOptional(array('image_path')); } /** * Passe l'url de l'image la vue * * @param \Symfony\Component\Form\FormView $view * @param \Symfony\Component\Form\FormInterface $form * @param array $options */ public function buildView(FormView $view, FormInterface $form, array $options) { if (array_key_exists('image_path', $options)) { $parentData = $form->getParent()->getData(); if (null !== $parentData) { $accessor = PropertyAccess::createPropertyAccessor(); $imageUrl = $accessor->getValue($parentData, $options['image_path']); } else { $imageUrl = null; } // dfinit une variable "image_url" qui sera disponible l'affichage du champ $view->vars['image_url'] = $imageUrl; } } } PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 177 Listing 45-5 Listing 45-6 Surcharger le fragment de template du widget File Chaque type de champ est affich grce un fragment de template. Ces fragments de templates peuvent tre surchargs pour personnaliser l'affichage du formulaire. Pour plus d'informations, vous pouvez consulter l'article Que sont les thmes de formulaire ?. Dans votre classe d'extension, vous avez ajout une nouvelle variable (image_url), mais vous n'avez pas encore tir profit de cette nouvelle variable dans vos templates. Spcifiquement, vous devez surcharger le bloc file_widget pour le faire : 1 2 3 4 5 6 7 8 9 10 11 12 13 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} {% extends 'form_div_layout.html.twig' %} {% block file_widget %} {% spaceless %} {{ block('form_widget') }} {% if image_url is not null %} <img src="{{ asset(image_url) }}"/> {% endif %} {% endspaceless %} {% endblock %} Vous devrez changer votre fichier de configuration ou spcifier explicitement que votre formulaire utilise un thme pour que Symfony utilise le bloc que vous avez surcharg. Pour plus d'informations, lisez Que sont les thmes de formulaire ?. Utiliser l'extension de type de formulaire A partir de maintenant, lorsque vous ajouterez un champ de type file dans un formulaire, vous pourrez spcifier l'option image_path qui sera utilise pour afficher une image ct du champ. Par exemple: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // src/Acme/DemoBundle/Form/Type/MediaType.php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class MediaType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', 'text') ->add('file', 'file', array('image_path' => 'webPath')); } public function getName() { return 'media'; } } PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 178 Lorsque vous afficherez le formulaire, si le modle sous-jacent a dj t associ une image, vous la verrez affiche ct du champ d'upload. PDF brought to you by generated on June 26, 2014 Chapter 45: Comment crer une extension de type de formulaire | 179 Listing 46-1 Listing 46-2 Chapter 46 Comment rduire la duplication de code avec "inherit_data" New in version 2.3: L'option inherit_data s'appelait virtual avant Symfony 2.3. L'option de champ de formulaire inherit_data peut tre trs utile si vous avez des champs dupliqus dans diffrentes entites. Par exemple, imaginez que vous avez deux entits, une Company et un Customer: 1 2 3 4 5 6 7 8 9 10 11 12 13 // src/Acme/HelloBundle/Entity/Company.php namespace Acme\HelloBundle\Entity; class Company { private $name; private $website; private $address; private $zipcode; private $city; private $country; } 1 2 3 4 5 6 7 8 // src/Acme/HelloBundle/Entity/Customer.php namespace Acme\HelloBundle\Entity; class Customer { private $firstName; private $lastName; PDF brought to you by generated on June 26, 2014 Chapter 46: Comment rduire la duplication de code avec "inherit_data" | 180 Listing 46-3 Listing 46-4 Listing 46-5 9 10 11 12 13 private $address; private $zipcode; private $city; private $country; } Comme vous pouvez le voir, chaque entit partage quelques champs communs : address, zipcode, city, country. Commencez par crer deux formulaires pour ces entits, CompanyType et CustomerType: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/HelloBundle/Form/Type/CompanyType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class CompanyType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', 'text') ->add('website', 'text'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/HelloBundle/Form/Type/CustomerType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\AbstractType; class CustomerType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('firstName', 'text') ->add('lastName', 'text'); } } Au lieu d'inclure les champs dupliqus address, zipcode, city et country dans les deux formulaires, crez un troisime formulaire appel LocationType pour cela: 1 2 3 4 5 6 7 8 9 10 // src/Acme/HelloBundle/Form/Type/LocationType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class LocationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) PDF brought to you by generated on June 26, 2014 Chapter 46: Comment rduire la duplication de code avec "inherit_data" | 181 Listing 46-6 Listing 46-7 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { $builder ->add('address', 'textarea') ->add('zipcode', 'text') ->add('city', 'text') ->add('country', 'text'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'inherit_data' => true )); } public function getName() { return 'location'; } } Le formulaire LocationType a une option intressante qui est dfinie, inherit_data. Cette option permet au formulaire d'hriter des donnes de son formulaire parent. S'il est imbriqu dans le formulaire CompanyType, les champs de LocationType accderont aux proprits de l'instance de Company. S'il est imbriqu dans le formulaire CustomerType, il accdera aux proprits de l'instance de Customer. Facile non ? Au lieu de dfinir l'option inherit_data dans LocationType, vous pouvez aussi (comme toute option) la passer comme troisime argument de $builder->add(). Finalement, ajoutez le formulaire LocationType vos deux formulaires originaux: 1 2 3 4 5 6 7 8 9 // src/Acme/HelloBundle/Form/Type/CompanyType.php public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->add('foo', new LocationType(), array( 'data_class' => 'Acme\HelloBundle\Entity\Company' )); } 1 2 3 4 5 6 7 8 9 // src/Acme/HelloBundle/Form/Type/CustomerType.php public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->add('bar', new LocationType(), array( 'data_class' => 'Acme\HelloBundle\Entity\Customer' )); } C'est tout! Vous avez extrait les dfinitions de champs dupliqus dans un formulaire spar que vous pouvez rutilis o vous le voulez. PDF brought to you by generated on June 26, 2014 Chapter 46: Comment rduire la duplication de code avec "inherit_data" | 182 Listing 47-1 Chapter 47 Comment tester unitairement vos formulaires Le composant Formulaire consiste en 3 objets: un type de formulaire (qui implmente FormTypeInterface 1 ), le Form 2 et la FormView 3 . La seule classe habituellement manipule par les programmeurs est la classe de type de formulaire qui sert de plan pour le formulaire. Elle est utilise pour gnrer le Formulaire et la vue (FormView). Vous pouvez la tester directement en bouchonnant les interactions avec la Factory mais cela risque d'tre complexe. Il est prfrable de la passer la FormFactory comme c'est le cas dans une vritable application. C'est simple initialiser et vous pouvez faire confiance aux composants de Symfony pour les utiliser dans vos tests. Il existe dj une classe dont vous pouvez tirer parti pour tester de simples types de champs : TypeTestCase 4 . Elle est utilise pour tester les types de champ de Symfony et vous pouvez l'utiliser pour tester vos propres types. New in version 2.3: La classe TypeTestCase a migr vers Symfony\Component\Form\Test dans Symfony 2.3. Auparavant, la classe tait situe dans Symfony\Component\Form\Tests\Extension\Core\Type. Selon la manire dont vous avez install Symfony ou le composant Form, les tests ne seront peut tre pas tlchargs. Utilisez l'option --prefer-source de Composer si c'est le cas. Les bases L'implmentation la plus simple de TypeTestCase ressemble : 1. http://api.symfony.com/2.3/Symfony/Component/Form/FormTypeInterface.html 2. http://api.symfony.com/2.3/Symfony/Component/Form/Form.html 3. http://api.symfony.com/2.3/Symfony/Component/Form/FormView.html 4. http://api.symfony.com/2.3/Symfony/Component/Form/Test/TypeTestCase.html PDF brought to you by generated on June 26, 2014 Chapter 47: Comment tester unitairement vos formulaires | 183 Listing 47-2 Listing 47-3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php namespace Acme\TestBundle\Tests\Form\Type; use Acme\TestBundle\Form\Type\TestedType; use Acme\TestBundle\Model\TestObject; use Symfony\Component\Form\Test\TypeTestCase; class TestedTypeTest extends TypeTestCase { public function testSubmitValidData() { $formData = array( 'test' => 'test', 'test2' => 'test2', ); $type = new TestedType(); $form = $this->factory->create($type); $object = new TestObject(); $object->fromArray($formData); // submit the data to the form directly $form->submit($formData); $this->assertTrue($form->isSynchronized()); $this->assertEquals($object, $form->getData()); $view = $form->createView(); $children = $view->children; foreach (array_keys($formData) as $key) { $this->assertArrayHasKey($key, $children); } } } Alors, qu'est ce que a teste? Voici une petite explication. Premirement, vous vrifiez que FormType compile. Cela inclut l'hritage de classe de base, la fonction buildForm et la rsolution d'options. Ce doit tre le premier test que vous crivez.: 1 2 $type = new TestedType(); $form = $this->factory->create($type); Ce test vrifie qu'aucune transformation de donnes utilise par le formulaire n'choue. La mthode isSynchronized() 5 est simplement dfinie false si une transformation de donnes lance une exception: 1 2 $form->submit($formData); $this->assertTrue($form->isSynchronized()); 5. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#isSynchronized() PDF brought to you by generated on June 26, 2014 Chapter 47: Comment tester unitairement vos formulaires | 184 Listing 47-4 Listing 47-5 Listing 47-6 Listing 47-7 Ne testez pas la validation : elle est applique par un couteur qui n'est pas actif dans le cas de test et est lie la configuration de validation. Au lieu de a, testez unitairement vos contraintes directement. Ensuite, vrifiez la soumission et le mapping du formulaire. Le test ci-dessous vrifie que tous les champs sont correctement spcifis: 1 $this->assertEquals($object, $form->getData()); Enfin, vrifiez la cration du FormView. Vous devriez virifier que tous les widgets que vous voulez afficher sont disponible dans la proprit children: 1 2 3 4 5 6 $view = $form->createView(); $children = $view->children; foreach (array_keys($formData) as $key) { $this->assertArrayHasKey($key, $children); } Ajouter un Type dont votre formulaire dpend Votre formulaire peut dpendre d'autres types qui sont dfinis comme services. Cela ressemblerait ceci: 1 2 3 4 // src/Acme/TestBundle/Form/Type/TestedType.php // ... the buildForm method $builder->add('acme_test_child_type'); Pour crer correctement votre formulaire, vous devez rendre le type disponible la Facotry dans votre test. La manire la plus simple est de l'enregistrer manuellement avant de crer le formulaire parent en utilisant la classe PreloadedExtension: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php namespace Acme\TestBundle\Tests\Form\Type; use Acme\TestBundle\Form\Type\TestedType; use Acme\TestBundle\Model\TestObject; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\PreloadedExtension; class TestedTypeTest extends TypeTestCase { protected function getExtensions() { $childType = new TestChildType(); return array(new PreloadedExtension(array( $childType->getName() => $childType, ), array())); } public function testSubmitValidData() { $type = new TestedType(); PDF brought to you by generated on June 26, 2014 Chapter 47: Comment tester unitairement vos formulaires | 185 Listing 47-8 22 23 24 25 26 $form = $this->factory->create($type); // ... your test } } Assurez vous que le type enfant que vous ajoutez est galement test. Autrement, vous pourriez avoir des erreurs qui ne sont pas lies au formulaire que vous testez mais ses enfants. Ajouter des Extensions spcifiques Il arrive souvent que vous utilisiez des options qui sont ajoutes par des extensions de formulaire. Cela peut tre, par exemple, ValidatorExtension avec son option invalid_message. Le TypeTestCase ne charge que les extensions de bases donc une exception "Invalid option" sera leve si vous tentez de l'utilisez dans une classe de test qui dpend d'autres extensions. Vous devez ajouter ces extensions l'objet Factory: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php namespace Acme\TestBundle\Tests\Form\Type; use Acme\TestBundle\Form\Type\TestedType; use Acme\TestBundle\Model\TestObject; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Forms; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; class TestedTypeTest extends TypeTestCase { protected function setUp() { parent::setUp(); $this->factory = Forms::createFormFactoryBuilder() ->addExtensions($this->getExtensions()) ->addTypeExtension( new FormTypeValidatorExtension( $this->getMock('Symfony\Component\Validator\ValidatorInterface') ) ) ->addTypeGuesser( $this->getMockBuilder( 'Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser' ) ->disableOriginalConstructor() ->getMock() ) ->getFormFactory(); $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); } PDF brought to you by generated on June 26, 2014 Chapter 47: Comment tester unitairement vos formulaires | 186 Listing 47-9 37 38 // ... your tests } Tester diffrents jeux de donnes Si vous n'tes pas familier avec le fournisseur de donnes 6 de PHPUnit, cela peut tre une bonne opportunit de l'utiliser: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php namespace Acme\TestBundle\Tests\Form\Type; use Acme\TestBundle\Form\Type\TestedType; use Acme\TestBundle\Model\TestObject; use Symfony\Component\Form\Test\TypeTestCase; class TestedTypeTest extends TypeTestCase { /** * @dataProvider getValidTestData */ public function testForm($data) { // ... your test } public function getValidTestData() { return array( array( 'data' => array( 'test' => 'test', 'test2' => 'test2', ), ), array( 'data' => array(), ), array( 'data' => array( 'test' => null, 'test2' => null, ), ), ); } } Le code ci-dessus lancera votre test trois fois avec trois jeux de donnes diffrents. Cela permet de dcoupler les donnes de test du test lui-mme et de tester facilement plusieurs jeux de donnes Vous pouvez galement passer un autre argument, comme un boolen si le formulaire doit tre synchronis avec le jeu de donnes ou non etc. 6. http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers PDF brought to you by generated on June 26, 2014 Chapter 47: Comment tester unitairement vos formulaires | 187 Listing 48-1 Listing 48-2 Chapter 48 Comment configurer des donnes vierges pour une classe de Formulaire L'option empty_data vous permet de spcifier un jeu de donnes vierges pour votre classe de formulaire. Ces donnes vierges seront utilises si vous soumettez votre formulaire, mais sans appeler la mthode setData() ou sans avoir initialis votre formulaire lors de sa cration. Par exemple: 1 2 3 4 5 6 7 8 9 10 11 12 public function indexAction() { $blog = ...; // $blog est pass lors de la cration, donc l'option empty_data option // n'est pas ncessaire $form = $this->createForm(new BlogType(), $blog); // Aucune donnes n'est passe, donc l'option empty_data est utilise pour // obtenir des donnes initiales $form = $this->createForm(new BlogType()); } Par dfaut, empty_data est dfinie null. Ou alors, si vous avez spcifi l'option data_class de votre classe de formulaire, l'option empty_data sera dfinie par dfaut comme une nouvelle instance de cette classe. L'instance sera cre en appelant le constructeur sans aucun argument. Si vous voulez surcharger ce comportement par dfaut, il y a deux manires de procder. Possibilit 1: Instancier une nouvelle classe L'une des raisons pour lesquelles vous voudrez utiliser cette possibilit est que vous voulez utiliser un constructeur en passant des arguments. Souvenez-vous, par dfaut, l'option data_class appelle le constructeur sans argument: PDF brought to you by generated on June 26, 2014 Chapter 48: Comment configurer des donnes vierges pour une classe de Formulaire | 188 Listing 48-3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // src/Acme/DemoBundle/Form/Type/BlogType.php // ... use Symfony\Component\Form\AbstractType; use Acme\DemoBundle\Entity\Blog; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class BlogType extends AbstractType { private $someDependency; public function __construct($someDependency) { $this->someDependency = $someDependency; } // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'empty_data' => new Blog($this->someDependency), )); } } Vous pouvez instancier votre classe comme vous le voulez. Dans cet exemple, nous passons une dpendance dans la classe BlogType au moment o elle est instancie, puis nous l'utilisons pour instancier un objet Blog. L'important est que vous pouvez dfinir empty_data comme tant un nouvel objet qui correspond exactement ce que vous voulez utiliser. Possibilit 2: Fournir une Closure Il est prfrable d'utiliser une closure car l'objet ne sera cr uniquement lorsqu'il sera ncessaire. La closure doit prendre une instance de FormInterface comme premier argument: 1 2 3 4 5 6 7 8 9 10 11 12 use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Form\FormInterface; // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'empty_data' => function (FormInterface $form) { return new Blog($form->get('title')->getData()); }, )); } PDF brought to you by generated on June 26, 2014 Chapter 48: Comment configurer des donnes vierges pour une classe de Formulaire | 189 Listing 49-1 Chapter 49 Comment utiliser la fonction submit() pour grer les soumissions de formulaires New in version 2.3: La mthode handleRequest() 1 a t ajoute dans Symfony 2.3. Avec la mthode handleRequest() il est trs facile de grer les soumissions de formulaires: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use Symfony\Component\HttpFoundation\Request; // ... public function newAction(Request $request) { $form = $this->createFormBuilder() // ... ->getForm(); $form->handleRequest($request); if ($form->isValid()) { // perform some action... return $this->redirect($this->generateUrl('task_success')); } return $this->render('AcmeTaskBundle:Default:new.html.twig', array( 'form' => $form->createView(), )); } 1. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#handleRequest() PDF brought to you by generated on June 26, 2014 Chapter 49: Comment utiliser la fonction submit() pour grer les soumissions de formulaires | 190 Listing 49-2 Listing 49-3 Pour en savoir plus sur cette mthode, lisez Grer la Soumission des Formulaires. Appeler Form::submit() manuellement New in version 2.3: Avant Symfony 2.3, la mthode submit() tait connue sous le nom de bind(). Dans certains cas, vous voudrez avoir plus de contrle sur le moment o votre formulaire est soumis et sur les donnes qui y sont passes. Au lieu d'utiliser la mthode handleRequest() 2 , passez directement les donnes soumises submit() 3 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use Symfony\Component\HttpFoundation\Request; // ... public function newAction(Request $request) { $form = $this->createFormBuilder() // ... ->getForm(); if ($request->isMethod('POST')) { $form->submit($request->request->get($form->getName())); if ($form->isValid()) { // perform some action... return $this->redirect($this->generateUrl('task_success')); } } return $this->render('AcmeTaskBundle:Default:new.html.twig', array( 'form' => $form->createView(), )); } Les formulaires qui contient des champs imbriqus attendent un tableau en argument de submit() 4 . Vous pouvez galement soumettre des champs individuels en appelant submit() 5 directement sur un champ: 1 $form->get('firstName')->submit('Fabien'); 2. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#handleRequest() 3. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#submit() 4. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#submit() 5. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#submit() PDF brought to you by generated on June 26, 2014 Chapter 49: Comment utiliser la fonction submit() pour grer les soumissions de formulaires | 191 Listing 49-4 Passer une requte Form::submit() (deprci) New in version 2.3: Avant Symfony 2.3, la mthode submit tait connue sous le nom de bind. Avant Symfony 2.3, la mthode submit() 6 acceptait un objet Request 7 . Un raccourci pratique de l'exemple prcdent: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use Symfony\Component\HttpFoundation\Request; // ... public function newAction(Request $request) { $form = $this->createFormBuilder() // ... ->getForm(); if ($request->isMethod('POST')) { $form->submit($request); if ($form->isValid()) { // perform some action... return $this->redirect($this->generateUrl('task_success')); } } return $this->render('AcmeTaskBundle:Default:new.html.twig', array( 'form' => $form->createView(), )); } Passer directement la Request 8 submit() 9 fonctionne toujours, mais c'est dprci et sera supprim dans Symfony 3.0. Vous devriez plutt utiliser handleRequest() 10 . 6. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#submit() 7. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html 8. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html 9. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#submit() 10. http://api.symfony.com/2.3/Symfony/Component/Form/FormInterface.html#handleRequest() PDF brought to you by generated on June 26, 2014 Chapter 49: Comment utiliser la fonction submit() pour grer les soumissions de formulaires | 192 Listing 50-1 Listing 50-2 Chapter 50 Comment utiliser l'Option de Champ de Formulaire Virtual L'option de champ de formulaire virtual peut tre trs utile quand vous avez des champs dupliqus dans diffrentes entits. Par exemple, imaginez que vous ayez deux entits, une Company et une Customer: 1 2 3 4 5 6 7 8 9 10 11 12 13 // src/Acme/HelloBundle/Entity/Company.php namespace Acme\HelloBundle\Entity; class Company { private $name; private $website; private $address; private $zipcode; private $city; private $country; } 1 2 3 4 5 6 7 8 9 10 11 12 13 // src/Acme/HelloBundle/Entity/Customer.php namespace Acme\HelloBundle\Entity; class Customer { private $firstName; private $lastName; private $address; private $zipcode; private $city; private $country; } PDF brought to you by generated on June 26, 2014 Chapter 50: Comment utiliser l'Option de Champ de Formulaire Virtual | 193 Listing 50-3 Listing 50-4 Listing 50-5 Comme vous pouvez le voir, chaque entit partage quelques champs qui sont identiques : address, zipcode, city, country. Maintenant, vous voulez construire deux formulaires : un pour l'entit Company et un autre pour l'entit Customer. Commencez par crer un CompanyType et un CustomerType: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/Acme/HelloBundle/Form/Type/CompanyType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; class CompanyType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', 'text') ->add('website', 'text'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/HelloBundle/Form/Type/CustomerType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class CustomerType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('firstName', 'text') ->add('lastName', 'text'); } } Maintenant, les quatre champs dupliqus doivent tre grs. Vous pouvez voir ci-dessous un (simple) type de formulaire location ( lieu en franais): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // src/Acme/HelloBundle/Form/Type/LocationType.php namespace Acme\HelloBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class LocationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('address', 'textarea') ->add('zipcode', 'string') ->add('city', 'string') ->add('country', 'text'); } PDF brought to you by generated on June 26, 2014 Chapter 50: Comment utiliser l'Option de Champ de Formulaire Virtual | 194 Listing 50-6 Listing 50-7 17 18 19 20 21 22 23 24 25 26 27 28 29 public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'virtual' => true, )); } public function getName() { return 'location'; } } Vous n'avez en fait pas de champ location dans vos entits, donc vous ne pouvez pas lier directement votre LocationType votre CompanyType ou votre CustomerType. Mais vous voulez absolument avoir un type de formulaire ddi pour grer le lieu (rappelez-vous, DRY - Don't Repeat Yourself!). L'option de champ de formulaire virtual est la solution. Vous pouvez dfinir l'option 'virtual' => true dans la mthode setDefaultOptions() de LocationType et commencer l'utiliser directement dans les deux types de formulaires initiaux. Voyez le rsultat: 1 2 3 4 5 6 7 // CompanyType public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('foo', new LocationType(), array( 'data_class' => 'Acme\HelloBundle\Entity\Company' )); } 1 2 3 4 5 6 7 // CustomerType public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('bar', new LocationType(), array( 'data_class' => 'Acme\HelloBundle\Entity\Customer' )); } Avec l'option virtual dfinie false (comportement par dfaut), le composant Form s'attend ce que chaque objet sous-jacent ait une proprit foo (ou bar) qui soit un objet ou un tableau contenant les quatre champs du lieu. Bien sr, vous n'avez pas cet objet/tableau dans vos entits et vous ne le voulez pas. Avec l'option virtual dfinie true , le composant Form ne s'occupe pas de la proprit foo (ou bar), et la place rcupre et dfinit ( gets et sets en anglais) les 4 champs du lieu directement sur l'objet sous-jacent. Au lieu de dfinir l'option virtual dans le type LocationType, vous pouvez (comme pour n'importe quelle autre option) aussi la passer comme une option sous forme de tableau en tant que troisime argument de $builder->add(). PDF brought to you by generated on June 26, 2014 Chapter 50: Comment utiliser l'Option de Champ de Formulaire Virtual | 195 Listing 51-1 Chapter 51 Comment utiliser Monolog pour crire des Logs Monolog 1 est une bibliothque pour PHP 5.3 servant crire des logs et utilise par Symfony2. Elle est inspire de la bibliothque Python LogBook. Utilisation Dans Monolog, chaque logger dfinit un canal de logging . Chaque canal possde une pile de gestionnaires pour crire les logs (les gestionnaires peuvent tre partags). Lorsque vous injectez le logger dans un service, vous pouvez utiliser un canal personnalis pour voir facilement quelle partie de l'application a logg le message. Le gestionnaire par dfaut est le StreamHandler qui crit les logs dans un stream (par dfaut dans le fichier app/logs/prod.log dans l'environnement de production et dans app/logs/dev.log dans l'environnement de dveloppement). Monolog est aussi livr avec un puissant gestionnaire intgr pour le logging en environnement de production : le FingersCrossedHandler. Il vous permet de stocker les messages dans un buffer ( mmoire tampon en franais) et de les crire dans le log que si un message atteint le niveau d'action (ERROR dans la configuration fournie dans l'dition standard) en transmettant les messages un autre gestionnaire. Pour logger un message, utilisez tout simplement le service logger depuis le conteneur dans un contrleur: 1. https://github.com/Seldaek/monolog PDF brought to you by generated on June 26, 2014 Chapter 51: Comment utiliser Monolog pour crire des Logs | 196 Listing 51-2 Listing 51-3 1 2 3 $logger = $this->get('logger'); $logger->info('Nous avons rcupr le logger'); $logger->error('Une erreur est survenue'); Utiliser uniquement les mthodes de l'interface LoggerInterface 2 permet de changer l'implmentation du logger sans changer votre code. Utiliser plusieurs gestionnaires Le logger utilise une pile de gestionnaires qui sont appels successivement. Ceci vous permet de logger facilement les messages de plusieurs manires. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # app/config/config*.yml monolog: handlers: syslog: type: stream path: /var/log/symfony.log level: error main: type: fingers_crossed action_level: warning handler: file file: type: stream level: debug La configuration ci-dessus dfinit une pile de gestionnaires qui vont tre appels dans l'ordre o ils sont dfinis. Le gestionnaire nomm file ne va pas tre inclus dans la pile elle-mme car il est utilis comme un gestionnaire imbriqu du gestionnaire fingers_crossed. Si vous voulez changer la configuration de MonologBundle dans un autre fichier de configuration, vous avez besoin de redfinir tout le bloc. Il ne peut pas tre fusionn car l'ordre importe et une fusion ne permet pas de contrler ce dernier. Changer la mise en forme Le gestionnaire utilise un Formatter pour mettre en forme une entre avant de la logger . Tous les gestionnaires Monolog utilisent une instance de Monolog\Formatter\LineFormatter par dfaut mais vous pouvez la remplacer facilement. Votre outil de mise en forme doit implmenter Monolog\Formatter\FormatterInterface. 1 2 # app/config/config.yml services: 2. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/Log/LoggerInterface.html PDF brought to you by generated on June 26, 2014 Chapter 51: Comment utiliser Monolog pour crire des Logs | 197 Listing 51-4 Listing 51-5 3 4 5 6 7 8 9 10 my_formatter: class: Monolog\Formatter\JsonFormatter monolog: handlers: file: type: stream level: debug formatter: my_formatter Ajouter des donnes supplmentaires dans les messages de log Monolog permet de traiter l'entre avant de la logger afin d'y ajouter des donnes supplmentaires. Un processeur peut tre appliqu pour la pile entire des gestionnaires ou uniquement pour un gestionnaire spcifique. Un processeur est simplement un callable recevant l'entre log en tant que son premier argument. Les processeurs sont configurs en utilisant la balise DIC monolog.processor. Voir la rfrence propos de celle-ci. Ajouter un jeton de Session/Requte Parfois il est difficile de dire quelles entres dans le log appartiennent quelle session et/ou requte. L'exemple suivant va ajouter un jeton unique pour chaque requte en utilisant un processeur. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 namespace Acme\MyBundle; use Symfony\Component\HttpFoundation\Session\Session; class SessionRequestProcessor { private $session; private $token; public function __construct(Session $session) { $this->session = $session; } public function processRecord(array $record) { if (null === $this->token) { try { $this->token = substr($this->session->getId(), 0, 8); } catch (\RuntimeException $e) { $this->token = '????????'; } $this->token .= '-' . substr(uniqid(), -8); } $record['extra']['token'] = $this->token; return $record; } } PDF brought to you by generated on June 26, 2014 Chapter 51: Comment utiliser Monolog pour crire des Logs | 198 # app/config/config.yml services: monolog.formatter.session_request: class: Monolog\Formatter\LineFormatter arguments: - "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n" monolog.processor.session_request: class: Acme\MyBundle\SessionRequestProcessor arguments: [ @session ] tags: - { name: monolog.processor, method: processRecord } monolog: handlers: main: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug formatter: monolog.formatter.session_request Si vous utilisez plusieurs gestionnaires, vous pouvez aussi dclarer le processeur au niveau du gestionnaire au lieu de le faire globalement. PDF brought to you by generated on June 26, 2014 Chapter 51: Comment utiliser Monolog pour crire des Logs | 199 Listing 52-1 Chapter 52 Comment configurer Monolog pour envoyer les erreurs par Email Monolog 1 peut tre configur pour envoyer un email lorsqu'une erreur se produit dans une application. Pour ce faire, la configuration ncessite quelques gestionnaires imbriqus afin d'viter de recevoir trop d'emails. Cette configuration parat complique en premier lieu mais chaque gestionnaire est facilement comprhensible lorsqu'on les analyse un par un. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # app/config/config.yml monolog: handlers: mail: type: fingers_crossed action_level: critical handler: buffered buffered: type: buffer handler: swift swift: type: swift_mailer from_email: [email protected] to_email: [email protected] subject: An Error Occurred! level: debug Le gestionnaire mail est un gestionnaire fingers_crossed, ce qui signifie qu'il est dclench uniquement lorsque le niveau d'action, dans notre cas critical est atteint. Il crit alors des logs pour tout, incluant les messages en dessous du niveau d'action. Le niveau critical est dclench seulement pour les erreurs HTTP de code 5xx. Le paramtre handler signifie que la sortie ( output en anglais) est alors passe au gestionnaire buffered. 1. https://github.com/Seldaek/monolog PDF brought to you by generated on June 26, 2014 Chapter 52: Comment configurer Monolog pour envoyer les erreurs par Email | 200 Listing 52-2 Si vous souhaitez que les erreurs de niveau 400 et 500 dclenchent un email, dfinissez action_level avec la valeur error la place de critical. Le gestionnaire buffered garde simplement tous les messages pour une requte et les passe ensuite au gestionnaire imbriqu en une seule fois. Si vous n'utilisez pas ce gestionnaire alors chaque message sera envoy par email sparment. C'est donc le gestionnaire swift qui prend le relais. C'est ce dernier qui se charge de vous envoyer l'erreur par email. Ses paramtres sont simples : to ( en franais), from ( de en franais) et le sujet. Vous pouvez combiner ces gestionnaires avec d'autres afin que les erreurs continuent d'tre logues sur le serveur en mme temps qu'elles sont envoyes par email : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # app/config/config.yml monolog: handlers: main: type: fingers_crossed action_level: critical handler: grouped grouped: type: group members: [streamed, buffered] streamed: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug buffered: type: buffer handler: swift swift: type: swift_mailer from_email: [email protected] to_email: [email protected] subject: An Error Occurred! level: debug Cette configuration utilise le gestionnaire group pour envoyer les messages aux deux membres du groupe, les gestionnaires buffered et stream. Les messages vont donc maintenant tre crits dans le fichier log et envoys par email. PDF brought to you by generated on June 26, 2014 Chapter 52: Comment configurer Monolog pour envoyer les erreurs par Email | 201 Listing 53-1 Chapter 53 Comment loguer des messages dans diffrents fichiers New in version 2.1: La possibilit de spcifier des canaux pour un gestionnaire spcifique a t ajoute au MonologBundle dans Symfony 2.1. L'Edition Standard de Symfony contient plusieurs canaux pour le logging : doctrine, event, security et request. Chaque canal correspond un service de logger (monolog.logger.XXX) dans le conteneur et est inject dans le service concern. Le but des canaux est d'tre capable d'organiser diffrents types de messages de logging. Par dfaut, Symfony2 logue tous les messages dans un fichier unique (peu importe le canal). Transfrer un canal vers un gestionnaire diffrent Maintenant, supposons que vous vouliez loguer le canal doctrine dans un fichier diffrent. Pour faire cela, crez simplement un nouveau gestionnaire et configurez-le comme ceci : 1 2 3 4 5 6 7 8 9 10 monolog: handlers: main: type: stream path: /var/log/symfony.log channels: !doctrine doctrine: type: stream path: /var/log/doctrine.log channels: doctrine PDF brought to you by generated on June 26, 2014 Chapter 53: Comment loguer des messages dans diffrents fichiers | 202 Listing 53-2 Spcification Yaml Vous pouvez spcifier la configuration de diffrentes faons : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 channels: ~ # Inclus tous les canaux channels: foo # Inclus seulement le canal "foo" channels: !foo # Inclus tous les canaux, except "foo" channels: [foo, bar] # Inclus seulement les canaux "foo" et "bar" channels: [!foo, !bar] # Inclus tous les canaux, except "foo" et "bar" channels: type: inclusive # Inclus seulement ceux lists ci-dessous elements: [ foo, bar ] channels: type: exclusive # Inclus tous, except ceux lists ci-dessous elements: [ foo, bar ] Crer votre propre Canal Vous pouvez remplacer les logs du canal de monolog par un service la fois. Cela s'effectue en ajoutant un tag monolog.logger votre service et en spcifiant dans quel canal le service devrait loguer. En faisant cela, le logger qui est inject dans ce service est prconfigur pour utiliser le canal que vous avez spcifi. Pour plus d'informations - incluant un exemple complet - lisez monolog.logger dans la section Tags d'Injection de Dpendance du document de rfrence. En savoir plus grce au Cookbook Comment utiliser Monolog pour crire des Logs PDF brought to you by generated on June 26, 2014 Chapter 53: Comment loguer des messages dans diffrents fichiers | 203 Listing 54-1 Chapter 54 Comment crer un Collecteur de Donnes personnalis Le Profiler de Symfony2 dlgue la collection de donnes aux collecteurs de donnes. Certains d'entre eux sont fournis avec Symfony2, mais vous pouvez facilement crer le vtre. Crer un Collecteur de Donnes Personnalis Crer un collecteur de donnes personnalis est aussi simple que d'implmenter la classe DataCollectorInterface 1 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface DataCollectorInterface { /** * Collects data for the given Request and Response. * * @param Request $request A Request instance * @param Response $response A Response instance * @param \Exception $exception An Exception instance */ function collect(Request $request, Response $response, \Exception $exception = null); /** * Returns the name of the collector. * * @return string The collector name */ function getName(); } La mthode getName() doit retourner un nom unique. Ceci est utilis pour accder l'information plus tard (voir Comment utiliser le Profiler dans un test fonctionnel par exemple). 1. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.html PDF brought to you by generated on June 26, 2014 Chapter 54: Comment crer un Collecteur de Donnes personnalis | 204 Listing 54-2 Listing 54-3 Listing 54-4 La mthode collect() est responsable de stocker les donnes auxquelles elle veut donner accs dans des proprits locales. Comme le profiler srialise les instances de collecteur de donnes, vous ne devriez pas stocker des objets ne pouvant pas tre srialiss (comme des objets PDO), ou vous devrez fournir votre propre mthode serialize(). La plupart du temps, il est pratique d'tendre la classe DataCollector 2 et de remplir la proprit $this- >data (elle se charge de srialiser la proprit $this->data): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class MemoryDataCollector extends DataCollector { public function collect(Request $request, Response $response, \Exception $exception = null) { $this->data = array( 'memory' => memory_get_peak_usage(true), ); } public function getMemory() { return $this->data['memory']; } public function getName() { return 'memory'; } } Activer les Collecteurs de Donnes Personnaliss Pour activer un collecteur de donnes, ajoutez le comme un service ordinaire dans l'une de vos configurations, et taggez le avec data_collector : 1 2 3 4 5 services: data_collector.your_collector_name: class: Fully\Qualified\Collector\Class\Name tags: - { name: data_collector } Ajouter des Templates de Profiler Web Quand vous voulez afficher les donnes collectes par votre Collecteur de Donnes dans la barre d'outils de dbuggage ou dans le profiler web, crez un template Twig en vous appuyant sur l'exemple suivant : 1 2 {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} 2. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/DataCollector/DataCollector.html PDF brought to you by generated on June 26, 2014 Chapter 54: Comment crer un Collecteur de Donnes personnalis | 205 Listing 54-5 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {% block toolbar %} {# le contenu de la barre d'outils de dbuggage web #} {% endblock %} {% block head %} {# si le panel du profiler web ncessite des fichiers JS ou CSS spcifiques #} {% endblock %} {% block menu %} {# le contenu du menu #} {% endblock %} {% block panel %} {# le contenu du panel #} {% endblock %} Chaque bloc est optionnel. Le bloc toolbar est utilis pour la barre d'outils de dbuggage web et les blocs menu et panel sont utiliss pour ajouter un panel au profiler web. Tous les blocs ont accs l'objet collector. Les templates intgrs utilisent une image encode en base64 pour la barre d'outils (<img src="src="data:image/png;base64,..."). Vous pouvez facilement calculer la valeur en base64 d'une image avec ce petit script : echo base64_encode(file_get_contents($_SERVER['argv'][1]));. Pour activer le template, ajoutez un attribut template au tag data_collector dans votre configuration. Par exemple, en supposant que votre template est dans un AcmeDebugBundle : 1 2 3 4 5 services: data_collector.your_collector_name: class: Acme\DebugBundle\Collector\Class\Name tags: - { name: data_collector, template: "AcmeDebugBundle:Collector:templatename", id: "your_collector_name" } PDF brought to you by generated on June 26, 2014 Chapter 54: Comment crer un Collecteur de Donnes personnalis | 206 Listing 55-1 Chapter 55 Comment dclarer un nouveau Format de Requte et un Type Mime Chaque Requte a un format (par exemple : html, json), qui est utilis pour dterminer quel type de contenu retourner dans la Rponse. En fait, le format de la requte, accessible via la mthode getRequestFormat() 1 , est utilis pour dfinir le type MIME de l'en-tte Content-Type de l'objet Response. En interne, Symfony contient un tableau des formats les plus communs (par exemple : text/ html, application/json). Bien sr, des types de format MIME additionnels peuvent aisment tre ajouts. Ce document va vous montrer comment vous pouvez ajouter le format jsonp ainsi que le type MIME correspondant. Crez un Listener kernel.request La solution pour dfinir un nouveau type MIME est de crer une classe qui va couter l'vnement kernel.request dispatch ( rparti en franais) par le kernel de Symfony. L'vnement kernel.request est dispatch trs tt dans le processus de gestion de la requte de Symfony et vous permet de modifier l'objet requte. Crez la classe suivante, en remplaant le chemin par un chemin vers un bundle de votre projet: 1 2 3 4 5 6 7 8 9 10 11 // src/Acme/DemoBundle/RequestListener.php namespace Acme\DemoBundle; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; class RequestListener { public function onKernelRequest(GetResponseEvent $event) { $event->getRequest()->setFormat('jsonp', 'application/javascript'); 1. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html#getRequestFormat() PDF brought to you by generated on June 26, 2014 Chapter 55: Comment dclarer un nouveau Format de Requte et un Type Mime | 207 Listing 55-2 12 13 } } Dclarer votre Listener Comme pour n'importe quel autre listener, vous avez besoin de l'ajouter dans l'un de vos fichiers de configuration et de le dclarer comme listener en ajoutant le tag kernel.event_listener : 1 2 3 4 5 6 7 8 9 10 11 12 <!-- app/config/config.xml --> <?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="acme.demobundle.listener.request" class="Acme\DemoBundle\RequestListener"> <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" /> </service> </services> </container> A ce stade, le service acme.demobundle.listener.request a t configur et sera notifi lorsque le kernel de Symfony dispatchera l'vnement kernel.request. Vous pouvez aussi dclarer le listener dans une configuration de classe extension (voir Importer la Configuration via les Extensions de Conteneur pour plus d'informations). PDF brought to you by generated on June 26, 2014 Chapter 55: Comment dclarer un nouveau Format de Requte et un Type Mime | 208 Listing 56-1 Listing 56-2 Chapter 56 Comment forcer les routes toujours utiliser HTTPS ou HTTP Quelquefois, vous voulez scuriser certaines routes et tre sr qu'on y accde toujours via le protocole HTTPS. Le composant Routing vous permet de forcer le systme de l'URI via la condition requise _scheme : 1 2 3 4 5 secure: pattern: /secure defaults: { _controller: AcmeDemoBundle:Main:secure } requirements: _scheme: https La configuration ci-dessus force la route nomme secure toujours utiliser HTTPS. Pendant la gnration de l'URL de secure, et si le systme actuel est HTTP, Symfony va automatiquement gnrer une URL absolue avec HTTPS comme scheme : 1 2 3 4 5 6 7 {# Si le scheme actuel est HTTPS #} {{ path('secure') }} {# gnre /secure {# Si le scheme actuel est HTTP #} {{ path('secure') }} {# gnre https://example.com/secure #} La condition requise est aussi force pour les requtes entrantes. Si vous essayez d'accder au chemin /secure avec HTTP, vous serez automatiquement redirig la mme URL, mais avec le scheme HTTPS. Les exemples ci-dessus utilisent https en tant que _scheme, mais vous pouvez aussi forcer une URL toujours utiliser http. PDF brought to you by generated on June 26, 2014 Chapter 56: Comment forcer les routes toujours utiliser HTTPS ou HTTP | 209 Le composant Security fournit une autre faon d'imposer HTTP ou HTTPS via le paramtre requires_channel. Cette mthode alternative est mieux adapte pour scuriser une zone de votre site web (toutes les URLs dans la zone /admin) ou pour scuriser les URLs dfinies dans un bundle tiers. PDF brought to you by generated on June 26, 2014 Chapter 56: Comment forcer les routes toujours utiliser HTTPS ou HTTP | 210 Listing 57-1 Chapter 57 Comment autoriser un caractre / dans un paramtre de route Parfois, on a besoin de construire des URLs avec des paramtres qui peuvent contenir un slash /. Prenons par exemple la route classique /hello/{name}. Par dfaut, /hello/Fabien va correspondre cette route mais pas /hello/Fabien/Kris. Cela est d au fait que Symfony utilise ce caractre comme sparateur entre les parties de la route. Ce guide explique comment vous pouvez modifier une route afin que /hello/Fabien/Kris corresponde la route /hello/{name}, o {name} quivaut Fabien/Kris. Configurer la Route Par dfaut, les composants de routage de Symfony requirent que les paramtres correspondent au pattern de regex suivant : [^/]+. Cela veut dire que tous les caractres sont autoriss sauf /. Vous devez explicitement autoriser le caractre / faire partie de votre paramtre en spcifiant un pattern de regex plus permissif. 1 2 3 4 5 _hello: pattern: /hello/{name} defaults: { _controller: AcmeDemoBundle:Demo:hello } requirements: name: ".+" C'est tout ! Maintenant, le paramtre {name} peut contenir le caractre /. PDF brought to you by generated on June 26, 2014 Chapter 57: Comment autoriser un caractre / dans un paramtre de route | 211 Listing 58-1 Chapter 58 Comment configurer une redirection vers une autre route sans contrleur personnalis Ce guide explique comment configurer une redirection d'une route vers une autre sans utiliser de contrleur spcifique. Supposez qu'il n'existe pas de contrleur par dfaut pour l'URL / de votre application et que vous voulez rediriger ces requtes vers /app. Votre configuration ressemblerait ceci : 1 2 3 4 5 6 7 8 9 10 11 AppBundle: resource: "@App/Controller/" type: annotation prefix: /app root: pattern: / defaults: _controller: FrameworkBundle:Redirect:urlRedirect path: /app permanent: true Dans cet exemple, vous configurez une route pour le chemin / et laissez la classe RedirectController 1 la grer. Ce contrleur est fourni avec Symfony et propose deux actions pour rediriger les requtes : urlRedirect redirige vers un autre chemin. Vous devez spcifier le paramtre path pour qu'il contienne le chemin vers la ressource vers laquelle vous voulez rediriger. redirect (pas montr ici) redirige vers une autre route. Vous devez dfinir le paramtre route avec le nom de la route vers laquelle vous voulez rediriger. Le paramtre permanent indique aux deux mthodes de retourner un code de statut HTTP 301 au lieu du code 302 par dfaut. 1. http://api.symfony.com/2.3/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.html PDF brought to you by generated on June 26, 2014 Chapter 58: Comment configurer une redirection vers une autre route sans contrleur personnalis | 212 Listing 59-1 Listing 59-2 Chapter 59 Comment utiliser des mthodes HTTP autres que GET et POST dans les routes La mthode HTTP d'une requte est l'un des prrequis qui peuvent tre vrifis pour valider si une route correspond ou pas. Cela est abord dans le chapitre sur le routage du Book Routage avec des exemples qui utilisent les mthodes GET et POST. Vous pouvez galement utiliser d'autres mthodes HTTP de la mme manire. Par exemple, si vous avez un billet de blog, alors vous pourriez utiliser le mme schma d'URL pour l'afficher, le modifier et le supprimer grce aux mthodes GET, PUT et DELETE. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 blog_show: pattern: /blog/{slug} defaults: { _controller: AcmeDemoBundle:Blog:show } requirements: _method: GET blog_update: pattern: /blog/{slug} defaults: { _controller: AcmeDemoBundle:Blog:update } requirements: _method: PUT blog_delete: pattern: /blog/{slug} defaults: { _controller: AcmeDemoBundle:Blog:delete } requirements: _method: DELETE Malheureusement, la vie n'est pas toujours si simple puisque plusieurs navigateurs ne supportent pas l'envoi de requtes PUT et DELETE. Heureusement, Symfony2 vous fournit de manire simple de contourner cette limitation. En incluant le paramtre _method dans la chane de caractres de la requte, ou dans les paramtres d'une requte HTTP, Symfony l'utilisera comme mthode pour trouver une route correspondante. Cela peut tre fait trs facilement dans les formulaires grce un champ cach. Supposons que vous ayez un formulaire pour diter un billet de blog : PDF brought to you by generated on June 26, 2014 Chapter 59: Comment utiliser des mthodes HTTP autres que GET et POST dans les routes | 213 Listing 59-3 1 2 3 4 5 <form action="{{ path('blog_update', {'slug': blog.slug}) }}" method="post"> <input type="hidden" name="_method" value="PUT" /> {{ form_widget(form) }} <input type="submit" value="Update" /> </form> La requte soumise correspondra maintenant la route blog_update et l'action updateAction sera utilise pour traiter le formulaire. De la mme manire, le formulaire de suppression peut tre modifi pour ressembler ceci : 1 2 3 4 5 <form action="{{ path('blog_delete', {'slug': blog.slug}) }}" method="post"> <input type="hidden" name="_method" value="DELETE" /> {{ form_widget(delete_form) }} <input type="submit" value="Delete" /> </form> Alors, la route blog_delete sera utilise. PDF brought to you by generated on June 26, 2014 Chapter 59: Comment utiliser des mthodes HTTP autres que GET et POST dans les routes | 214 Listing 60-1 Listing 60-2 Listing 60-3 Chapter 60 Comment utiliser des paramtres du conteneur de services dans vos routes New in version 2.1: La possibilit d'utiliser ces paramtres dans vos routes est une nouveaut de Symfony 2.1. Parfois, vous pouvez trouver utile de rendre certaines parties de vos routes configurables de faon globale. Par exemple, si vous construisez un site internationalis, vous commencerez probablement pas une ou deux locales. Vous ajouterez certainement un prrequis dans vos routes pour empcher l'utilisateur d'accder une autre locale que celles que vous supportez. Vous pourriez coder en dure votre prrequis _locale dans toutes vos routes. Mais une meilleure solution sera d'utiliser un paramtre configurable du conteneur de services dans la configuration de votre routage : contact: pattern: /{_locale}/contact defaults: { _controller: AcmeDemoBundle:Main:contact } requirements: _locale: %acme_demo.locales% Vous pouvez maintenant contrler et dfinir le paramtre acme_demo.locales quelque part dans votre conteneur : 1 2 3 # app/config/config.yml parameters: acme_demo.locales: en|es Vous pouvez aussi utiliser un paramtre pour dfinir votre schma de route (ou une partie de votre schma) : PDF brought to you by generated on June 26, 2014 Chapter 60: Comment utiliser des paramtres du conteneur de services dans vos routes | 215 1 2 3 some_route: pattern: /%acme_demo.route_prefix%/contact defaults: { _controller: AcmeDemoBundle:Main:contact } Tout comme dans les fichiers de configuration classiques du conteneur, si vous avez besoin d'un % dans votre route, vous pouvez chapper le signe popurcentage en le doublant. Par exemple, /score-50%% deviendra /score-50%. PDF brought to you by generated on June 26, 2014 Chapter 60: Comment utiliser des paramtres du conteneur de services dans vos routes | 216 Chapter 61 Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) La couche de scurit est l'un des outils les plus intelligents de Symfony. Il gre deux choses : les procds d'authentification et d'autorisation. Bien qu'il puisse tre difficile de comprendre comment cela fonctionne en interne, le systme de scurit est trs flexible et vous permet d'intgrer votre application avec n'importe quel backend d'authentification, comme Active Directory, un serveur OAuth ou une base de donnes. Introduction Cet article traite de l'authentification des utilisateurs travers une table de base de donnes gre par une classe entit Doctrine. Le contenu de cet article du Cookbook est divis en trois parties. La premire partie parle de la conception d'une classe entit Doctrine User ainsi que du fait de la rendre utilisable par la couche de scurit de Symfony. La deuxime partie dcrit comment authentifier facilement un utilisateur avec l'objet Doctrine EntityUserProvider 1 fourni avec le framework et quelques lments de configurations. Finalement, le tutoriel va dmontrer comment crer un objet personnalis EntityUserProvider 2 pour rcuprer des utilisateurs depuis la base de donnes sous certaines conditions personnalises. Ce tutoriel suppose qu'il existe un bundle Acme\UserBundle dmarr et charg dans le kernel de l'application. Le Modle de Donnes Pour cet article du cookbook, le bundle AcmeUserBundle contient une classe entit User avec les champs suivants : id, username, salt, password, email et isActive. Le champ isActive prcise si oui ou non le compte de l'utilisateur est activ. 1. http://api.symfony.com/2.3/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html 2. http://api.symfony.com/2.3/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 217 Listing 61-1 Pour faire court, les mthodes getter et setter de chacun des champs ont t supprimes pour se concentrer sur les mthodes les plus importantes qui proviennent de l'interface UserInterface 3 . 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 // src/Acme/UserBundle/Entity/User.php namespace Acme\UserBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; /** * Acme\UserBundle\Entity\User * * @ORM\Table(name="acme_users") * @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository") */ class User implements UserInterface, \Serializable { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length=25, unique=true) */ private $username; /** * @ORM\Column(type="string", length=32) */ private $salt; /** * @ORM\Column(type="string", length=40) */ private $password; /** * @ORM\Column(type="string", length=60, unique=true) */ private $email; /** * @ORM\Column(name="is_active", type="boolean") */ private $isActive; public function __construct() { $this->isActive = true; $this->salt = md5(uniqid(null, true)); } /** * @inheritDoc */ 3. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 218 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public function getUsername() { return $this->username; } /** * @inheritDoc */ public function getSalt() { return $this->salt; } /** * @inheritDoc */ public function getPassword() { return $this->password; } /** * @inheritDoc */ public function getRoles() { return array('ROLE_USER'); } /** * @inheritDoc */ public function eraseCredentials() { } /** * @see \Serializable::serialize() */ public function serialize() { return serialize(array( $this->id, )); } /** * @see \Serializable::unserialize() */ public function unserialize($serialized) { list ( $this->id, ) = unserialize($serialized); } } Afin d'utiliser une instance de la classe AcmeUserBundle:User dans la couche de scurit de Symfony, la classe entit doit implmenter l'interface UserInterface 4 . Cette interface force la classe implmenter PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 219 Listing 61-2 Listing 61-3 les cinq mthodes suivantes : * getRoles(), * getPassword(), * getSalt(), * getUsername(), * eraseCredentials() Pour plus de dtails sur chacune d'entre elles, voir UserInterface 5 . L'interface Serializable 6 ainsi que ses mthodes serialize et unserialize ont t ajoutes pour permettre la classe User d'tre srialisable dans la session. Cela peut ou non tre ncessaire en fonction de votre configuration, mais c'est certainement une bonne ide. Seule la proprit id a besoin d'tre srialise, car la mthode refreshUser() 7 recharge l'utilisateur chaque requte en utilisant la proprit id. 1 2 3 4 5 6 7 8 9 10 11 // src/Acme/UserBundle/Entity/User.php namespace Acme\UserBundle\Entity; use Symfony\Component\Security\Core\User\EquatableInterface; // ... public function isEqualTo(UserInterface $user) { return $this->username === $user->getUsername(); } Voici, ci-dessous, un export de ma table User depuis MySQL. Pour plus de dtails sur la cration des entres utilisateur et l'encodage de leur mot de passe, lisez le chapitre Encoder les mots de passe. 1 2 3 4 5 6 7 8 9 10 mysql> select * from user; +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ | id | username | salt | password | email | is_active | +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ | 1 | hhamon | 7308e59b97f6957fb42d66f894793079 | 09610f61637408828a35d7debee5b38a8350eebe | [email protected] | 1 | | 2 | jsmith | ce617a6cca9126bf4036ca0c02e82dee | 8390105917f3a3d533815250ed7c64b4594d7ebf | [email protected] | 1 | | 3 | maxime | cd01749bb995dc658fa56ed45458d807 | 9764731e5f7fb944de5fd8efad4949b995b72a3c | [email protected] | 0 | | 4 | donald | 6683c2bfd90c0426088402930cadd0f8 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | [email protected] | 1 | +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ 4 rows in set (0.00 sec) La base de donnes contient dsormais quatre utilisateurs avec diffrents noms d'utilisateurs, emails et statuts. La prochaine partie va traiter de l'authentification de l'un de ces utilisateurs grce au fournisseur d'entit utilisateur Doctrine et quelques lignes de configuration. Authentifier quelqu'un travers une base de donnes Authentifier un utilisateur Doctrine travers une base de donnes avec la couche de scurit de Symfony est vraiment trs facile. Tout rside dans la configuration du SecurityBundle stocke dans le fichier app/ config/security.yml. Vous trouverez ci-dessous un exemple de configuration o l'utilisateur va entrer son nom d'utilisateur et son mot de passe via une authentification basique HTTP. Cette information sera alors compare et vrifie avec nos entres d'entit User de la base de donnes : 4. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html 5. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html 6. http://php.net/manual/en/class.serializable.php 7. http://api.symfony.com/2.3/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html#refreshUser() PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 220 Listing 61-4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # app/config/security.yml security: encoders: Acme\UserBundle\Entity\User: algorithm: sha1 encode_as_base64: false iterations: 1 role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ] providers: administrators: entity: { class: AcmeUserBundle:User, property: username } firewalls: admin_area: pattern: ^/admin http_basic: ~ access_control: - { path: ^/admin, roles: ROLE_ADMIN } La section encoders associe l'encodeur de mot de passe sha1 la classe entit. Cela signifie que Symfony va s'attendre ce que le mot de passe stock dans la base de donnes soit encod l'aide de cet algorithme. Pour plus de dtails sur la cration d'un nouvel objet User avec un mot de passe encrypt correctement, lisez la section Encoder les mots de passe du chapitre sur la scurit. La section providers dfinit un fournisseur d'utilisateur administrators. Un fournisseur d'utilisateur est une source indiquant o les utilisateurs sont chargs lors de l'authentification. Dans ce cas, le mot-cl entity signifie que Symfony va utiliser le fournisseur d'entit utilisateur Doctrine pour charger des objets entit User depuis la base de donnes en utilisant le champ unique username. En d'autres termes, cela indique Symfony comment rcuprer l'utilisateur depuis la base de donnes avant de vrifier la validit du mot de passe. Ce code et cette configuration fonctionnent mais ce n'est pas suffisant pour scuriser l'application pour des utilisateurs activs. En effet, maintenant, vous pouvez toujours vous authentifier avec maxime. La section suivante explique comment interdire l'accs aux utilisateurs non-activs. Interdire les Utilisateurs non-activs La manire la plus facile d'exclure des utilisateurs non-activs est d'implmenter l'interface AdvancedUserInterface 8 qui se charge de vrifier le statut du compte de l'utilisateur. L'interface AdvancedUserInterface 9 tend l'interface UserInterface 10 , donc vous devez simplement utiliser la nouvelle interface dans la classe entit AcmeUserBundle:User afin de bnficier de comportements d'authentification simples et avancs. L'interface AdvancedUserInterface 11 ajoute quatre mthodes supplmentaires pour valider le statut du compte : isAccountNonExpired() vrifie si le compte de l'utilisateur a expir, 8. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/AdvancedUserInterface.html 9. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/AdvancedUserInterface.html 10. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html 11. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/AdvancedUserInterface.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 221 Listing 61-5 isAccountNonLocked() vrifie si l'utilisateur est verrouill, isCredentialsNonExpired() vrifie si les informations de connexion de l'utilisateur (mot de passe) ont expir, isEnabled() vrifie si l'utilisateur est activ. Pour cet exemple, les trois premires mthodes vont retourner true alors que la mthode isEnabled() va retourner la valeur boolenne du champ isActive. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // src/Acme/UserBundle/Entity/User.php namespace Acme\UserBundle\Entity; // ... use Symfony\Component\Security\Core\User\AdvancedUserInterface; class User implements AdvancedUserInterface, \Serializable { // ... public function isAccountNonExpired() { return true; } public function isAccountNonLocked() { return true; } public function isCredentialsNonExpired() { return true; } public function isEnabled() { return $this->isActive; } } Si vous essayez de vous authentifier avec maxime, l'accs est maintenant interdit puisque cet utilisateur n'a pas un compte activ. La prochaine section va se concentrer sur l'implmentation d'un fournisseur d'entit personnalis pour authentifier un utilisateur avec son nomd'utilisateur ou avec son adresse email. Authentifier quelqu'un avec un fournisseur d'entit personnalis La prochaine tape est de permettre un utilisateur de s'authentifier avec son nom d'utilisateur ou avec son adresse email comme ils sont tous les deux uniques dans la base de donnes. Malheureusement, le fournisseur d'entit natif est seulement capable de grer une proprit unique pour rcuprer l'utilisateur depuis la base de donnes. Pour raliser ceci, crez un fournisseur d'entit personnalis qui cherche un utilisateur dont le champ nom d'utilisateur ou email correspond au nom d'utilisateur soumis lors de la phase de connexion/ login. La bonne nouvelle est qu'un objet Repository Doctrine peut agir comme un fournisseur d'entit utilisateur s'il implmente UserProviderInterface 12 . Cette interface est fournie avec trois mthodes 12. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserProviderInterface.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 222 Listing 61-6 implmenter : loadUserByUsername($username), refreshUser(UserInterface $user), et supportsClass($class). Pour plus de dtails, lisez UserProviderInterface 13 . Le code ci-dessous montre l'implmentation de UserProviderInterface 14 dans la classe UserRepository: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 // src/Acme/UserBundle/Entity/UserRepository.php namespace Acme\UserBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NoResultException; class UserRepository extends EntityRepository implements UserProviderInterface { public function loadUserByUsername($username) { $q = $this ->createQueryBuilder('u') ->where('u.username = :username OR u.email = :email') ->setParameter('username', $username) ->setParameter('email', $username) ->getQuery(); try { // La mthode Query::getSingleResult() lance une exception // s'il n'y a pas d'entre correspondante aux critres $user = $q->getSingleResult(); } catch (NoResultException $e) { throw new UsernameNotFoundException(sprintf('Unable to find an active admin AcmeUserBundle:User object identified by "%s".', $username), 0, $e); } return $user; } public function refreshUser(UserInterface $user) { $class = get_class($user); if (!$this->supportsClass($class)) { throw new UnsupportedUserException( sprintf( 'Instances of "%s" are not supported.', $class ) ); } return $this->find($user->getId()); } public function supportsClass($class) { return $this->getEntityName() === $class || is_subclass_of($class, 13. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserProviderInterface.html 14. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserProviderInterface.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 223 Listing 61-7 Listing 61-8 51 52 $this->getEntityName()); } } Pour finir l'implmentation, la configuration de la couche de scurit doit tre modifie pour dire Symfony d'utiliser le nouveau fournisseur d'entit personnalis la place du fournisseur d'entit Doctrine gnrique. Ceci est facile raliser en supprimant le champ property dans la section security.providers.administrators.entity du fichier security.yml. 1 2 3 4 5 6 7 # app/config/security.yml security: # ... providers: administrators: entity: { class: AcmeUserBundle:User } # ... En faisant cela, la couche de scurit va utiliser une instance de UserRepository et appeler sa mthode loadUserByUsername() pour rcuprer un utilisateur depuis la base de donnes, qu'il ait saisi son nom d'utilisateur ou son adresse email. Grer les rles via la Base de Donnes La fin de ce tutoriel se concentre sur comment stocker et rcuprer une liste de rles depuis la base de donnes. Comme prcis prcdemment, lorsque votre utilisateur est charg , sa mthode getRoles() retourne le tableau contenant ses rles de scurit qui doivent lui tre assigns. Vous pouvez charger ces donnes depuis n'importe o - une liste code en dur et utilise pour tous les utilisateurs (par exemple : array('ROLE_USER')), un tableau Doctrine en tant que proprit nomme roles, ou via une relation Doctrine, comme vous allez le voir dans cette section. Avec une installation typique, vous devriez toujours retourner au moins 1 rle avec la mthode getRoles(). Par convention, un rle appel ROLE_USER est gnralement retourn. Si vous ne russissez pas retourner un quelconque rle, cela voudrait dire que votre utilisateur n'est pas authentifi du tout. Dans cet exemple, la classe entit AcmeUserBundle:User dfinit une relation many-to-many avec une classe entit AcmeUserBundle:Group. Un utilisateur peut tre reli plusieurs groupes et un groupe peut tre compos d'un ou plusieurs utilisateurs. Comme un groupe est aussi un rle, la mthode prcdente getRoles() retourne maintenant la liste des groupes relis: 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/UserBundle/Entity/User.php namespace Acme\UserBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; // ... class User implements AdvancedUserInterface { /** * @ORM\ManyToMany(targetEntity="Group", inversedBy="users") * PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 224 Listing 61-9 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 */ private $groups; public function __construct() { $this->groups = new ArrayCollection(); } // ... public function getRoles() { return $this->groups->toArray(); } } La classe entit AcmeUserBundle:Group dfinit trois champs de table (id, name et role). Le champ unique role contient le nom du rle utilis par la couche de scurit de Symfony pour scuriser des parties de l'application. La chose la plus importante noter est que la classe entit AcmeUserBundle:Group implmente l'interface RoleInterface 15 qui la force avoir une mthode getRole(): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // src/Acme/Bundle/UserBundle/Entity/Group.php namespace Acme\UserBundle\Entity; use Symfony\Component\Security\Core\Role\RoleInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="acme_groups") * @ORM\Entity() */ class Group implements RoleInterface { /** * @ORM\Column(name="id", type="integer") * @ORM\Id() * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="name", type="string", length=30) */ private $name; /** * @ORM\Column(name="role", type="string", length=20, unique=true) */ private $role; /** * @ORM\ManyToMany(targetEntity="User", mappedBy="groups") */ private $users; 15. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Role/RoleInterface.html PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 225 Listing 61-10 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public function __construct() { $this->users = new ArrayCollection(); } // ... getters and setters for each property /** * @see RoleInterface */ public function getRole() { return $this->role; } } Pour amliorer les performances et viter le lazy loading de groupes lors de la rcupration d'un utilisateur depuis le fournisseur d'entit personnalis, la meilleure solution est d'effectuer une jointure avec la relation des groupes dans la mthode UserRepository::loadUserByUsername(). Cela va rcuprer l'utilisateur ainsi que ses rles/groupes associs avec une requte unique: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // src/Acme/UserBundle/Entity/UserRepository.php namespace Acme\UserBundle\Entity; // ... class UserRepository extends EntityRepository implements UserProviderInterface { public function loadUserByUsername($username) { $q = $this ->createQueryBuilder('u') ->select('u, g') ->leftJoin('u.groups', 'g') ->where('u.username = :username OR u.email = :email') ->setParameter('username', $username) ->setParameter('email', $username) ->getQuery(); // ... } // ... } La mthode QueryBuilder::leftJoin() joint et cherche les groupes lis depuis la classe modle AcmeUserBundle:User lorsqu'un utilisateur est rcupr grce son adresse email ou son nom d'utilisateur. PDF brought to you by generated on June 26, 2014 Chapter 61: Comment charger les utilisateurs depuis la base de donnes (le fournisseur d'Entit) | 226 Listing 62-1 Listing 62-2 Chapter 62 Comment ajouter la fonctionnalit de login Se souvenir de moi Une fois qu'un utilisateur est authentifi, ses informations de connexion sont gnralement stockes dans la session. Cela signifie que lorsque la session se termine, cet utilisateur sera dconnect et devra fournir de nouveau ses informations de login la prochaine fois qu'il voudra accder l'application. Vous pouvez laisser aux utilisateurs la possibilit de choisir de rester connect plus longtemps que la dure d'une session en utilisant un cookie avec l'option pare-feu remember_me. Le pare-feu a besoin d'avoir une cl secrte configure, qui est utilise pour encrypter le contenu du cookie. Il possde aussi plusieurs options avec des valeurs par dfaut qui sont dtailles ici : 1 2 3 4 5 6 7 8 # app/config/security.yml firewalls: main: remember_me: key: "%secret%" lifetime: 31536000 # 365 jours en secondes path: / domain: ~ # Prend la valeur par dfaut du domaine courant depuis $_SERVER C'est une bonne ide de fournir la possibilit l'utilisateur de choisir s'il veut utiliser la fonctionnalit remember me ou non, comme cela ne sera pas toujours appropri. La manire usuelle d'effectuer ceci est d'ajouter une checkbox au formulaire de login. En donnant le nom _remember_me la checkbox , le cookie va automatiquement tre dfini lorsque la checkbox sera coche et que l'utilisateur se sera connect avec succs. Donc, votre formulaire de login spcifique pourrait au final ressembler cela : 1 2 3 4 5 6 7 8 {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} {% if error %} <div>{{ error.message }}</div> {% endif %} <form action="{{ path('login_check') }}" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> PDF brought to you by generated on June 26, 2014 Chapter 62: Comment ajouter la fonctionnalit de login Se souvenir de moi | 227 Listing 62-3 9 10 11 12 13 14 15 16 17 <label for="password">Password:</label> <input type="password" id="password" name="_password" /> <input type="checkbox" id="remember_me" name="_remember_me" checked /> <label for="remember_me">Keep me logged in</label> <input type="submit" name="login" /> </form> L'utilisateur va donc tre connect automatiquement lors de ses prochaines visites tant que le cookie restera valide. Forcer l'utilisateur se r-authentifier avant d'accder certaines ressources Lorsque l'utilisateur retourne sur votre site, il ou elle est authentifi automatiquement en se basant sur les informations stockes dans le cookie remember me . Cela permet l'utilisateur d'accder des ressources protges comme si l'utilisateur s'tait authentifi lors de sa visite sur le site. Cependant, dans certains cas, vous pourriez vouloir forcer l'utilisateur se r-authentifier avant d'accder certains ressources. Par exemple, vous pourriez autoriser un utilisateur avec un cookie remember me voir les informations basiques de son compte, mais par contre vous pourriez lui imposer de se r- authentifier avant de modifier cette information. Le composant de scurit fournit une manire simple de faire cela. En plus des rles qui leurs sont explicitement assigns, les utilisateurs possdent automatiquement l'un des rles suivants dpendant de la manire dont ils sont authentifis : IS_AUTHENTICATED_ANONYMOUSLY - automatiquement assign un utilisateur qui se trouve dans une zone protge du site par un pare-feu mais qui ne s'est pas connect/logu. Cela est possible uniquement si l'accs anonyme a t autoris. IS_AUTHENTICATED_REMEMBERED - automatiquement assign un utilisateur qui a t authentifi via un cookie remember me . IS_AUTHENTICATED_FULLY - automatiquement assign un utilisateur qui a fourni ses informations de login durant la session courante. Vous pouvez utiliser ces rles pour contrler l'accs en plus des autres rles explicitement assigns. Si vous avez le rle IS_AUTHENTICATED_REMEMBERED, alors vous avez aussi le rle IS_AUTHENTICATED_ANONYMOUSLY. Si vous avez le rle IS_AUTHENTICATED_FULLY, alors vous possdez aussi les deux autres rles. En d'autres termes, ces rles reprsentent trois niveaux croissants de force d'authentification. Vous pouvez utiliser ces rles additionnels pour effectuer un contrle d'une granularit plus fine sur l'accs certaines parties d'un site. Par exemple, vous pourriez souhaiter que votre utilisateur soit capable de voir son compte en se rendant /account lorsqu'il est authentifi par cookie, mais qu'il doive fournir ses informations de login pour pouvoir diter les dtails de son compte. Vous pouvez effectuer ceci en scurisant certaines actions d'un contrleur spcifique en utilisant ces rles. L'action edit dans le contrleur pourrait tre scurise en utilisant le contexte du service. Dans l'exemple suivant, l'action est autorise seulement si l'utilisateur possde le rle IS_AUTHENTICATED_FULLY. PDF brought to you by generated on June 26, 2014 Chapter 62: Comment ajouter la fonctionnalit de login Se souvenir de moi | 228 Listing 62-4 1 2 3 4 5 6 7 8 9 10 11 12 13 // ... use Symfony\Component\Security\Core\Exception\AccessDeniedException public function editAction() { if (false === $this->get('security.context')->isGranted( 'IS_AUTHENTICATED_FULLY' )) { throw new AccessDeniedException(); } // ... } Vous pouvez aussi choisir d'installer et d'utiliser le bundle optionnel JMSSecurityExtraBundle 1 qui peut scuriser votre contrleur en utilisant des annotations : 1 2 3 4 5 6 7 8 9 use JMS\SecurityExtraBundle\Annotation\Secure; /** * @Secure(roles="IS_AUTHENTICATED_FULLY") */ public function editAction($name) { // ... } Si vous aviez aussi un contrle d'accs dans votre configuration de scurit qui requiert qu'un utilisateur possde un rle ROLE_USER afin d'accder n'importe quelle partie de la zone account , alors vous auriez la situation suivante : Si un utilisateur non-authentifi (ou authentifi anonymement) essaye d'accder la zone account , il sera demand cet utilisateur de s'authentifier. Une fois que l'utilisateur a entr son nom d'utilisateur et son mot de passe, et en supposant que l'utilisateur reoive le rle ROLE_USER par votre configuration, ce dernier aura le rle IS_AUTHENTICATED_FULLY et sera capable d'accder n'importe quelle page de la section account , incluant l'action editAction du contrleur. Enfin, supposons que la session de l'utilisateur se termine ; quand ce dernier retourne sur le site, il sera capable d'accder chaque page de la partie account - excepte la page edit - sans tre oblig de se r-authentifier. Cependant, quand il essaye d'accder l'action editAction du contrleur, il sera oblig de se r-authentifier, puisqu'il n'est pas (encore) totalement authentifi. Pour plus d'informations sur la scurisation de services ou de mthodes de cette manire, lisez Comment scuriser n'importe quel service ou mthode de votre application. 1. https://github.com/schmittjoh/JMSSecurityExtraBundle PDF brought to you by generated on June 26, 2014 Chapter 62: Comment ajouter la fonctionnalit de login Se souvenir de moi | 229 Listing 63-1 Chapter 63 Comment implmenter votre propre Voteur pour ajouter des adresses IP sur une liste noire Le composant de scurit de Symfony2 fournit plusieurs couches pour authentifier les utilisateurs. L'une de ces couches est appele un voteur. Un voteur est une classe ddie qui vrifie si l'utilisateur possde les droits ncessaires pour se connecter l'application. Par exemple, Symfony2 fournit une couche qui vrifie si l'utilisateur est totalement authentifi ou s'il possde quelques rles attendus. Il est parfois utile de crer un voteur personnalis pour grer un cas spcifique non-gr par le framework. Dans cette section, vous allez apprendre comment crer un voteur qui vous permettra d'ajouter des utilisateurs sur une liste noire suivant leur adresse IP. L'Interface Voter Un voteur personnalis doit implmenter VoterInterface 1 , qui requiert les trois mthodes suivantes : 1 2 3 4 5 6 interface VoterInterface { function supportsAttribute($attribute); function supportsClass($class); function vote(TokenInterface $token, $object, array $attributes); } La mthode supportsAttribute() est utilise pour vrifier si le voteur supporte l'attribut de l'utilisateur donn (c-a-d un rle, une ACL, etc.). La mthode supportsClass() est utilise pour vrifier si le voteur supporte la classe du token de l'utilisateur courant. La mthode vote() doit implmenter la logique mtier qui vrifie si oui ou non un utilisateur est autoris accder l'application. Cette mthode doit retourner l'une des valeurs suivantes : VoterInterface::ACCESS_GRANTED: L'utilisateur est autoris accder l'application 1. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.html PDF brought to you by generated on June 26, 2014 Chapter 63: Comment implmenter votre propre Voteur pour ajouter des adresses IP sur une liste noire | 230 Listing 63-2 VoterInterface::ACCESS_ABSTAIN: Le voteur ne peut pas dcider si l'utilisateur est oui ou non autoris accder l'application VoterInterface::ACCESS_DENIED: L'utilisateur n'est pas autoris accder l'application Dans cet exemple, vous allez vrifier si l'adresse IP de l'utilisateur correspond l'une des adresses de la liste noire. Si c'est le cas, vous retournerez VoterInterface::ACCESS_DENIED, sinon vous retournerez VoterInterface::ACCESS_ABSTAIN comme le but de ce voteur est uniquement de refuser l'accs, et non pas de l'autoriser. Crer un Voteur personnalis Pour ajouter un utilisateur sur la liste noire en se basant sur son IP, vous pouvez utiliser le service request et comparer l'adresse IP avec un ensemble d'adresses IP de la liste noire : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // src/Acme/DemoBundle/Security/Authorization/Voter/ClientIpVoter.php namespace Acme\DemoBundle\Security\Authorization\Voter; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class ClientIpVoter implements VoterInterface { public function __construct(ContainerInterface $container, array $blacklistedIp = array()) { $this->container = $container; $this->blacklistedIp = $blacklistedIp; } public function supportsAttribute($attribute) { // vous n'allez pas vrifier l'attribut de l'utilisateur, alors retournez true return true; } public function supportsClass($class) { // votre voteur supporte tous les types de classes de token, donc retournez true return true; } function vote(TokenInterface $token, $object, array $attributes) { $request = $this->container->get('request'); if (in_array($request->getClientIp(), $this->blacklistedIp)) { return VoterInterface::ACCESS_DENIED; } return VoterInterface::ACCESS_ABSTAIN; } } C'est tout ! Votre voteur est termin. La prochaine tape est d'injecter le voteur dans la couche de scurit. Cela peut tre effectu facilement l'aide du conteneur de service. PDF brought to you by generated on June 26, 2014 Chapter 63: Comment implmenter votre propre Voteur pour ajouter des adresses IP sur une liste noire | 231 Listing 63-3 Listing 63-4 Dclarer le Voteur comme service Pour injecter le voteur dans la couche de scurit, vous devez le dclarer en tant que service, et le tagger comme un security.voter : # src/Acme/AcmeBundle/Resources/config/services.yml services: security.access.blacklist_voter: class: Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter arguments: [@service_container, [123.123.123.123, 171.171.171.171]] public: false tags: - { name: security.voter } Soyez sr d'importer ce fichier de configuration depuis le fichier de configuration de votre application principale (par exemple : app/config/config.yml). Pour plus d'informations, lisez Importer la Configuration avec imports. Pour en savoir plus concernant la dfinition de services en gnral, lisez le chapitre Service Container. Changer la stratgie de dcision d'accs Afin que votre nouveau voteur soit utilis, vous devez changer la stratgie de dcision d'accs par dfaut, qui d'habitude autorise l'accs si n'importe quel voteur autorise l'accs. Dans ce cas, choisissez la stratgie unanimous. Contrairement la stratgie par dfaut affirmative, avec la stratgie unanimous, si seulement un voteur refuse l'accs (par exemple : le ClientIpVoter), alors l'accs n'est pas autoris pour l'utilisateur final. Pour faire cela, surchargez la section par dfaut access_decision_manager du fichier de configuration de votre application avec le code suivant. 1 2 3 4 5 # app/config/security.yml security: access_decision_manager: # La valeur de Strategy peut tre : affirmative, unanimous ou consensus strategy: unanimous C'est tout ! Maintenant, lors de la dcision de savoir si oui ou non un utilisateur devrait avoir accs, le nouveau voteur va refuser l'accs quiconque possdant une IP qui se trouve dans la liste noire. PDF brought to you by generated on June 26, 2014 Chapter 63: Comment implmenter votre propre Voteur pour ajouter des adresses IP sur une liste noire | 232 Listing 64-1 Chapter 64 Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) Dans les applications complexes, vous allez souvent rencontrer le problme que les dcisions d'accs ne peuvent pas uniquement se baser sur la personne (Token) qui demande l'accs, mais qu'elles impliquent aussi un objet domaine auquel l'accs est demand. C'est l o le systme des ACL intervient. Imaginez que vous tes en train de crer un systme de blog dans lequel vos utilisateurs peuvent commenter vos posts. Maintenant, vous voulez qu'un utilisateur puisse diter ses propres commentaires, mais pas ceux d'autres utilisateurs ; en outre, vous voulez vous-mme tre capable d'diter tous les commentaires. Dans ce scnario, Comment (commentaire) serait l'objet auquel vous souhaitez restreindre l'accs. Vous pouvez envisager plusieurs approches pour accomplir cela en utilisant Symfony2 ; les deux approches basiques sont (liste non-exhaustive) : Forcer la scurit dans vos mthodes mtier : cela signifie garder une rfrence dans chaque Comment de tous les utilisateurs qui ont accs, et alors de comparer ces utilisateurs avec le Token fourni. Forcer la scurit avec des rles : avec cette approche, vous ajouteriez un rle pour chaque objet Comment, i.e. ROLE_COMMENT_1, ROLE_COMMENT_2, etc. Les deux approches sont parfaitement valides. Cependant, elles associent votre logique d'autorisation votre code mtier, ce qui rend le tout moins rutilisable ailleurs, et qui augmente aussi la difficult d'effectuer des tests unitaires. En outre, vous pourriez rencontrer des problmes de performance si beaucoup d'utilisateurs accdaient un mme et unique objet domaine. Heureusement, il y a une meilleure faon de faire, que vous allez dcouvrir maintenant. Initialisation ( Bootstrapping en anglais) Maintenant, avant que vous puissiez finalement passer l'action, vous avez besoin d'effectuer certaines initialisations. Premirement, vous devez configurer la connexion que le systme d'ACL est suppos utiliser : PDF brought to you by generated on June 26, 2014 Chapter 64: Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) | 233 Listing 64-2 Listing 64-3 1 2 3 4 # app/config/security.yml security: acl: connection: default Le systme ACL requiert qu'une connexion DBAL Doctrine (utilisable par dfaut) ou qu'une connexion ODM Doctrine (utilisable avec MongoDBAclBundle 1 ) soit configure. Cependant, cela ne veut pas dire que vous devez utiliser l'ORM ou l'ODM Doctrine pour faire correspondre vos objets. Vous pouvez utiliser l'outil de correspondance de votre choix pour vos objets, que ce soit l'ORM Doctrine, l'ODM Mongo, Propel, ou du SQL brut, le choix reste le vtre. Une fois la connexion configure, vous devez importer la structure de la base de donnes. Heureusement, il existe une tche pour cela. Excutez simplement la commande suivante : 1 $ php app/console init:acl Dmarrage Revenez ce petit exemple du dbut et implmentez ses ACLs. Crer un ACL, et ajouter un ACE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // src/Acme/DemoBundle/Controller/BlogController.php namespace Acme\DemoBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Acl\Domain\ObjectIdentity; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; use Symfony\Component\Security\Acl\Permission\MaskBuilder; class BlogController { // ... public function addCommentAction(Post $post) { $comment = new Comment(); // prparation du $form et liaison (bind) des donnes if ($form->isValid()) { $entityManager = $this->get('doctrine.orm.default_entity_manager'); $entityManager->persist($comment); $entityManager->flush(); // cration de l'ACL $aclProvider = $this->get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($comment); 1. https://github.com/IamPersistent/MongoDBAclBundle PDF brought to you by generated on June 26, 2014 Chapter 64: Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) | 234 Listing 64-4 28 29 30 31 32 33 34 35 36 37 38 39 40 $acl = $aclProvider->createAcl($objectIdentity); // retrouve l'identifiant de scurit de l'utilisateur actuellement connect $securityContext = $this->get('security.context'); $user = $securityContext->getToken()->getUser(); $securityIdentity = UserSecurityIdentity::fromAccount($user); // donne accs au propritaire $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER); $aclProvider->updateAcl($acl); } } } Il y a plusieurs dcisions d'implmentation importantes dans ce petit bout de code. Pour le moment, je veux mettre en vidence seulement deux d'entre elles : Tout d'abord, vous avez peut-tre remarqu que ->createAcl() n'accepte pas d'objets de domaine directement, mais uniquement des implmentations de ObjectIdentityInterface. Cette tape additionnelle d'indirection vous permet de travailler avec les ACLs mme si vous n'avez pas d'instance d'objet domaine sous la main. Cela va tre extrmement utile si vous voulez vrifier les permissions pour un grand nombre d'objets sans avoir les dsrialiser. L'autre partie intressante est l'appel ->insertObjectAce(). Dans l'exemple, vous accordez l'utilisateur connect un accs propritaire au Comment. Le MaskBuilder::MASK_OWNER est un masque binaire prdfini ; ne vous inquitez pas, le constructeur de masque va abstraire la plupart des dtails techniques, mais en utilisant cette technique vous pouvez stocker plein de permissions diffrentes dans une mme ligne de base de donnes ; ce qui vous offre un avantage considrable au niveau performance. L'ordre dans lequel les ACEs sont vrifies est important. En tant que rgle gnrale, vous devriez placer les entres les plus spcifiques au dbut. Vrification des Accs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // src/Acme/DemoBundle/Controller/BlogController.php // ... class BlogController { // ... public function editCommentAction(Comment $comment) { $securityContext = $this->get('security.context'); // check for edit access if (false === $securityContext->isGranted('EDIT', $comment)) { throw new AccessDeniedException(); } // ... rcuprez le bon objet comment , et ditez-le ici PDF brought to you by generated on June 26, 2014 Chapter 64: Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) | 235 Listing 64-5 Listing 64-6 20 21 } } Dans cet exemple, vous vrifiez que l'utilisateur possde la permission EDIT. En interne, Symfony2 fait correspondre la permission avec plusieurs masques binaires, et vrifie si l'utilisateur possde l'un d'entre eux. Vous pouvez dfinir jusqu' 32 permissions de base (dpendant du PHP de votre OS, cela pourrait varier entre 30 et 32). De plus, vous pouvez aussi dfinir des permissions cumules. Permissions Cumules Dans le premier exemple ci-dessus, vous avez accord uniquement la permission basique OWNER l'utilisateur. Bien que cela permette aussi l'utilisateur d'effectuer n'importe quelle opration telle que la lecture, l'dition, etc. sur l'objet domaine, il y a des cas o vous voudrez accorder ces permissions explicitement. Le MaskBuilder peut tre utilis pour crer des masques binaires facilement en combinant plusieurs permissions de base : 1 2 3 4 5 6 7 8 $builder = new MaskBuilder(); $builder ->add('view') ->add('edit') ->add('delete') ->add('undelete') ; $mask = $builder->get(); // int(29) Ce masque binaire reprsent par un entier peut ainsi tre utilis pour accorder un utilisateur les permissions de base que vous avez ajoutes ci-dessus : 1 2 $identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User'); $acl->insertObjectAce($identity, $mask); L'utilisateur a dsormais le droit de lire, diter, supprimer, et annuler une suppression sur des objets. PDF brought to you by generated on June 26, 2014 Chapter 64: Comment utiliser les Access Control Lists (ACLs) ( liste de contrle d'accs en franais) | 236 Chapter 65 Comment utiliser les concepts d'ACL avancs Le but de ce chapitre est de donner une vision plus profonde du systme des ACL ( Liste de Contrle d'Accs en franais), et aussi d'expliquer certaines dcisions prises en ce qui concerne sa conception. Concepts d'Architecture Les capacits de scurit de l'instance objet de Symfony2 sont bases sur le concept d'une Access Control List . Chaque instance d'un objet domaine a sa propre ACL. L'instance ACL contient une liste dtaille des Access Control Entries (ACEs ou Entres de Contrle d'Accs en franais) qui sont utilises pour prendre les dcisions d'accs. Le systme d'ACL de Symfony2 se concentre sur deux objectifs : fournir un moyen de rcuprer de manire efficace un grand nombre d'ACLs/ACEs pour vos objets domaine, et de les modifier ; fournir un moyen de prendre les dcisions facilement quant savoir si une personne est autorise effectuer une action sur un objet domaine ou non. Comme spcifi dans le premier point, l'une des principales facults du systme ACL de Symfony2 est de fournir une manire trs performante de rcuprer des ACLs/ACEs. Ceci est extrmement important sachant que chaque ACL pourrait avoir plusieurs ACEs, et hriter d'une autre ACL la manire d'une structure en arbre. Donc, nous ne nous servons pas d'un ORM spcifique, mais l'implmentation par dfaut intragit avec votre connexion en utilisant directement le DBAL de Doctrine. Identits d'Objet Le systme ACL est compltement dcoupl de vos objets domaine. Ils ne doivent mme pas tre stocks dans la mme base de donnes, ou sur le mme serveur. Pour pouvoir accomplir ce dcouplage, vos objets sont reprsents dans le systme ACL par des objets d'identit d'objet. Chaque fois que vous voulez rcuprer une ACL pour un objet domaine, le systme ACL va d'abord crer une identit d'objet pour votre objet domaine, et va ensuite passer cette identit d'objet au fournisseur d'ACL pour un traitement ultrieur. PDF brought to you by generated on June 26, 2014 Chapter 65: Comment utiliser les concepts d'ACL avancs | 237 Identits de Scurit Ceci est similaire l'identit d'objet, mais reprsente un utilisateur, ou un rle dans votre application. Chaque rle ou utilisateur possde sa propre identit de scurit. Structure de Table dans la Base de Donnes L'implmentation par dfaut utilise cinq tables de base de donnes qui sont listes ci-dessous. Les tables sont classes par nombre de lignes, de celle contenant le moins de lignes celle en contenant le plus dans une application classique : acl_security_identities : Cette table enregistre toutes les identits de scurit (SID) qui dtiennent les ACEs. L'implmentation par dfaut est fournie avec deux identits de scurit : RoleSecurityIdentity, et UserSecurityIdentity ; acl_classes : Cette table fait correspondre les noms de classe avec un id unique qui peut tre rfrenc depuis d'autres tables ; acl_object_identities : Chaque ligne dans cette table reprsente une unique instance d'objet domaine ; acl_object_identity_ancestors : Cette table nous autorise dterminer tous les anctres d'une ACL de manire trs efficace ; acl_entries : Cette table contient toutes les ACEs. C'est typiquement la table avec le plus de lignes. Elle peut en contenir des dizaines de millions sans impacter de faon significative les performances. Porte des Access Control Entries Les entres de contrle d'accs peuvent avoir diffrentes portes dans lesquelles elles s'appliquent. Dans Symfony2, il existe principalement deux portes diffrentes : Porte de la Classe : Ces entres s'appliquent tous les objets ayant la mme classe. Porte de l'Objet : Ceci est la porte utilise dans le chapitre prcdent, et elle s'applique uniquement un objet spcifique. Parfois, vous aurez besoin d'appliquer une ACE uniquement sur le champ spcifique d'un objet. Supposons que vous voulez que l'ID soit uniquement visible par un administrateur mais pas par votre service client. Pour solutionner ce problme commun, deux sous-portes supplmentaires ont t ajoutes : Porte d'un Champ de Classe : Ces entres s'appliquent tous les objets ayant la mme classe, mais uniquement un champ spcifique de ces objets. Porte d'un Champ d'Objet : Ces entres s'appliquent un objet spcifique, et uniquement un champ spcifique de cet objet. Dcisions de pr-autorisation Pour les dcisions de pr-autorisation, que ce soit des dcisions prises avant quelconque mthode ou bien une action scurise qui est invoque, le service AccessDecisionManager est utilis. Ce service est galement utilis pour connatre les dcisions d'autorisation bases sur des rles. Comme les rles, le systme d'ACL ajoute plusieurs nouveaux attributs qui pourraient tre utiliss pour vrifier diffrentes permissions. PDF brought to you by generated on June 26, 2014 Chapter 65: Comment utiliser les concepts d'ACL avancs | 238 Table de Permission Intgre Attribut Signification Masques Binaires VIEW Si quelqu'un est autoris voir l'objet domaine. VIEW, EDIT, OPERATOR, MASTER, or OWNER EDIT Si quelqu'un est autoris effectuer des changements sur l'objet domaine. EDIT, OPERATOR, MASTER, or OWNER CREATE Si quelqu'un est autoris crer l'objet domaine. CREATE, OPERATOR, MASTER, or OWNER DELETE Si quelqu'un est autoris supprimer l'objet domaine. DELETE, OPERATOR, MASTER, or OWNER UNDELETE Si quelqu'un est autoris restaurer un objet domaine prcdemment supprim. UNDELETE, OPERATOR, MASTER, or OWNER OPERATOR Si quelqu'un est autoris effectuer toutes les actions ci-dessus. OPERATOR, MASTER, or OWNER MASTER Si quelqu'un est autoris effectuer toutes les actions ci-dessus, et en plus a le droit d'affecter n'importe laquelle d'entre elles quelqu'un d'autre. MASTER, or OWNER OWNER Si quelqu'un possde l'objet domaine. Un propritaire peut effectuer n'importe laquelle des actions ci-dessus et affecter les permissions master et owner. OWNER Attributs de Permission vs. Masques Binaires de Permission Les attributs sont utiliss par l' AccessDecisionManager , tout comme les rles. Souvent, ces attributs reprsentent en fait une agrgation de masques binaires. D'un autre ct, les masques binaires sous forme d'entier sont utiliss par le systme d'ACL en interne pour stocker de manire efficace les permissions de vos utilisateurs dans la base de donnes, et pour effectuer des vrifications en utilisant des oprations sur les masques binaires extrmement rapides. Extensibilit La table de permissions ci-dessus n'est en rien statique, et pourrait thoriquement tre compltement remplace. Cependant, elle devrait couvrir la plupart des problmes que vous pourriez rencontrer, et pour des raisons d'introprabilit avec d'autres bundles, vous tes encourag conserver les significations initialement prvues pour ces permissions. Dcisions de post-autorisation Les dcisions de post-autorisation sont effectues aprs qu'une mthode scurise a t invoque, et impliquent typiquement l'objet domaine qui est retourn par une telle mthode. Aprs invocations, les fournisseurs permettent aussi de modifier, ou de filtrer l'objet domaine avant qu'il ne soit retourn. PDF brought to you by generated on June 26, 2014 Chapter 65: Comment utiliser les concepts d'ACL avancs | 239 A cause de limitations actuelles du langage PHP, il n'y a pas de fonctionnalits de post-autorisation implmentes dans le composant coeur Security . Nanmoins, il y a un bundle exprimental appel JMSSecurityExtraBundle 1 qui ajoute ces fonctionnalits. Lisez sa documentation pour avoir plus d'informations pour comprendre comment ceci est ralis. Processus pour connatre les dcisions d'autorisation La classe ACL fournit deux mthodes pour dterminer si une identit de scurit possde les masques binaires requis, isGranted et isFieldGranted. Lorsque l'ACL reoit une requte d'autorisation travers l'une de ces mthodes, elle dlgue cette requte une implmentation de PermissionGrantingStrategy . Cela vous permet de remplacer la manire dont les dcisions d'accs sont atteintes sans modifier la classe ACL elle-mme. La PermissionGrantingStrategy vrifie en premier toutes les ACEs de vos portes d'objet ; si aucune n'est applicable, les ACEs de vos portes de classe vont tre vrifies, et si aucune n'est applicable, alors le processus va tre rpt avec les ACEs du parent de l'ACL. Si aucun parent de l'ACL n'existe, une exception sera lance. 1. https://github.com/schmittjoh/JMSSecurityExtraBundle PDF brought to you by generated on June 26, 2014 Chapter 65: Comment utiliser les concepts d'ACL avancs | 240 Listing 66-1 Listing 66-2 Chapter 66 Comment forcer HTTPS ou HTTP pour des URLs Diffrentes Vous pouvez forcer certaines parties de votre site utiliser le protocole HTTPS dans la configuration de la scurit. Cela s'effectue grce aux rgles access_control en utilisant l'option requires_channel. Par exemple, si vous voulez forcer toutes les URLs commenant par /secure utiliser HTTPS, alors vous pourriez utiliser la configuration suivante : 1 2 3 4 access_control: - path: ^/secure roles: ROLE_ADMIN requires_channel: https Le formulaire de login lui-mme a besoin d'autoriser un accs anonyme, sinon les utilisateurs seront incapables de s'authentifier. Pour le forcer utiliser HTTPS vous pouvez toujours utiliser les rgles de access_control en vous servant du rle IS_AUTHENTICATED_ANONYMOUSLY : 1 2 3 4 access_control: - path: ^/login roles: IS_AUTHENTICATED_ANONYMOUSLY requires_channel: https Il est aussi possible de spcifier l'utilisation d'HTTPS dans la configuration de routage, lisez Comment forcer les routes toujours utiliser HTTPS ou HTTP pour plus de dtails. PDF brought to you by generated on June 26, 2014 Chapter 66: Comment forcer HTTPS ou HTTP pour des URLs Diffrentes | 241 Listing 67-1 Chapter 67 Comment personnaliser votre formulaire de login Utiliser un formulaire de login est une mthode classique et flexible pour grer l'authentification dans Symfony2. Quasiment chaque aspect du formulaire de login peut tre personnalis. La configuration complte par dfaut est dtaille dans la prochaine section. Rfrence de Configuration du formulaire de login 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # app/config/security.yml security: firewalls: main: form_login: # l'utilisateur est redirig ici quand il/elle a besoin de se connecter login_path: /login # si dfini true, forward l'utilisateur vers le formulaire de # login au lieu de le rediriger use_forward: false # soumet le formulaire de login vers cette URL check_path: /login_check # par dfaut, le formulaire de login *doit* tre un POST, # et pas un GET post_only: true # options de redirection lorsque le login a russi (vous # pouvez en lire plus ci-dessous) always_use_default_target_path: false default_target_path: / target_path_parameter: _target_path PDF brought to you by generated on June 26, 2014 Chapter 67: Comment personnaliser votre formulaire de login | 242 Listing 67-2 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 use_referer: false # options de redirection lorsque le login choue (vous # pouvez en lire plus ci-dessous) failure_path: null failure_forward: false # noms des champs pour le nom d'utilisateur et le mot # de passe username_parameter: _username password_parameter: _password # options du token csrf csrf_parameter: _csrf_token intention: authenticate Rediriger aprs un succs Vous pouvez changer l'URL de redirection aprs que le formulaire de login a t soumis avec succs via plusieurs options de configuration. Par dfaut, le formulaire va rediriger l'utilisateur vers l'URL qu'il a demande (c-a-d l'URL qui a dclenche le formulaire de login qui est montr). Par exemple, si l'utilisateur a demand http://www.example.com/admin/post/18/edit, alors aprs, il sera ventuellement redirig vers http://www.example.com/admin/post/18/edit dans le cas d'un succs de connexion. Cela est effectu en stockant l'URL demande dans la session. Si aucune URL n'est prsente dans la session (peut-tre que l'utilisateur a t directement sur la page de login), alors l'utilisateur est redirig vers la page par dfaut, qui est / (c-a-d. la page d'accueil) par dfaut. Vous pouvez changer ce comportement de diffrentes faons. Comme prcis, par dfaut, l'utilisateur est redirig vers la page qu'il avait demande la base. Quelquefois, cela peut poser des problmes, comme par exemple si une requte AJAX en arrire- plan apparat comme tant la dernire URL visite, redirigeant l'utilisateur vers cette dernire. Pour plus d'informations sur comment contrler ce comportement, lisez Comment changer le comportement par dfaut du chemin cible. Changer la page par dfaut Tout d'abord, la page par dfaut peut tre dfinie (c-a-d la page vers laquelle l'utilisateur est redirige si aucune page n'avait t prcdemment stocke dans la session). Pour la dfinir en tant que /admin, utilisez la configuration suivante : 1 2 3 4 5 6 7 # app/config/security.yml security: firewalls: main: form_login: # ... default_target_path: /admin Maintenant, quand aucune URL n'est dfinie dans la session, l'utilisateur va tre envoy vers /admin. PDF brought to you by generated on June 26, 2014 Chapter 67: Comment personnaliser votre formulaire de login | 243 Listing 67-3 Listing 67-4 Listing 67-5 Toujours rediriger vers la page par dfaut Vous pouvez faire en sorte que les utilisateurs soient toujours redirigs vers la page par dfaut sans tenir compte de l'URL qu'ils avaient demande en dfinissant l'option always_use_default_target_path true : 1 2 3 4 5 6 7 # app/config/security.yml security: firewalls: main: form_login: # ... always_use_default_target_path: true Utiliser l'URL rfrante Dans le cas o aucune URL n'a t stocke dans la session, vous pourriez souhaiter essayer d'utiliser HTTP_REFERER la place, comme ce dernier sera souvent identique. Vous pouvez effectuer cela en dfinissant use_referer true (par dfaut la valeur est false ) : 1 2 3 4 5 6 7 # app/config/security.yml security: firewalls: main: form_login: # ... use_referer: true New in version 2.1: Depuis la version 2.1, si le rfrant est gal l'option login_path, l'utilisateur sera redirig vers le default_target_path. Contrler l'URL de redirection depuis le formulaire Vous pouvez aussi surcharger le chemin vers lequel l'utilisateur est redirig via le formulaire lui-mme en incluant un champ cach avec le nom _target_path. Par exemple, pour rediriger vers l'URL dfinie par une route account, utilisez ce qui suit : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} {% if error %} <div>{{ error.message }}</div> {% endif %} <form action="{{ path('login_check') }}" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> <label for="password">Password:</label> <input type="password" id="password" name="_password" /> <input type="hidden" name="_target_path" value="account" /> <input type="submit" name="login" /> </form> PDF brought to you by generated on June 26, 2014 Chapter 67: Comment personnaliser votre formulaire de login | 244 Listing 67-6 Listing 67-7 Maintenant, l'utilisateur va tre redirig vers la valeur du champ cach du formulaire. La valeur de l'attribut peut tre un chemin relatif, une URL absolue, ou un nom de route. Vous pouvez mme changer le nom du champ cach du formulaire en changeant l'option target_path_parameter. 1 2 3 4 5 6 # app/config/security.yml security: firewalls: main: form_login: target_path_parameter: redirect_url Redirection en cas d'chec du login En plus de la redirection lorsqu'un utilisateur russit se connecter, vous pouvez aussi dfinir l'URL vers laquelle l'utilisateur devrait tre redirig aprs un chec lors de la phase de login (par exemple : un nom d'utilisateur ou mot de passe non-valide a t soumis). Par dfaut, l'utilisateur est redirig vers le formulaire de login lui-mme. Vous pouvez dfinir une URL diffrente en utilisant la configuration suivante : 1 2 3 4 5 6 7 # app/config/security.yml security: firewalls: main: form_login: # ... failure_path: /login_failure PDF brought to you by generated on June 26, 2014 Chapter 67: Comment personnaliser votre formulaire de login | 245 Listing 68-1 Listing 68-2 Chapter 68 Comment scuriser n'importe quel service ou mthode de votre application Dans le chapitre sur la scurit, vous pouvez voir comment scuriser un contrleur en rcuprant le service security.context depuis le Conteneur de Service et en vrifiant le rle actuel de l'utilisateur: 1 2 3 4 5 6 7 8 9 10 11 // ... use Symfony\Component\Security\Core\Exception\AccessDeniedException; public function helloAction($name) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); } // ... } Vous pouvez aussi scuriser n'importe quel service d'une manire similaire en lui injectant le service security.context. Pour une introduction gnrale sur l'injection de dpendances dans un service, voyez le chapitre Service Container du book. Par exemple, supposons que vous ayez une classe NewsletterManager qui envoie des emails et que vous souhaitiez restreindre son utilisation aux utilisateurs ayant un rle nomm ROLE_NEWSLETTER_ADMIN uniquement. Avant l'ajout de cette scurit, la classe ressemble ceci : 1 2 3 4 5 6 7 8 9 // src/Acme/HelloBundle/Newsletter/NewsletterManager.php namespace Acme\HelloBundle\Newsletter; class NewsletterManager { public function sendNewsletter() { // c'est ici que vous effectuez le travail PDF brought to you by generated on June 26, 2014 Chapter 68: Comment scuriser n'importe quel service ou mthode de votre application | 246 Listing 68-3 Listing 68-4 Listing 68-5 10 11 12 13 } // ... } Votre but est de vrifier le rle de l'utilisateur lorsque la mthode sendNewsletter() est appele. La premire tape pour y parvenir est d'injecter le service security.context dans l'objet. Sachant qu'il serait assez risqu de ne pas effectuer de vrification de scurit, nous avons ici un candidat idal pour l'injection via le constructeur, qui garantit que l'objet du contexte de scurit sera disponible dans la classe NewsletterManager: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 namespace Acme\HelloBundle\Newsletter; use Symfony\Component\Security\Core\SecurityContextInterface; class NewsletterManager { protected $securityContext; public function __construct(SecurityContextInterface $securityContext) { $this->securityContext = $securityContext; } // ... } Puis, dans votre configuration de service, vous pouvez injecter le service : # src/Acme/HelloBundle/Resources/config/services.yml parameters: newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager services: newsletter_manager: class: "%newsletter_manager.class%" arguments: [@security.context] Le service inject peut ds lors tre utilis pour effectuer la vrification de scurit lorsque la mthode sendNewsletter() est appele: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace Acme\HelloBundle\Newsletter; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\SecurityContextInterface; // ... class NewsletterManager { protected $securityContext; public function __construct(SecurityContextInterface $securityContext) { $this->securityContext = $securityContext; } public function sendNewsletter() PDF brought to you by generated on June 26, 2014 Chapter 68: Comment scuriser n'importe quel service ou mthode de votre application | 247 Listing 68-6 Listing 68-7 17 18 19 20 21 22 23 24 25 26 { if (false === $this->securityContext->isGranted('ROLE_NEWSLETTER_ADMIN')) { throw new AccessDeniedException(); } //... } // ... } Si l'utilisateur actuel ne possde pas le rle ROLE_NEWSLETTER_ADMIN, il lui sera demand de se connecter. Scuriser des mthodes en utilisant des annotations Vous pouvez aussi scuriser des appels de mthodes dans n'importe quel service avec des annotations en utilisant le bundle facultatif JMSSecurityExtraBundle 1 . Ce bundle n'est pas inclus dans la Distribution Standard de Symfony2, mais vous pouvez choisir de l'installer. Pour activer la fonctionnalit des annotations, taggez le service que vous voulez scuriser avec le tag security.secure_service (vous pouvez aussi activer automatiquement cette fonctionnalit pour tous les services, voir l'encadr ci-dessous) : 1 2 3 4 5 6 7 8 # src/Acme/HelloBundle/Resources/config/services.yml # ... services: newsletter_manager: # ... tags: - { name: security.secure_service } Vous pouvez ainsi parvenir aux mmes rsultats que ci-dessus en utilisant une annotation: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace Acme\HelloBundle\Newsletter; use JMS\SecurityExtraBundle\Annotation\Secure; // ... class NewsletterManager { /** * @Secure(roles="ROLE_NEWSLETTER_ADMIN") */ public function sendNewsletter() { //... } // ... } 1. https://github.com/schmittjoh/JMSSecurityExtraBundle PDF brought to you by generated on June 26, 2014 Chapter 68: Comment scuriser n'importe quel service ou mthode de votre application | 248 Listing 68-8 Les annotations fonctionnent car une classe proxy est cre pour votre classe qui effectue les vrifications de scurit. Cela signifie que vous pouvez utiliser les annotations sur des mthodes public ou protected , mais que vous ne pouvez pas les utiliser avec des mthodes private ou avec des mthodes marques comme final Le JMSSecurityExtraBundle vous permet aussi de scuriser les paramtres et les valeurs retournes par les mthodes. Pour plus d'informations, lisez la documentation du JMSSecurityExtraBundle 2 . Activer la Fonctionnalit des Annotations pour tous les Services Quand vous scurisez la mthode d'un service (comme expliqu ci-dessus), vous pouvez soit tagger chaque service individuellement, ou activer la fonctionnalit pour tous les services en une seule fois. Pour ce faire, dfinissez l'option de configuration secure_all_services true : 1 2 3 4 # app/config/config.yml jms_security_extra: # ... secure_all_services: true L'inconvnient de cette mthode est que, si elle est active, le chargement initial de la page pourrait tre trs lent selon le nombre de services que vous avez dfini. 2. https://github.com/schmittjoh/JMSSecurityExtraBundle PDF brought to you by generated on June 26, 2014 Chapter 68: Comment scuriser n'importe quel service ou mthode de votre application | 249 Listing 69-1 Chapter 69 Comment crer un Fournisseur d'Utilisateur personnalis Une partie du processus d'authentification standard de Symfony dpend des fournisseurs d'utilisateur . Lorsqu'un utilisateur soumet un nom d'utilisateur et un mot de passe, la couche d'authentification demande au fournisseur d'utilisateur configur de retourner un objet utilisateur pour un nom d'utilisateur donn. Symfony vrifie alors si le mot de passe de cet utilisateur est correct ou non et gnre un token de scurit afin que l'utilisateur reste authentifi pendant la session courante. Symfony possde par dfaut un fournisseur d'utilisateur in_memory et entity . Dans cet article, vous verrez comment vous pouvez crer votre propre fournisseur d'utilisateur, ce qui pourrait tre utile si vous accdez vos utilisateurs via une base de donnes personnalise, un fichier, ou - comme le montre cet exemple - travers un service web. Crer une Classe Utilisateur Tout d'abord, quelle que soit la source de vos donnes utilisateurs, vous allez avoir besoin de crer une classe User qui reprsente ces donnes. Le User peut ressembler ce que vous voulez et contenir n'importe quelles donnes. La seule condition requise est que la classe implmente UserInterface 1 . Les mthodes de cette interface doivent donc tre dfinies dans la classe utilisateur personnalise : getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials(), equals(). Voyons cela en action: 1 2 3 4 5 6 7 8 // src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php namespace Acme\WebserviceUserBundle\Security\User; use Symfony\Component\Security\Core\User\UserInterface; class WebserviceUser implements UserInterface { private $username; 1. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html PDF brought to you by generated on June 26, 2014 Chapter 69: Comment crer un Fournisseur d'Utilisateur personnalis | 250 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 private $password; private $salt; private $roles; public function __construct($username, $password, $salt, array $roles) { $this->username = $username; $this->password = $password; $this->salt = $salt; $this->roles = $roles; } public function getRoles() { return $this->roles; } public function getPassword() { return $this->password; } public function getSalt() { return $this->salt; } public function getUsername() { return $this->username; } public function eraseCredentials() { } public function equals(UserInterface $user) { if (!$user instanceof WebserviceUser) { return false; } if ($this->password !== $user->getPassword()) { return false; } if ($this->getSalt() !== $user->getSalt()) { return false; } if ($this->username !== $user->getUsername()) { return false; } return true; } } PDF brought to you by generated on June 26, 2014 Chapter 69: Comment crer un Fournisseur d'Utilisateur personnalis | 251 Listing 69-2 Si vous avez plus d'informations propos de vos utilisateurs - comme un prnom - alors vous pouvez ajouter un champ firstName pour contenir cette donne. Pour plus de dtails sur chacune de ces mthodes, voir l'interface UserInterface 2 . Crer un Fournisseur d'Utilisateur Maintenant que vous avez une classe User, vous allez crer un fournisseur d'utilisateur qui va rcuprer les informations utilisateur depuis un service web, et nous allons aussi crer un objet WebserviceUser et le remplir avec des donnes. Le fournisseur d'utilisateur est juste une classe PHP qui doit implmenter UserProviderInterface 3 , qui requiert que trois mthodes soient dfinies : loadUserByUsername($username), refreshUser(UserInterface $user), et supportsClass($class). Pour plus de dtails, voir l'interface UserProviderInterface 4 . Voici un exemple de ce quoi cela pourrait ressembler: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php namespace Acme\WebserviceUserBundle\Security\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; class WebserviceUserProvider implements UserProviderInterface { public function loadUserByUsername($username) { // effectuez un appel votre service web ici $userData = ... // supposons qu'il retourne un tableau en cas de succs, ou bien // false s'il n'y a pas d'utilisateur if ($userData) { $password = '...'; // ... return new WebserviceUser($username, $password, $salt, $roles) } throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); } public function refreshUser(UserInterface $user) { if (!$user instanceof WebserviceUser) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } return $this->loadUserByUsername($user->getUsername()); 2. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserInterface.html 3. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserProviderInterface.html 4. http://api.symfony.com/2.3/Symfony/Component/Security/Core/User/UserProviderInterface.html PDF brought to you by generated on June 26, 2014 Chapter 69: Comment crer un Fournisseur d'Utilisateur personnalis | 252 Listing 69-3 Listing 69-4 Listing 69-5 38 39 40 41 42 } public function supportsClass($class) { return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser'; } } Crer un Service pour le Fournisseur d'Utilisateur Maintenant, vous allez rendre le fournisseur d'utilisateur disponible en tant que service. 1 2 3 4 5 6 7 # src/Acme/WebserviceUserBundle/Resources/config/services.yml parameters: webservice_user_provider.class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider services: webservice_user_provider: class: "%webservice_user_provider.class%" L'implmentation relle du fournisseur d'utilisateur aura probablement certaines dpendances, des options de configuration ou d'autres services. Ajoutez ces derniers en tant qu'arguments dans la dfinition du service. Assurez-vous que le fichier des services est import. Lisez Importer la Configuration avec imports pour plus de dtails. Modifier security.yml Dans le fichier /app/config/security.yml, tout vient ensemble. Ajoutez le fournisseur d'utilisateur la liste des fournisseurs dans la section security . Choisissez un nom pour le fournisseur d'utilisateur (par exemple : webservice ) et spcifiez l'id du service que vous venez de dfinir. 1 2 3 4 security: providers: webservice: id: webservice_user_provider Symfony a aussi besoin de savoir comment encoder les mots de passe qui sont soumis par les utilisateurs du site web, par exemple lorsque ces derniers remplissent un formulaire de login. Vous pouvez effectuer cela en ajoutant une ligne dans la section encoders dans le fichier /app/config/security.yml. 1 2 3 security: encoders: Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512 PDF brought to you by generated on June 26, 2014 Chapter 69: Comment crer un Fournisseur d'Utilisateur personnalis | 253 Listing 69-6 La valeur spcifie ici devrait correspondre l'algorithme utilis initialement pour l'encodage des mots de passe lors de la cration de vos utilisateurs (qu'importe la manire dont vous avez cr ces utilisateurs). Quand un utilisateur soumet son mot de passe, ce dernier est rajout la valeur du salt et puis encod en utilisant cet algorithme avant d'tre compar avec le mot de passe crypt retourn par votre mthode getPassword(). De plus, en fonction de vos options, le mot de passe pourrait tre encod plusieurs fois et encod ensuite en base64. Dtails sur la manire dont les mots de passe sont encrypts Symfony utilise une mthode spcifique pour combiner le salt et encoder le mot de passe avant de le comparer avec votre mot de passe encod. Si getSalt() ne retourne rien, alors le mot de passe soumis est simplement encod en utilisant l'algorithme que vous avez spcifi dans le fichier security.yml. Si un salt est spcifi, alors la valeur suivante est cre et ensuite assemble pour former un hash via l'algorithme : $password.'{'.$salt.'}'; Si vos utilisateurs externes ont leurs mots de passe encods d'une faon diffrente, alors vous aurez besoin de travailler un peu plus afin que Symfony encode correctement le mot de passe. Ceci est au-del de la porte de cet article, mais devrait inclure de crer une sous-classe MessageDigestPasswordEncoder ainsi que surcharger la mthode mergePasswordAndSalt. De surcrot, le hash , par dfaut, est encod plusieurs fois puis encod en base64. Pour des dtails plus spcifiques, voir MessageDigestPasswordEncoder 5 . Pour viter cela, configurez-le dans le fichier security.yml : 1 2 3 4 5 6 security: encoders: Acme\WebserviceUserBundle\Security\User\WebserviceUser: algorithm: sha512 encode_as_base64: false iterations: 1 5. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php PDF brought to you by generated on June 26, 2014 Chapter 69: Comment crer un Fournisseur d'Utilisateur personnalis | 254 Chapter 70 Comment crer un Fournisseur d'Authentification Personnalis Si vous avez lu le chapitre La scurit, vous comprenez la distinction que Symfony2 fait entre authentification et autorisation dans l'implmentation de la scurit. Ce chapitre traite des classes noyaux impliques dans le processus d'authentification, et de l'implmentation d'un fournisseur d'authentification personnalis. Comme l'authentification et l'autorisation sont des concepts spars, cette extension sera indpendante du fournisseur d'utilisateur , et fonctionnera avec les fournisseurs d'utilisateur de votre application, qu'ils soient stocks en mmoire, dans une base de donnes, ou n'importe o ailleurs. A la rencontre de WSSE Le chapitre suivant dmontre comment crer un fournisseur d'authentification personnalis pour une authentification WSSE. Le protocole de scurit pour WSSE fournit plusieurs avantages concernant la scurit : 1. Encryptage du Nom d'utilisateur / Mot de passe 2. Scurit contre les attaques de type replay 3. Aucune configuration de serveur web requise WSSE est trs utile pour scuriser des services web, qu'ils soient SOAP ou REST. Il existe de nombreuses et trs bonnes documentations sur WSSE 1 , mais cet article ne va pas se focaliser sur le protocole de scurit, mais plutt sur la manire dont un protocole personnalis peut tre ajout votre application Symfony2. La base de WSSE est qu'un en-tte de requte est vrifi pour y trouver des informations de connexions encryptes en utilisant un timestamp et un nonce 2 , et est authentifi pour l'utilisateur demand en utilisant un digest du mot de passe. 1. http://www.xml.com/pub/a/2003/12/17/dive.html 2. http://en.wikipedia.org/wiki/Cryptographic_nonce PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 255 Listing 70-1 WSSE supporte aussi la validation par cl d'application, qui est utile pour les services web, mais qui est hors-sujet dans ce chapitre. Le Token Le rle du token dans le contexte de scurit de Symfony2 est important. Un token reprsente les donnes d'authentification de l'utilisateur prsentes dans la requte. Une fois qu'une requte est authentifie, le token conserve les donnes de l'utilisateur, et dlivre ces donnes au travers du contexte de scurit. Premirement, vous allez crer votre classe token. Cela permettra de passer toutes les informations pertinentes votre fournisseur d'authentification. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // src/Acme/DemoBundle/Security/Authentication/Token/WsseUserToken.php namespace Acme\DemoBundle\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; class WsseUserToken extends AbstractToken { public $created; public $digest; public $nonce; public function __construct(array $roles = array()) { parent::__construct($roles); // Si l'utilisateur a des rles, on le considre comme authentifi $this->setAuthenticated(count($roles) > 0); } public function getCredentials() { return ''; } } La classe WsseUserToken tend la classe du composant de scurit AbstractToken 3 , qui fournit la fonctionnalit basique du token. Implmentez l'interface TokenInterface 4 dans chaque classe que vous souhaitez utiliser comme token. Le Listener Ensuite, vous avez besoin d'un listener pour couter le contexte de scurit. Le listener est charg de transmettre les requtes au pare-feu et d'appeler le fournisseur d'authentification. Un listener doit tre une instance de ListenerInterface 5 . Un listener de scurit devrait grer l'vnement GetResponseEvent 6 , et dfinir un token authentifi dans le contexte de scurit en cas de succs. 3. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.html 4. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.html 5. http://api.symfony.com/2.3/Symfony/Component/Security/Http/Firewall/ListenerInterface.html 6. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/Event/GetResponseEvent.html PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 256 Listing 70-2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 // src/Acme/DemoBundle/Security/Firewall/WsseListener.php namespace Acme\DemoBundle\Security\Firewall; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Http\Firewall\ListenerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Acme\DemoBundle\Security\Authentication\Token\WsseUserToken; class WsseListener implements ListenerInterface { protected $securityContext; protected $authenticationManager; public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager) { $this->securityContext = $securityContext; $this->authenticationManager = $authenticationManager; } public function handle(GetResponseEvent $event) { $request = $event->getRequest(); $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/'; if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { return; } $token = new WsseUserToken(); $token->setUser($matches[1]); $token->digest = $matches[2]; $token->nonce = $matches[3]; $token->created = $matches[4]; try { $authToken = $this->authenticationManager->authenticate($token); $this->securityContext->setToken($authToken); } catch (AuthenticationException $failed) { // ... you might log something here // To deny the authentication clear the token. This will redirect to the login page. // $this->securityContext->setToken(null); // return; // Deny authentication with a '403 Forbidden' HTTP response $response = new Response(); $response->setStatusCode(403); $event->setResponse($response); } PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 257 Listing 70-3 } } Ce listener vrifie l'en-tte X-WSSE attendu dans la rponse, fait correspondre la valeur retourne pour l'information WSSE attendue, cre un token utilisant cette information, et passe le token au gestionnaire d'authentification. Si la bonne information n'est pas fournie, ou si le gestionnaire d'authentification lance une AuthenticationException 7 , alors une rponse 403 est retourne. Une classe non utilise ci-dessus, la classe AbstractAuthenticationListener 8 , est une classe de base trs utile qui fournit certaines fonctionnalits communes pour les extensions de scurit. Ceci inclut le fait de maintenir le token dans la session, fournir des gestionnaires en cas de succs/chec, des URLs de formulaire de login, et plus encore. Comme WSSE ne requiert pas de maintenir les sessions d'authentification ou les formulaires de login, cela ne sera pas utilis dans cet exemple. Le Fournisseur d'Authentification Le fournisseur d'authentification va effectuer la vrification du WsseUserToken. C'est--dire que le fournisseur va vrifier que la valeur de l'en-tte Created est valide dans les cinq minutes, que la valeur de l'en-tte Nonce est unique dans les cinq minutes, et que la valeur de l'en-tte PasswordDigest correspond au mot de passe de l'utilisateur. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // src/Acme/DemoBundle/Security/Authentication/Provider/WsseProvider.php namespace Acme\DemoBundle\Security\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\NonceExpiredException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Acme\DemoBundle\Security\Authentication\Token\WsseUserToken; class WsseProvider implements AuthenticationProviderInterface { private $userProvider; private $cacheDir; public function __construct(UserProviderInterface $userProvider, $cacheDir) { $this->userProvider = $userProvider; $this->cacheDir = $cacheDir; } public function authenticate(TokenInterface $token) { $user = $this->userProvider->loadUserByUsername($token->getUsername()); if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { $authenticatedToken = new WsseUserToken($user->getRoles()); $authenticatedToken->setUser($user); 7. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Exception/AuthenticationException.html 8. http://api.symfony.com/2.3/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.html PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 258 Listing 70-4 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 return $authenticatedToken; } throw new AuthenticationException('The WSSE authentication failed.'); } protected function validateDigest($digest, $nonce, $created, $secret) { // Expire le timestamp aprs 5 minutes if (time() - strtotime($created) > 300) { return false; } // Valide que le nonce est unique dans les 5 minutes if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) { throw new NonceExpiredException('Previously used nonce detected'); } file_put_contents($this->cacheDir.'/'.$nonce, time()); // Valide le Secret $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); return $digest === $expected; } public function supports(TokenInterface $token) { return $token instanceof WsseUserToken; } } La classe AuthenticationProviderInterface 9 requiert une mthode authenticate sur le token de l'utilisateur ainsi qu'une mthode supports, qui dit au gestionnaire d'authentification d'utiliser ou non ce fournisseur pour le token donn. Dans le cas de fournisseurs multiples, le gestionnaire d'authentification se dplacera alors jusqu'au prochain fournisseur dans la liste. La Factory ( l'usine en franais) Vous avez cr un token personnalis, un listener personnalis, et un fournisseur personnalis. Maintenant, vous avez besoin de les relier tous ensemble. Comment mettez-vous votre fournisseur disposition de votre configuration de scurit ? La rponse est : en utilisant une factory. Une factory est l o vous intervenez dans le composant de scurit en lui disant le nom de votre fournisseur ainsi que toutes ses options de configuration disponibles. Tout d'abord, vous devez crer une classe qui implmente SecurityFactoryInterface 10 . 1 2 3 // src/Acme/DemoBundle/DependencyInjection/Security/Factory/WsseFactory.php namespace Acme\DemoBundle\DependencyInjection\Security\Factory; 9. http://api.symfony.com/2.3/Symfony/Component/Security/Core/Authentication/Provider/AuthenticationProviderInterface.html 10. http://api.symfony.com/2.3/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 259 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; class WsseFactory implements SecurityFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.wsse.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) ->replaceArgument(0, new Reference($userProvider)) ; $listenerId = 'security.authentication.listener.wsse.'.$id; $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener')); return array($providerId, $listenerId, $defaultEntryPoint); } public function getPosition() { return 'pre_auth'; } public function getKey() { return 'wsse'; } public function addConfiguration(NodeDefinition $node) { } } La SecurityFactoryInterface 11 requiert les mthodes suivantes : la mthode create, qui ajoute le listener et le fournisseur d'authentification au conteneur d'Injection de Dpendances pour le contexte de scurit appropri ; la mthode getPosition, qui doit tre de type pre_auth, form, http et remember_me et qui dfinit le moment auquel le fournisseur est appel ; la mthode getKey qui dfinit la cl de configuration utilise pour rfrencer le fournisseur ; la mthode addConfiguration, qui est utilise pour dfinir les options de configuration en dessous de la cl de configuration dans votre configuration de scurit. Comment dfinir les options de configuration sera expliqu plus tard dans ce chapitre. Une classe non utilise dans cet exemple, AbstractFactory 12 , est une classe de base trs utile qui fournit certaines fonctionnalits communes pour les factories de scurit. Cela pourrait tre utile lors de la dfinition d'un fournisseur d'authentification d'un type diffrent. 11. http://api.symfony.com/2.3/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 260 Listing 70-5 Listing 70-6 Maintenant que vous avez cr une classe factory, la cl wsse peut tre utilise comme un pare-feu dans votre configuration de scurit. Vous vous demandez peut-tre pourquoi avez vous besoin d'une classe factory spciale pour ajouter des listeners et fournisseurs un conteneur d'injection de dpendances ? . C'est une trs bonne question. La raison est que vous pouvez utiliser votre pare-feu plusieurs fois afin de scuriser plusieurs parties de votre application. Grce cela, chaque fois que votre pare-feu sera utilis, un nouveau service sera cr dans le conteneur d'injection de dpendances. La factory est ce qui cre ces nouveaux services. Configuration Il est temps de voir votre fournisseur d'authentification en action. Vous allez avoir besoin de faire quelques petites choses afin qu'il fonctionne. La premire chose est d'ajouter les services ci-dessus dans le conteneur d'injection de dpendances. Votre classe factory ci-dessus fait rfrence des IDs de service qui n'existent pas encore : wsse.security.authentication.provider et wsse.security.authentication.listener. Il est temps de dfinir ces services. # src/Acme/DemoBundle/Resources/config/services.yml services: wsse.security.authentication.provider: class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider arguments: ['', %kernel.cache_dir%/security/nonces] wsse.security.authentication.listener: class: Acme\DemoBundle\Security\Firewall\WsseListener arguments: [@security.context, @security.authentication.manager] Maintenant que vos services sont dfinis, informez votre contexte de scurit de l'existence de votre factory dans la classe de votre bundle : New in version 2.1: Avant 2.1, la factory ci-dessous tait ajoute via le fichier security.yml la place. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/DemoBundle/AcmeDemoBundle.php namespace Acme\DemoBundle; use Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; class AcmeDemoBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $extension = $container->getExtension('security'); $extension->addSecurityListenerFactory(new WsseFactory()); 12. http://api.symfony.com/2.3/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.html PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 261 Listing 70-7 Listing 70-8 Listing 70-9 16 17 } } Vous avez termin ! Vous pouvez maintenant dfinir des parties de votre application comme tant sous la protection de WSSE. 1 2 3 4 5 security: firewalls: wsse_secured: pattern: /api/.* wsse: true Flicitations ! Vous avez crit votre tout premier fournisseur d'authentification de scurit personnalis ! Un Petit Extra Que diriez-vous de rendre votre fournisseur d'authentification WSSE un peu plus excitant ? Les possibilits sont sans fin. Voyons comment nous pouvons apporter plus d'clat tout cela ! Configuration Vous pouvez ajouter des options personnalises sous la cl wsse de votre configuration de scurit. Par exemple, le temps allou avant que l'en-tte Created expire est, par dfaut, 5 minutes. Rendez cela configurable, afin que diffrents pares-feu puissent avoir des longueurs de timeout diffrentes. Vous allez tout d'abord avoir besoin d'diter WsseFactory puis ensuite de dfinir la nouvelle option dans la mthode addConfiguration. 1 2 3 4 5 6 7 8 9 10 11 12 class WsseFactory implements SecurityFactoryInterface { // ... public function addConfiguration(NodeDefinition $node) { $node ->children() ->scalarNode('lifetime')->defaultValue(300) ->end(); } } Maintenant, dans la mthode create de la factory, l'argument $config va contenir une cl lifetime , dclare 5 minutes (300 secondes) moins qu'elle soit dfinie ailleurs dans la configuration. Passez cet argument votre fournisseur d'authentification afin qu'il l'utilise. 1 2 3 4 5 6 7 8 9 class WsseFactory implements SecurityFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.wsse.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 262 Listing 70-10 10 11 12 13 14 15 ->replaceArgument(0, new Reference($userProvider)) ->replaceArgument(2, $config['lifetime']); // ... } // ... } Vous allez aussi avoir besoin d'ajouter un troisime argument la configuration du service wsse.security.authentication.provider, qui peut tre vide, mais qui sera rempli avec la valeur lifetime dans la factory. La classe WsseProvider va maintenant avoir besoin d'accepter un troisime argument dans son constructeur - la valeur lifetime - qu'elle devrait utiliser la place des 300 secondes codes en dur. Ces deux tapes ne sont pas montres ici. La valeur lifetime de chaque requte wsse est maintenant configurable, et peut tre dfinie par quelconque valeur que ce soit par pare-feu. 1 2 3 4 5 security: firewalls: wsse_secured: pattern: /api/.* wsse: { lifetime: 30 } Le reste dpend de vous ! N'importe quels autres points de configuration peuvent tre dfinis dans la factory et consomm ou pass d'autres classes dans le conteneur. PDF brought to you by generated on June 26, 2014 Chapter 70: Comment crer un Fournisseur d'Authentification Personnalis | 263 Listing 71-1 Listing 71-2 Chapter 71 Comment changer le comportement par dfaut du chemin cible Par dfaut, le composant de scurit conserve l'information de l'URI de la dernire requte dans une variable de session nomme _security.target_path. Lors d'une connexion russie, l'utilisateur est redirig vers ce chemin afin de l'aider continuer sa visite en le renvoyant vers la dernire page connue qu'il a parcourue. Dans certains cas, ce n'est pas la meilleure solution. Par exemple, quand l'URI de la dernire requte est une mthode POST HTTP avec une route configure pour accepter seulement une mthode POST, l'utilisateur est redirig vers cette route et se retrouvera confront invitablement une erreur 404. Pour contourner ce comportement, vous auriez simplement besoin d'tendre la classe ExceptionListener et surcharger la mthode par dfaut nomme setTargetPath(). Tout d'abord, surchargez le paramtre security.exception_listener.class dans votre fichier de configuration. Cela peut tre effectu depuis votre fichier de configuration principal (dans app/config) ou depuis un fichier de configuration import depuis un bundle : 1 2 3 4 # src/Acme/HelloBundle/Resources/config/services.yml parameters: # ... security.exception_listener.class: Acme\HelloBundle\Security\Firewall\ExceptionListener Ensuite, crez votre propre ExceptionListener: 1 2 3 4 5 6 7 8 9 10 // src/Acme/HelloBundle/Security/Firewall/ExceptionListener.php namespace Acme\HelloBundle\Security\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Firewall\ExceptionListener as BaseExceptionListener; class ExceptionListener extends BaseExceptionListener { protected function setTargetPath(Request $request) { PDF brought to you by generated on June 26, 2014 Chapter 71: Comment changer le comportement par dfaut du chemin cible | 264 11 12 13 14 15 16 17 18 19 20 // Ne conservez pas de chemin cible pour les requtes XHR et non-GET // Vous pouvez ajouter n'importe quelle logique supplmentaire ici // si vous le voulez if ($request->isXmlHttpRequest() || 'GET' !== $request->getMethod()) { return; } $request->getSession()->set('_security.target_path', $request->getUri()); } } Ajoutez plus ou moins de logique ici comme votre scnario le requiert ! PDF brought to you by generated on June 26, 2014 Chapter 71: Comment changer le comportement par dfaut du chemin cible | 265 Listing 72-1 Chapter 72 Comment utiliser le Serializer Srialiser et dsrialiser vers et depuis des objets et diffrents formats (ex JSON ou XML) est un sujet trs complexe. Symfony est fourni avec un Composant Serializer, qui vous donne certains outils pour rsoudre vos problmes. En fait, avant de commencer, familiarisez vous avec le srialiseur, les normaliseurs et les encodeurs en lisant la documentation du Serializer. Vous pouvez galement vous renseigner sur le JMSSerializerBundle 1 qui tend certaines des fonctionnalits offertes par le Serializer de Symfony. Activer le Serializer New in version 2.3: Le Serializer a toujours exist dans Symfony mais, avant la version 2.3, vous deviez construire le service serializer vous mme. Le service serializer n'est pas disponible par dfaut. Pour le rendre disponible, vous devez l'activer dans votre configuration : 1 2 3 4 5 # app/config/config.yml framework: # ... serializer: enabled: true 1. http://jmsyst.com/bundles/JMSSerializerBundle PDF brought to you by generated on June 26, 2014 Chapter 72: Comment utiliser le Serializer | 266 Listing 72-2 Ajouter des Normaliseurs et des Encodeurs Une fois activ, le service serializer sera disponible dans le conteneur et sera charg avec deux encodeurs (JsonEncoder 2 et XmlEncoder 3 ) mais aucun normaliseurs, ce qui veut dire que vous devrez charger le vtre. Vous pouvez charger des normaliseurs et/ou des encodeurs en les taggant comme serializer.normalizer et serializer.encoder. Il est galement possible de dfinir la priorit du tag pour influencer l'ordre dans lequel il seront pris. Voici un exemple de chargement du GetSetMethodNormalizer 4 : 1 2 3 4 5 6 # app/config/config.yml services: get_set_method_normalizer: class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer tags: - { name: serializer.normalizer } La classe GetSetMethodNormalizer 5 est inefficace de par sa conception. Ds que vous aurez un modle de donnes circulaire, une boucle infinie sera cre l'appel des getters. Vous tes donc invit ajouter vos propres normaliseurs pour rpondre ce besoin. 2. http://api.symfony.com/2.3/Symfony/Component/Serializer/Encoder/JsonEncoder.html 3. http://api.symfony.com/2.3/Symfony/Component/Serializer/Encoder/XmlEncoder.html 4. http://api.symfony.com/2.3/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.html 5. http://api.symfony.com/2.3/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.html PDF brought to you by generated on June 26, 2014 Chapter 72: Comment utiliser le Serializer | 267 Listing 73-1 Chapter 73 Comment crer un listener ( couteur en franais) d'vnement Symfony possde divers vnements et hooks qui peuvent tre utiliss pour dclencher un comportement personnalis dans votre application. Ces vnements sont lancs par le composant HttpKernel et peuvent tre consults dans la classe KernelEvents 1 . Afin de personnaliser un vnement avec votre propre logique, vous devez crer un service qui va agir en tant que listener d'vnement pour cet vnement. Dans cet article, nous allons crer un service qui agit en tant que Listener d'Exception, vous permettant de modifier comment les exceptions sont affiches par notre application. L'vnement KernelEvents::EXCEPTION est l'un des vnements du coeur du noyau: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // src/Acme/DemoBundle/Listener/AcmeExceptionListener.php namespace Acme\DemoBundle\Listener; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class AcmeExceptionListener { public function onKernelException(GetResponseForExceptionEvent $event) { // nous rcuprons l'objet exception depuis l'vnement reu $exception = $event->getException(); $message = 'My Error says: ' . $exception->getMessage() . ' with code: ' . $exception->getCode(); // personnalise notre objet rponse pour afficher les dtails de notre exception $response = new Response(); $response->setContent($message); // HttpExceptionInterface est un type d'exception spcial qui 1. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/KernelEvents.html PDF brought to you by generated on June 26, 2014 Chapter 73: Comment crer un listener ( couteur en franais) d'vnement | 268 Listing 73-2 Listing 73-3 22 23 24 25 26 27 28 29 30 31 32 // contient le code statut et les dtails de l'entte if ($exception instanceof HttpExceptionInterface) { $response->setStatusCode($exception->getStatusCode()); $response->headers->replace($exception->getHeaders()); } else { $response->setStatusCode(500); } // envoie notre objet rponse modifi l'vnement $event->setResponse($response); } } Chaque vnement reoit un objet de type $event lgrement diffrent. Pour l'vnement kernel.exception, c'est GetResponseForExceptionEvent 2 . Pour voir quel est le type d'objet que chaque listener d'vnement reoit, voyez KernelEvents 3 . Maintenant que la classe est cre, nous devons juste la dfinir en tant que service et notifier Symfony que c'est un listener de l'vnement kernel.exception en utilisant un tag spcifique : 1 2 3 4 5 6 # app/config/config.yml services: kernel.listener.your_listener_name: class: Acme\DemoBundle\Listener\AcmeExceptionListener tags: - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } Il y a une autre option priority pour le tag qui est optionnelle et qui a pour valeur par dfaut 0. Cette valeur peut aller de -255 255, et les listeners seront excutes dans l'ordre de leur priorit (le plus grand est le plus prioritaire). Cela est utile lorsque vous avez besoin de garantir qu'un listener est excut avant un autre. vnement de requte, vrification des types Une mme page peut faire plusieurs requtes (une requte principale et plusieurs sous-requtes, c'est pourquoi, lorsque vous travaillez avec l'vnement KernelEvents::REQUEST, vous pourriez avoir besoin de vrifier le type de la requte. Cela peut tre effectu trs facilement comme ceci: 1 2 3 4 5 6 7 8 9 // src/Acme/DemoBundle/Listener/AcmeRequestListener.php namespace Acme\DemoBundle\Listener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernel; class AcmeRequestListener { public function onKernelRequest(GetResponseEvent $event) 2. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.html 3. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/KernelEvents.html PDF brought to you by generated on June 26, 2014 Chapter 73: Comment crer un listener ( couteur en franais) d'vnement | 269 10 11 12 13 14 15 16 17 18 { if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) { // ne rien faire si ce n'est pas la requte principale return; } // ... } } Deux types de requte sont disponibles dans l'interface HttpKernelInterface 4 : HttpKernelInterface::MASTER_REQUEST et HttpKernelInterface::SUB_REQUEST. 4. http://api.symfony.com/2.3/Symfony/Component/HttpKernel/HttpKernelInterface.html PDF brought to you by generated on June 26, 2014 Chapter 73: Comment crer un listener ( couteur en franais) d'vnement | 270 Chapter 74 Comment travailler avec les champs d'applications ( scopes en anglais) Nous traiterons ici des champs d'applications, un sujet d'un niveau avanc en relation avec Service Container. Si vous avez dj observ une erreur mentionnant le terme scopes lors de la cration de vos services, ou avez eu besoin de crer des services qui dpendent du service request, cet article est fait pour vous. Comprendre les champs d'applications Le champs d'application d'un service contrle les intractions d'une instance de ce service avec son conteneur. Le composant d'injection de dpendance fourni deux champs d'applications gnriques : container (valeur par dfaut): la mme instance est utilise chaque requte. prototype: Une nouvelle instance est cre chaque requte. Le FrameworkBundle dfinit lui un troisime champ d'application : request. Celui-ci le lie la requte. Ainsi pour chaque sous-requte une nouvelle instance est cre. La consquence est qu'il est indisponible en dehors de la requte (en ligne de commande par example). Les champs d'applications ajoutent une contrainte sur les dpendances d'un service : un service ne peut pas dpendre d'un champ d'application moins large. Par exemple, si vous crez un service gnrique my_foo, mais essayez d'injecter le composant request, vous recevrez une ScopeWideningInjectionException 1 au moment de la compilation du conteneur. Pour plus de dtails, lisez les notes dans la barre latrale. 1. http://api.symfony.com/2.3/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.html PDF brought to you by generated on June 26, 2014 Chapter 74: Comment travailler avec les champs d'applications ( scopes en anglais) | 271 Listing 74-1 Champs d'application et dpendances Imaginez que vous ayez configur un service my_mailer, sans configurer de champs d'application pour ce service ; par dfaut il sera rgl sur container. Ainsi chaque fois que vous appellerez le conteneur du service my_mailer, vous recevrez le mme objet. Ce qui est habituel dans l'utilisation d'un service. Imaginez cependant que vous ayez besoin du service request dans votre service my_mailer, peut tre parce que vous lisez une URL de la requte courante. Vous l'ajoutez comme un argument du constructeur. Observons quelles pourraient tre les consquences : Quand vous appelez my_mailer, une instance de my_mailer (appelons le MailerA) est cre et un service request (RequestA) lui est envoy. Facile! Vous effectuez maintenant une sous-requte dans Symfony, ce qui est une faon sympatique de dire que vous avez appelez, par exemple, la fonction twig {% render ... %}, partir d'un autre contrleur. En interne, l'ancien service request (RequestA) est ainsi remplac par une nouvelle instance (RequestB). Cela se droule en arrire-plan et est tout fait normal. Dans votre contrleur intgr, vous interrogez une nouvelle fois le service my_mailer. Votre service ayant le champ d'application container, la mme instance (MailerA) est rutilise. Et voil le problme : l'instance MailerA contient toujours l'ancien objet RequestA, qui ne correspond plus maintenant l'objet requte mis jour (RequestB est maintenant le service courant request). C'est subtile mais l'erreur pourrait engendrer des problmes majeurs, et cela explique pourquoi cela est interdit. Ainsi, voil pourquoi les champs d'applications existent, et comment il peuvent causer des problmes. En continuant cette lecture nous vous indiquerons les solutions prconises. Un service peut bien entendu dpendre d'un service provenant d'un champ d'application plus tendu. Configurer le champ d'application dans la dfinition Le champ d'application d'un service est indiqu dans la dfinition de ce service l'aide du paramtre scope : 1 2 3 4 5 # src/Acme/HelloBundle/Resources/config/services.yml services: greeting_card_manager: class: Acme\HelloBundle\Mail\GreetingCardManager scope: request Si vous n'indiquez pas ce paramtre, il sera li par dfaut au conteneur, ce qui est le fonctionnement habituel d'un service. A moins que votre service ne dpende d'un autre service qui soit dans un champ d'application plus restreint (le plus courant tant request), vous n'aurez probablement pas modifier votre configuration. PDF brought to you by generated on June 26, 2014 Chapter 74: Comment travailler avec les champs d'applications ( scopes en anglais) | 272 Listing 74-2 Listing 74-3 Utiliser un service provenant d'un champ d'application restreint Si votre service dpend d'un autre service au champ d'application dtermin, la meilleure solution est de dfinir le mme champ d'application pour celui-ci (ou un champ d'application encore plus restreint). Habituellement, cela implique de placer votre service dans le champ d'application request. Mais cel n'est pas toujours possible (par exemple, une extension twig doit tre dans le champ d'application conteneur au regard de lenvironnement Twig dont elle est dpendante). Dans ces cas de figure, vous devrez configurer votre conteneur en tant que service et charger les dpendances provenant d'un champ d'application restreint chaque appel, afin d'tre certain d'obtenir les instances mises jour: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // src/Acme/HelloBundle/Mail/Mailer.php namespace Acme\HelloBundle\Mail; use Symfony\Component\DependencyInjection\ContainerInterface; class Mailer { protected $container; public function __construct(ContainerInterface $container) { $this->container = $container; } public function sendEmail() { $request = $this->container->get('request'); // Utilisez la requte ici } } Faites attention ne pas enregistrer la requte dans une proprit de votre objet pour un appel futur ; cela engendrerait les mmes inconsistances que celles dcrites prcdemment (except que dans ce cas, Symfony ne pourrait dtecter cette erreur). La configuration du service pour cette classe : 1 2 3 4 5 6 7 8 9 10 # src/Acme/HelloBundle/Resources/config/services.yml parameters: # ... my_mailer.class: Acme\HelloBundle\Mail\Mailer services: my_mailer: class: "%my_mailer.class%" arguments: - "@service_container" # scope: container can be omitted as it is the default Injecter le container entier dans un service est gnralement proscrire (injectez seulement les paramtres utiles). Dans quelques rares cas, cela est ncessaire quand vous avez un service dans un champ d'application container qui a besoin d'un service du champ d'application request. PDF brought to you by generated on June 26, 2014 Chapter 74: Comment travailler avec les champs d'applications ( scopes en anglais) | 273 Si vous dfinissez un contrleur comme un service, alors vous pourrez appelez l'objet Request sans injecter le conteneur comme un argument de votre mthode action. Voir La Requte en tant qu'argument du Contrleur pour plus de dtails. PDF brought to you by generated on June 26, 2014 Chapter 74: Comment travailler avec les champs d'applications ( scopes en anglais) | 274 Listing 75-1 Chapter 75 Comment travailler avec les Passes de Compilation dans les Bundles Les passes de compilation vous donnent l'opportunit de manipuler d'autres dfinitions de service qui ont t dfinies via le conteneur de service. Pour savoir comment les crer, vous pouvez lire la section des composants Compiler le Conteneur . Pour dfinir une passe de compilation depuis un bundle, vous devez l'ajouter la mthode build se situant dans la classe du bundle: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // src/Acme/MailerBundle/AcmeMailerBundle.php namespace Acme\MailerBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Acme\MailerBundle\DependencyInjection\Compiler\CustomCompilerPass; class AcmeMailerBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new CustomCompilerPass()); } } L'un des cas d'utilisation les plus frquents des passes de compilation est lorsque vous travaillez avec des services taggs (apprenez-en plus propos des tags en lisant la section sur les composants Travailler avec des Services Taggs ). Si vous utilisez des tags personnaliss dans un bundle, alors par convention, les noms de tag se constituent du nom du bundle (en minuscules, avec des tirets du bas en tant que sparateurs), suivi par un point, et finalement le nom rel . Par exemple, si vous voulez introduire un tag transport dans votre AcmeMailerBundle, vous devriez l'appeler acme_mailer.transport. PDF brought to you by generated on June 26, 2014 Chapter 75: Comment travailler avec les Passes de Compilation dans les Bundles | 275 Listing 76-1 Listing 76-2 Chapter 76 Exemple de Session Proxy Le mcanisme de proxy de session possde une varit d'utilisations et cet exemple prsente deux utilisations communes. Plutt que d'injecter un session handler comme d'habitude, un handler est inject dans le proxy et est enregistr avec un pilote de stockage de session 1 2 3 4 5 6 use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; $proxy = new YourProxy(new PdoSessionHandler()); $session = new Session(new NativeSessionStorage(array(), $proxy)); Plus bas, vous apprendrez deux rels exemples qui peuvent tre utiliss pour la classe YourProxy : le chiffrement des donnes de session et les sessions invites en lecture seules. Chiffrement des Donnes en Session Si vous vouliez chiffrer les donnes en session, vous pouvez utiliser le proxy pour chiffrer ou dchiffrer la session comme suit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; class EncryptedSessionProxy extends SessionHandlerProxy { private $key; public function __construct(\SessionHandlerInterface $handler, $key) { $this->key = $key; parent::__construct($handler); } public function read($id) PDF brought to you by generated on June 26, 2014 Chapter 76: Exemple de Session Proxy | 276 Listing 76-3 15 16 17 18 19 20 21 22 23 24 25 26 27 { $data = parent::read($id); return mcrypt_decrypt(\MCRYPT_3DES, $this->key, $data); } public function write($id, $data) { $data = mcrypt_encrypt(\MCRYPT_3DES, $this->key, $data); return parent::write($id, $data); } } Les sessions invites en lecture seule Il y a quelques applications o une session est requise pour les utilisateurs invits, mais il n'y a pas spcialement besoin de persister cette session. Dans ce cas vous pouvez intercepter la session avant qu'elle ne soit crite 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use Foo\User; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; class ReadOnlyGuestSessionProxy extends SessionHandlerProxy { private $user; public function __construct(\SessionHandlerInterface $handler, User $user) { $this->user = $user; parent::__construct($handler); } public function write($id, $data) { if ($this->user->isGuest()) { return; } return parent::write($id, $data); } } PDF brought to you by generated on June 26, 2014 Chapter 76: Exemple de Session Proxy | 277 Listing 77-1 Chapter 77 Faire que la Locale soit "persistente" durant la session de l'utilisateur Avant Symfony 2.1, la locale tait enregistre dans un attribut de session appel _locale. Depuis 2.1, c'est stock dans la Request, ce qui signifie qu'elle n'est pas "persistante" durant la requte de l'utilisateur. Dans cet article, vous allez apprendre comment faire que la locale de l'utilisateur soit persistante pour que, une fois fixe, la mme locale soit utilise pour chaque requte suivante. Crer un LocaleListener Pour simuler le fait que la locale soit stocke en session, vous devez crer et enregistrer un nouvel event listener (couteur d'vnement). Le listener ressemblera quelque chose comme le code qui suit. Typiquement, _locale est utilis comme paramtre du routing pour indiquer la locale, ceci dit la manire dont vous dterminez la locale dsire pour une requte n'est pas important 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // src/Acme/LocaleBundle/EventListener/LocaleListener.php namespace Acme\LocaleBundle\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class LocaleListener implements EventSubscriberInterface { private $defaultLocale; public function __construct($defaultLocale = 'en') { $this->defaultLocale = $defaultLocale; } public function onKernelRequest(GetResponseEvent $event) { PDF brought to you by generated on June 26, 2014 Chapter 77: Faire que la Locale soit "persistente" durant la session de l'utilisateur | 278 Listing 77-2 Listing 77-3 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 $request = $event->getRequest(); if (!$request->hasPreviousSession()) { return; } // on essaie de voir si la locale a t fixe dans le paramtre de routing _locale if ($locale = $request->attributes->get('_locale')) { $request->getSession()->set('_locale', $locale); } else { // si aucune locale n'a t fixe explicitement dans la requte, on utilise celle de la session $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale)); } } public static function getSubscribedEvents() { return array( // doit tre enregistr avant le Locale listener par dfaut KernelEvents::REQUEST => array(array('onKernelRequest', 17)), ); } } Puis enregistrez le listener : 1 2 3 4 5 6 services: acme_locale.locale_listener: class: Acme\LocaleBundle\EventListener\LocaleListener arguments: ["%kernel.default_locale%"] tags: - { name: kernel.event_subscriber } Et voil ! Maintenant clbrons en changeant la locale de l'utilisateur qui est persist au fil des requtes. Souvenez-vous, pour rcuprer la locale de l'utilisateur, utilisez toujours la mthode Request::getLocale 1 1 2 3 4 5 6 7 // depuis un controller... use Symfony\Component\HttpFoundation\Request; public function indexAction(Request $request) { $locale = $request->getLocale(); } 1. http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html#getLocale() PDF brought to you by generated on June 26, 2014 Chapter 77: Faire que la Locale soit "persistente" durant la session de l'utilisateur | 279 Listing 78-1 Listing 78-2 Chapter 78 Configurer le Dossier o les Fichiers pour les Sessions sont Enregistrs Par dfaut, la Symfony Standard Edition utilise les valeurs par dfaut de php.ini pour session.save_handler et session.save_path pour dterminer l'endroit o persister les donnes de session. Cela est possible grce la configuration suivante : 1 2 3 4 5 # app/config/config.yml framework: session: # handler_id fix null utilisera le session handler de php.ini handler_id: ~ Avec cette configuration, c'est vous de changer le fichier php.ini` pour changer l'endroit o les metadata de votre session seront stockes. Nanmoins, si vous disposez de la configuration suivante, Symfony stockera les donnes de la session dans des fichiers contenus dans le dossier de cache %kernel.cache_dir%/sessions. Cela signifie que lorsque vous purgez le cache, toutes les sessions courantes seront galement effaces : 1 2 3 # app/config/config.yml framework: session: ~ Utiliser un dossier diffrent pour enregistrer les donnes de session est l'une des mthodes permettant d'assurer que les sessions courantes ne seront pas perdues lorsque le vous nettoierez le cache Symfony. Une excellente mthode (la plus complexe) de gestion de session avec Symfony est d'utiliser un handler d'enregistrement de session diffrent. Consultez Configurer les Sessions et les gestionnaires de sauvegarde pour aller plus loin avec les handlers de sauvegarde de session. Il existe galement un cookbook pour le stockage de sessions en base de donnes. PDF brought to you by generated on June 26, 2014 Chapter 78: Configurer le Dossier o les Fichiers pour les Sessions sont Enregistrs | 280 Listing 78-3 Pour modifier le dossier dans lequel Symfony enregistre les donnes de session, vous avez uniquement besoin de changer la configuration du framework. Dans cet exemple, vous allez changer le dossier de session pour app/sessions : 1 2 3 4 5 # app/config/config.yml framework: session: handler_id: session.handler.native_file save_path: "%kernel.root_dir%/sessions" PDF brought to you by generated on June 26, 2014 Chapter 78: Configurer le Dossier o les Fichiers pour les Sessions sont Enregistrs | 281 Listing 79-1 Listing 79-2 Chapter 79 Combler une application legacy avec les sessions de Symfony New in version 2.3: Cette intgration avec la session PHP legacy a t introduite en Symfony 2.3. Si vous intgrez le framework Symfony full-stack dans une application legacy qui dmarre la session avec session_start(), vous pouvez encore tre capable d'utiliser la gestion de session de Symfony en utilisant le PHP Bridge session. Si l'application a fix son propre handler PHP d'enregistrement vous pouvez spcifier null le paramtre handler_id : 1 2 3 4 framework: session: storage_id: session.storage.php_bridge handler_id: ~ Autrement, si le problme est simplement que vous ne pouvez pas viter que l'application ne dmarre la session avec session_start(), vous pouvez encore utiliser un handler de sauvegarde bas sur Symfony en spcifiant le handler de sauvergarde comme dans l'exemple qui suit : 1 2 3 4 framework: session: storage_id: session.storage.php_bridge handler_id: session.handler.native_file PDF brought to you by generated on June 26, 2014 Chapter 79: Combler une application legacy avec les sessions de Symfony | 282 Si l'application legacy requiert son propose handler de sauvegarde de session, ne le surchargez pas. A la place, configurez handler_id: ~. Notez qu'un handler de sauvegarde ne peut pas tre chang une fois la session dmarre. Si l'application dmarre la session avant que Symfony ne soit initialis, le handler de sauvegarde sera dja fix. Dans ce cas, vous aurez besoin de handler_id: ~`. Surchargez le handler de sauvegarde uniquement si vous tes sr que l'application legacy peut utiliser le handler de sauvegarde de Symfony sans effets de bord et que la session n'ait pas t dmarre avant que Symfony n'ait t initialis. Pour plus de dtails, consultez /components/http_foundation/session_php_bridge. PDF brought to you by generated on June 26, 2014 Chapter 79: Combler une application legacy avec les sessions de Symfony | 283 Chapter 80 En quoi Symfony2 diffre de Symfony1 Le framework Symfony2 correspond une volution majeure si on le compare la premire version du framework. Heureusement, comme il est bas sur l'architecture MVC, les qualits utilises pour matriser un projet symfony1 continuent d'tre pertinentes pour dvelopper avec Symfony2. Bien entendu, app.yml n'existe plus mais le routage, les contrleurs et les templates sont toujours prsents. Dans ce chapitre, nous allons parcourir les diffrences entre symfony1 et Symfony2. Comme vous allez le voir, de nombreuses oprations sont effectues de manire lgrement diffrente. Vous apprcierez ces diffrences mineures car elles permettent des comportements stables, sans surprises, testables et une sparation claire des logiques utilises au sein de vos applications Symfony2. Ainsi, asseyez vous et relaxez vous pendant votre voyage du pass au prsent . Arborescence des rpertoires Quand vous observez un projet Symfony2 - par exemple, l'dition Standard Symfony2 1 - vous pouvez noter une structure de rpertoire trs diffrente de celle prsente dans symfony1. Les differences sont cependant superficielles. Le dossier app/ Dans symfony1, vos projets avaient une ou plusieurs applications, et chacune se situait dans le rpertoire apps/ (ex. apps/frontend). Par dfaut, dans Symfony2, vous avez une seule application reprsente par le dossier app/. Comme dans symfony1, le dossier app/ contient une configuration spcifique l'application. Il contient aussi le cache, les logs et les templates spcifiques l'application ainsi qu'une classe Kernel (AppKernel), qui est l'objet de base reprsentant l'application. A la diffrence de symfony1, trs peu de code PHP se trouve dans le dossier app/. Ce rpertoire n'est pas destin contenir les modules maison ou les fichiers des bibliothques comme il le faisait dans symfony1. Il correspond au rpertoire o se situent les fichiers de configuration et autres ressources gnrales (templates, fichiers de traduction). 1. https://github.com/symfony/symfony-standard PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 284 Listing 80-1 Le dossier src/ En rsum, votre code se trouve ici. Dans Symfony2, tout le code applicatif se trouve dans un bundle (plus ou moins quivalent aux plugins de symfony1) et, par dfaut, chaque bundle se place dans le dossier src. Sur cet aspect, le rpertoire src est un peu comme le dossier plugins dans symfony1, tout en comportant beaucoup plus de flexibilit. De plus, pendant que vos bundles sont dans le rpertoire src/, les bundles de bibliothques tierces se situeront dans le rpertoire vendor/. Afin d'avoir une ide plus prcise du rpertoire src/ , pensez d'abord une application symfony1. Premirement, certaines parties de votre code sont situes dans une ou plusieurs applications. Ces applications incluent, le plus souvent, des modules mais peuvent galement inclure n'importe quelle classe PHP. Vous avez probablement cr un fichier schema.yml dans le rpertoire config de votre projet et gnr de nombreux fichiers de modles. Finalement, afin d'utiliser certaines fonctionnalits communes, vous avez utilis de nombreuses bibliothques externes prsentes dans le rpertoire plugins/. En d'autres termes, le code qui fait fonctionner votre application se trouve plusieurs endroits. Dans Symfony2, la vie est plus simple car tout le code Symfony2 doit tre plac dans un bundle. Dans les projets symfony1, tout le code applicatif pourrait tre dplac dans un ou plusieurs plugins (ce qui est une bonne pratique en fait). Supposons que tous les modules, les classes PHP, les schmas, les configurations des routes, etc... soient dplaces dans un plugin, alors le dossier symfony1 plugins/ serait trs similaire au dossier Symfony2 src/. Rsumons de manire simple, le dossier src/ est l'endroit o placer tout le code, les ressources, les templates et tous les outils spcifiques votre projet. Le dossier vendor/ Le dossier vendor/ est grossirement l'quivalent du dossier lib/vendor/ prsent dans symfony1, qui tait le dossier conventionnel pour toutes les bibliothques et les bundles externes. Par dfaut, vous trouverez les fichiers de la bibliothque Symfony2 dans ce rpertoire, ainsi que de nombreuses autres bibliothques comme Doctrine2, Twig et Swiftmailer. Les bundles tierces intgrs Symfony2 se situent quelque part dans le rpertoire vendor/. Le dossier web/ Peu de choses ont chang dans le dossier web/. Les diffrences les plus notables sont l'absence des dossiers css/, js/ et images/. C'est intentionnel. Tout comme le code PHP, les ressources devraient galement se placer dans un bundle. Avec l'aide des commandes de la console, le dossier Resources/ public/ de chaque bundle est copi ou symboliquement li (ln -s) au dossier web/bundles/. Cela permet de conserver vos ressources organises dans votre bundle, tout en permettant de les rendre publiques. Afin de vous assurer que tous les bundles soient disponibles, lancez la commande suivante: 1 php app/console assets:install web Cette commande est l'quivalent Symfony2 de la commande symfony1 plugin:publish-assets. Auto-chargement Un des avantages d'un framework moderne est de ne jamais s'occuper des imports de fichiers. En utilisant un autoloader (chargeur automatique), vous pouvez faire rfrence n'importe quelle classe de votre PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 285 Listing 80-2 Listing 80-3 project et ainsi tre certain qu'elle est disponible. L'autoloader a chang dans Symfony2 afin d'tre plus universel, plus rapide, et moins dpendant du nettoyage du cache. Dans symfony1, le chargement automatique tait ralis en recherchant, dans tout le projet, la prsence de classes PHP et en mettant en cache cette information dans un gigantesque tableau. Ce tableau disait symfony1 les correspondances exactes entre les fichiers et les classes. Dans l'environnement de production, cela impliquait de nettoyer le cache lorsque des classes taient ajoutes ou dplaces. Dans Symfony2, une nouvelle classe - UniversalClassLoader - effectue ce travail. L'ide derrire l'autoloadeur est simple : le nom de votre classe (incluant l'espace de nom) doit correspondre avec le chemin du fichier contenant la classe. Prenez le FrameworkExtraBundle faisant partie de l'dition standard Symfony2 comme exemple: 1 2 3 4 5 6 7 8 9 namespace Sensio\Bundle\FrameworkExtraBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; // ... class SensioFrameworkExtraBundle extends Bundle { // ... } Le fichier lui mme est prsent dans vendor/sensio/framework-extra-bundle/Sensio/Bundle/ FrameworkExtraBundle/SensioFrameworkExtraBundle.php. Comme vous pouvez le voir, l'emplacement de ce fichier suit l'espace de nom de la classe. Plus prcisment, l'espace de nom Sensio\Bundle\FrameworkExtraBundle, correspond au rpertoire o le fichier doit tre trouv (vendor/ sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/). Cela s'explique par le fait que dans le fichier app/autoload.php, vous avec configur Symfony pour qu'il recherche l'espace de nom Sensio dans le rpertoire vendor/sensio: 1 2 3 4 5 6 7 // app/autoload.php // ... $loader->registerNamespaces(array( ..., 'Sensio' => __DIR__.'/../vendor/sensio/framework-extra-bundle', )); Si ce fichier ne se trouve pas cette position exacte, vous recevrez une erreur Class "Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle" does not exist.. Dans Symfony2, une erreur "class does not exist" implique que l'espace de nom de la classe incrimine et son emplacement physique ne correspondent pas. Plus simplement, Symfony2 recherche cette classe dans un emplacement prcis, mais cet emplacement n'existe pas (ou contient une classe diffrente). Pour qu'une classe soit charge automatiquement, vous n'avez jamais besoin de nettoyer le cache dans Symfony2. Comme mentionn prcdemment, pour que le chargement automatique fontionne, il a besoin de savoir que l'espace de nom Sensio se trouve dans le dossier vendor/bundles et que, par exemple, l'espace de nom Doctrine se trouve dans le dossier vendor/doctrine/orm/lib/. Cette association est entirement sous votre contrle via le fichier app/autoload.php. Si vous observez le contrleur HelloController de l'dition Standard de Symfony2 vous remarquerez qu'il est plac dans l'espace de nom Acme\DemoBundle\Controller. Cependant, l'espace de nom Acme n'est pas dfini dans le fichier app/autoload.php. En effet, par dfaut vous n'avez pas dfinir explicitement l'emplacement de vos bundles prsents l'intrieur du rpertoire src/. L'UniversalClassLoader est configur pour rechercher par dfaut dans le rpertoire src/ en utilisant la mthode registerNamespaceFallbacks: PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 286 Listing 80-4 Listing 80-5 Listing 80-6 Listing 80-7 1 2 3 4 5 6 // app/autoload.php // ... $loader->registerNamespaceFallbacks(array( __DIR__.'/../src', )); Utilisation de la console Dans symfony1, la console est dans le rpertoire racine de votre projet et est appele symfony: 1 php symfony Dans Symfony2, la console est maintenant dans le sous-dossier app et est appele console: 1 php app/console Applications Dans un projet symfony1, il est commun d'avoir plusieurs applications : une pour la partie front(frontend) et une pour la partie administrative (backend) par exemple. Dans un projet Symfony2, vous n'avez besoin de crer qu'une application (un blog, une application intranet, ...). Le plus souvent, si vous voulez crer une seconde application, vous devriez plutt crer un autre projet et partager certains bundles entre eux. Et si vous avez besoin de sparer la partie frontend de la partie backend de certains bundles, vous pouvez crer des sous-espaces de noms pour les contrleurs, des sous-rpertoires pour les templates, diffrentes configurations smantiques, sparer les configurations de routages, et bien plus encore. Bien sur, il n'y a rien de mal avoir plusieurs applications dans votre projet, c'est vous de dcider. Une deuxime application impliquerait un nouveau rpertoire, par exemple my_app/, avec la mme configuration que le rpertoire app/. Vous pouvez lire ce sujet la dfinition des termes Projet, Application, et Bundle dans le glossaire. Bundles et Plugins Dans un projet symfony1, un plugin pouvait contenir de la configuration, des modules, des bibliothques PHP, des ressources ou tout autre fichier en relation avec votre projet. Dans Symfony2, l'ide de plugin est remplace par celle de bundle . Un bundle est encore plus puissant qu'un plugin, la preuve le coeur du framework Symfony2 est compos d'une srie de bundles. Dans Symfony2, les bundles sont les citoyens de premire classe si flexibles que mme le coeur de Symfony2 est lui-mme un bundle. Dans symfony1, un plugin doit tre activ l'intrieur de la classe ProjectConfiguration: PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 287 Listing 80-8 Listing 80-9 Listing 80-10 Listing 80-11 1 2 3 4 5 // config/ProjectConfiguration.class.php public function setup() { $this->enableAllPluginsExcept(array(/* some plugins here */)); } Dans Symfony2, les bundles sont activs l'intrieur du noyau applicatif: 1 2 3 4 5 6 7 8 9 10 11 12 // app/AppKernel.php public function registerBundles() { $bundles = array( new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), ..., new Acme\DemoBundle\AcmeDemoBundle(), ); return $bundles; } Routage (routing.yml) et Configuration (config.yml) Dans symfony1, les fichiers de configurations routing.yml et app.yml taient automatiquement chargs depuis un plugin. Dans Symfony2, les configurations de routages et d'applications inclues dans un bundle doivent tre charges manuellement. Par exemple, pour inclure un fichier de routage partir d'un bundle appel AcmeDemoBundle, vous devez faire: 1 2 3 # app/config/routing.yml _hello: resource: "@AcmeDemoBundle/Resources/config/routing.yml" Cela chargera automatiquement les routes trouves dans le fichier Resources/config/routing.yml du bundle AcmeDemoBundle. La convention @AcmeDemoBundle` est un raccourci qui, en interne, est remplac par le chemin complet du bundle. Vous pouvez utiliser la mme stratgie pour charger une configuration provenant d'un bundle: 1 2 3 # app/config/config.yml imports: - { resource: "@AcmeDemoBundle/Resources/config/config.yml" } Dans Symfony2, la configuration ressemble au app.yml prsent dans symfony1, except qu'elle est mieux encadre. Dans app.yml, vous pouviez crer toutes les clefs dont vous aviez besoin. Par dfaut, ces entres taient dnues de sens et dpendaient entirement de comment vous les utilisiez dans votre application : 1 2 3 4 # un fichier app.yml provenant de symfony1 all: email: from_address: [email protected] Dans Symfony2, vous pouvez aussi crer des clefs arbitraires l'intrieur de la clef parameters de votre configuration: PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 288 Listing 80-12 Listing 80-13 1 2 parameters: email.from_address: [email protected] Vous pouvez maintenant accder cette valeur depuis votre contrleur, par exemple: 1 2 3 4 public function helloAction($name) { $fromAddress = $this->container->getParameter('email.from_address'); } En ralit, la configuration de Symfony2 est beaucoup plus puissante et est utilise principalement pour configurer les objets que vous pouvez utiliser. Pour plus d'informations, consultez le chapitre intitul Service Container . PDF brought to you by generated on June 26, 2014 Chapter 80: En quoi Symfony2 diffre de Symfony1 | 289 Listing 81-1 Listing 81-2 Listing 81-3 Listing 81-4 Chapter 81 Comment injecter des variables dans tous les modles (i.e. Variables Globales) Parfois vous voulez qu'une variable soit accessible dans tous les modles que vous utilisez. C'est possible l'intrieur du fichier app/config/config.yml: 1 2 3 4 5 # app/config/config.yml twig: # ... globals: ga_tracking: UA-xxxxx-x Maintenant, la variable ga_tracking est disponible dans tous les modles Twig : 1 <p>Our google tracking code is: {{ ga_tracking }} </p> C'est aussi simple que cela! Vous pouvez aussi tirer parti du systme intgr Paramtres de Service, qui vous permet d'isoler ou de rutiliser une valeur : ; app/config/parameters.yml [parameters] ga_tracking: UA-xxxxx-x 1 2 3 4 # app/config/config.yml twig: globals: ga_tracking: "%ga_tracking%" La mme variable est disponible exactement comme prcdemment. PDF brought to you by generated on June 26, 2014 Chapter 81: Comment injecter des variables dans tous les modles (i.e. Variables Globales) | 290 Des variables globales complexes Si vous voulez utiliser une variable globale plus complexe, comme un objet, alors vous devez utiliser une autre mthode. Ainsi vous aurez besoin de crer une Extension Twig et de retourner la variable globale comme une des valeurs du tableau retourn par la mthode getGlobals. PDF brought to you by generated on June 26, 2014 Chapter 81: Comment injecter des variables dans tous les modles (i.e. Variables Globales) | 291 Listing 82-1 Listing 82-2 Listing 82-3 Chapter 82 Comment utiliser et enregistrer des chemins Twig namespacs Habituellement, lorsque vous vous rfrez un template, vous allez utiliser le format MyBundle:Subdir:filename.html.twig (consultez Nommage de template et Emplacements). Twig offre galement nativement une fonctionnalit appele "chemins namespacs" ("namespaced paths" en anglais), et elle est intgre automatiquement dans vos bundles. Prenons les chemins suivants comme exemple : 1 2 {% extends "AcmeDemoBundle::layout.html.twig" %} {% include "AcmeDemoBundle:Foo:bar.html.twig" %} Avec les chemins namespacs, ce qui suit fonctionne galement : 1 2 {% extends "@AcmeDemo/layout.html.twig" %} {% include "@AcmeDemo/Foo/bar.html.twig" %} Les deux chemins sont valides et fonctionnel par dfaut en Symfony2. Comme bonus, la syntaxe namespace est plus rapide. Enregistrer vos propres namespaces Vous pouvez galement enregistrer vos propes namespaces. Supposez que vous utilisez une quelconque bibliothque tierce incluant des templates Twig placs dans vendor/acme/foo-bar/templates. Premirement, enregistrez un namespace pour pour ce dossier : PDF brought to you by generated on June 26, 2014 Chapter 82: Comment utiliser et enregistrer des chemins Twig namespacs | 292 Listing 82-4 1 2 3 4 5 # app/config/config.yml twig: # ... paths: "%kernel.root_dir%/../vendor/acme/foo-bar/templates": foo_bar Le namespace enregistr est appel foo_bar, se rfrant au dossier vendor/acme/foo-bar/templates. Supposant qu'il y est un fichier nomm sidebar.twig dans ce dossier, vous pouvez utiliser facilement : 1 {% include '@foo_bar/side.bar.twig' %} PDF brought to you by generated on June 26, 2014 Chapter 82: Comment utiliser et enregistrer des chemins Twig namespacs | 293 Listing 83-1 Listing 83-2 Chapter 83 Comment utiliser PHP plutt que Twig dans les templates Mme si Symfony2 utilise Twig en tant que moteur de rendu par dfaut, vous pouvez utiliser du code PHP si vous le dsirez. Ces deux moteurs de rendu sont en effet supports part gal au sein de Symfony2. Symfony2 ajoute mme PHP quelques possibilits utiles permettant d'crire des modles encore plus puissants. Rendu des templates PHP Si vous voulez utiliser le moteur de rendu PHP, il vous faut d'abord vous assurer d'activer celui-ci dans le fichier de configuration de votre application: 1 2 3 4 # app/config/config.yml framework: # ... templating: { engines: ['twig', 'php'] } Vous pouvez maintenant utiliser le moteur de rendu en utilisant des templates PHP plutt que des templates Twig simplement en utilisant l'extension .php dans le nom de vos templates la place de l'extension .twig. Le contrleur suivant dlivre ainsi le template index.html.php 1 2 3 4 5 6 7 8 // src/Acme/HelloBundle/Controller/HelloController.php // ... public function indexAction($name) { return $this->render('AcmeHelloBundle:Hello:index.html.php', array('name' => $name)); } PDF brought to you by generated on June 26, 2014 Chapter 83: Comment utiliser PHP plutt que Twig dans les templates | 294 Listing 83-3 Listing 83-4 Listing 83-5 Listing 83-6 Ou vous pouvez utiliser le raccourci @Template pour afficher le template par dfaut AcmeHelloBundle:Hello:index.html.php: 1 2 3 4 5 6 7 8 9 10 11 // src/Acme/HelloBundle/Controller/HelloController.php // ... /** * @Template(engine="php") */ public function indexAction($name) { return array('name' => $name); } Templates de dcorations Trs souvent, les templates au sein d'un mme projet partagent des composants communs comme l'en- tte bien connu ou le pied de page. Chez Symfony, nous aimons penser ce problme diffremment : un template peut tre dcor par un autre. Le template index.html.php est dcor par layout.html.php, grce l'appel de la mthode extend() : 1 2 3 4 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php --> <?php $view->extend('AcmeHelloBundle::layout.html.php') ?> Hello <?php echo $name ?>! La notation AcmeHelloBundle::layout.html.php vous parait peut tre familire ; c'est en effet la mme notation qui est utilise pour rfrencer un template l'intrieur d'un contrleur. La partie :: s'expliquant simplement par l'absence d'un sous-dossier correspondant habituellement au contrleur et qui sera donc cherch directement la racine du dossier views/. Maintenant, regardons d'un peu plus prs le fichier layout.html.php : 1 2 3 4 5 6 <!-- src/Acme/HelloBundle/Resources/views/layout.html.php --> <?php $view->extend('::base.html.php') ?> <h1>Hello Application</h1> <?php $view['slots']->output('_content') ?> Le dcorateur ou layout est lui-mme dcor par un autre (::base.html.php). Symfony2 supporte en effet de multiples niveaux de dcoration : un dcorateur peut lui-mme tre dcor par un autre, et cel indfiniment. Quand la partie bundle du nom du template est vide, les vues sont recherches dans le dossier app/Resources/views/. Ce dossier contient donc les vues globales utilises dans tout le projet. 1 2 3 4 5 6 7 8 <!-- app/Resources/views/base.html.php --> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title><?php $view['slots']->output('title', 'Hello Application') ?></title> </head> <body> PDF brought to you by generated on June 26, 2014 Chapter 83: Comment utiliser PHP plutt que Twig dans les templates | 295 Listing 83-7 Listing 83-8 Listing 83-9 Listing 83-10 9 10 11 <?php $view['slots']->output('_content') ?> </body> </html> Pour les deux dcorateurs, l'expression $view['slots']->output('_content') est remplace par le contenu du template fils, respectivement index.html.php et layout.html.php (voir la prochaine section sur les slots). Comme vous pouvez le voir, Symfony2 fourni des mthodes sur l'objet $view. Dans un template, la variable $view est toujours disponible et rfre un objet fournissant un ensemble de mthodes rendant le moteur de rendu puissant. Travailler avec les slots Un slot est un bout de code dfini dans un template et rutilisable dans tous les dcorateurs de ce template. Ainsi dans le template index.html.php un slot title correspond : 1 2 3 4 5 6 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php --> <?php $view->extend('AcmeHelloBundle::layout.html.php') ?> <?php $view['slots']->set('title', 'Hello World Application') ?> Hello <?php echo $name ?>! Le dcorateur de base a dj le code pour afficher le titre dans le header html : 1 2 3 4 5 <!-- app/Resources/views/base.html.php --> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title><?php $view['slots']->output('title', 'Hello Application') ?></title> </head> La mthode output() insert le contenu d'un slot et optionnellement prend une valeur par dfaut si le slot n'est pas dfini. _content est quand lui un slot special qui contient le rendu du template enfant. Pour les slots plus longs, il existe aussi une syntaxe tendue : 1 2 3 <?php $view['slots']->start('title') ?> Du code html sur de nombreuses lignes <?php $view['slots']->stop() ?> Inclure d'autres templates La meilleure faon de partager une partie d'un template est de dfinir un template qui pourra tre inclus dans d'autres. Crez un template hello.html.php : 1 2 <!-- src/Acme/HelloBundle/Resources/views/Hello/hello.html.php --> Hello <?php echo $name ?>! Et changez le template index.html.php pour qu'il comporte : PDF brought to you by generated on June 26, 2014 Chapter 83: Comment utiliser PHP plutt que Twig dans les templates | 296 Listing 83-11 Listing 83-12 Listing 83-13 1 2 3 4 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php --> <?php $view->extend('AcmeHelloBundle::layout.html.php') ?> <?php echo $view->render('AcmeHelloBundle:Hello:hello.html.php', array('name' => $name)) ?> La mthode render() value et retourne le contenu d'un autre template (c'est exactement la mme mthode que celle utilise dans le contrleur). Intgrer d'autre contrleurs Intgrer le rsultat d'un contrleur dans un template peut tre trs utile afin de factoriser certaines partie de l'application, en particulier lors de traitements Ajax, ou quand les templates intgrs ont besoin de certaines variables non-incluses dans le template principal. Si vous crez une action nomme fancy, et que vous voulez l'inclure dans le template index.html.php, utilisez simplement le code suivant : 1 2 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php --> <?php echo $view['actions']->render('AcmeHelloBundle:Hello:fancy', array('name' => $name, 'color' => 'green')) ?> Ici, la chane de caractres AcmeHelloBundle:Hello:fancy fait rfrence l'action fancy du contrleur Hello 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/Acme/HelloBundle/Controller/HelloController.php class HelloController extends Controller { public function fancyAction($name, $color) { // create some object, based on the $color variable $object = ...; return $this->render('AcmeHelloBundle:Hello:fancy.html.php', array('name' => $name, 'object' => $object)); } // ... } Mais o est dfini le tableau d'lments $view['actions'] ? Comme $view['slots'], c'est un template helper et la section suivante vous en apprendra plus son propos. Utiliser les templates helpers Le systme de rendu par template utilis par Symfony peut tre tendu facilement grace des helpers . Les helpers sont des objets PHP qui fournissent des possibilits utiles dans le contexte des templates. actions et slots sont ainsi deux des nombreux helpers intgrs dans Symfony2. Crer des liens entre les pages A l'intrieur d'une application web, crez des liens entre les pages ncessite d'utiliser des mthode propres l'application si l'on souhaite conserver une volutivit et une maintenabilit sans failles. Ainsi PDF brought to you by generated on June 26, 2014 Chapter 83: Comment utiliser PHP plutt que Twig dans les templates | 297 Listing 83-14 Listing 83-15 Listing 83-16 Listing 83-17 Listing 83-18 l'utilisation d'un helper router l'intrieur des template permet de gnrer des URLs bases sur la configuration du routage. De cette faon, toutes les URLs peuvent facilement tre mises jour directement en changeant simplement la configuration: 1 2 3 <a href="<?php echo $view['router']->generate('hello', array('name' => 'Thomas')) ?>"> Greet Thomas! </a> La mthode generate() prend comme arguments le nom de la route et un tableau de paramtres. Le nom de la route est la cl principale sous laquelle celle-ci est dfinie, les paramtres sont des valeurs remplaant les paramtres inclus dans celle-ci : 1 2 3 4 # src/Acme/HelloBundle/Resources/config/routing.yml hello: # The route name pattern: /hello/{name} defaults: { _controller: AcmeHelloBundle:Hello:index } Utiliser des assets : images, JavaScripts, et feuilles de style Que serait Internet sans images, sans JavaScript ou sans feuille de style ? Symfony2 fourni le tag assets pour les utiliser facilement : 1 2 3 <link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet" type="text/css" /> <img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" /> Les helpers assets ont pour but principal de rendre votre application plus portable. Grce ceux-ci, vous pouvez dplacer le rpertoire principal de votre application o vous le souhaitez l'intrieur d'un dossier web sans changer quoique ce soit dans le code de vos templates. Echappement des variables de sortie ( Output Escaping en anglais) Quand vous utilisez des templates, les variables peuvent tre conserves tant qu'elles ne sont pas affiches l'utilisateur: 1 <?php echo $view->escape($var) ?> Par dfaut, la mthode escape() assume que la variable est affiche dans un contexte HTML. Le second argument vous permet de dfinir le contexte. Par exemple, pour afficher cette variable dans un script JavaScript, il est possible d'utiliser le contexte js: 1 <?php echo $view->escape($var, 'js') ?> PDF brought to you by generated on June 26, 2014 Chapter 83: Comment utiliser PHP plutt que Twig dans les templates | 298 Listing 84-1 Chapter 84 Comment crire une Extension Twig personnalise La motivation principale d'crire une extension est de dplacer du code souvent utilis dans une classe rutilisable, comme par exemple ajouter le support pour l'internationalisation. Une extension peut dfinir des tags, des filtres, des tests, des oprateurs, des variables globales, des fonctions et des noeuds visiteurs. Crer une extension permet aussi une meilleure sparation du code qui est excut au moment de la compilation et du code ncessaire lors de l'excution. De ce fait, cela rend votre code plus rapide. Avant d'crire vos propres extensions, jetez un oeil au dpt officiel des extensions Twig 1 . Crer la Classe Extension Pour avoir votre fonctionnalit personnalise, vous devez crer en premier lieu une classe Extension Twig. En tant qu'exemple, nous allons crer un filtre prix afin de formatter un nombre donn en un prix: 1 2 3 4 5 6 7 8 9 10 // src/Acme/DemoBundle/Twig/AcmeExtension.php namespace Acme\DemoBundle\Twig; class AcmeExtension extends \Twig_Extension { public function getFilters() { return array( new \Twig_SimpleFilter('price', array($this, 'priceFilter')), ); 1. https://github.com/fabpot/Twig-extensions PDF brought to you by generated on June 26, 2014 Chapter 84: Comment crire une Extension Twig personnalise | 299 Listing 84-2 Listing 84-3 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 } public function priceFilter($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') { $price = number_format($number, $decimals, $decPoint, $thousandsSep); $price = '$' . $price; return $price; } public function getName() { return 'acme_extension'; } } Dans le mme style que les filtres personnaliss, vous pouvez aussi ajouter des fonctions personnalises et dfinir des variables globales. Dfinir une Extension en tant que Service Maintenant, vous devez informer le Conteneur de Service de l'existence de votre Extension Twig nouvellement cre : 1 2 3 4 5 6 <!-- src/Acme/DemoBundle/Resources/config/services.xml --> <services> <service id="acme.twig.acme_extension" class="Acme\DemoBundle\Twig\AcmeExtension"> <tag name="twig.extension" /> </service> </services> Gardez en mmoire que les Extensions Twig ne sont pas charges de manire paresseuse ( lazy loading en anglais). Cela signifie qu'il y a de grandes chances que vous obteniez une CircularReferenceException ou une ScopeWideningInjectionException si quelconques services (ou votre Extension Twig dans ce cas) sont dpendants du service de requte. Pour plus d'informations, jetez un oeil sur Comment travailler avec les champs d'applications ( scopes en anglais). Utiliser l'Extension personnalise Utiliser votre Extension Twig nouvellement cre n'est en rien diffrent des autres : 1 2 {# affiche $5,500.00 #} {{ '5500'|price }} Passez d'autres arguments votre filtre : PDF brought to you by generated on June 26, 2014 Chapter 84: Comment crire une Extension Twig personnalise | 300 Listing 84-4 1 2 {# affiche $5500,2516 #} {{ '5500.25155'|price(4, ',', '') }} En savoir plus Pour tudier le sujet des Extensions Twig plus en dtail, veuillez jeter un coup d'oeil la documentation des extensions Twig 2 . 2. http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension PDF brought to you by generated on June 26, 2014 Chapter 84: Comment crire une Extension Twig personnalise | 301 Listing 85-1 Listing 85-2 Chapter 85 Comment rendre un template sans passer par un contrleur Normalement, quand vous avez besoin de crer une page, vous devez crer un contrleur et rendre un template depuis ce contrleur. Mais si vous avez besoin d'afficher un simple template, qui ne ncessite pas de passage de paramtre, vous pouvez vous passer compltement de la cration d'un contrleur, en utilisant le contrleur intgr FrameworkBundle:Template:template. Par exemple, supposons que vous vouliez afficher un template AcmeBundle:Static:privacy.html.twig, qui ne ncessite aucun passage de variable. Vous pouvez le faire sans crer de contrleur. 1 2 3 4 5 acme_privacy: path: /privacy defaults: _controller: FrameworkBundle:Template:template template: 'AcmeBundle:Static:privacy.html.twig' Le contrleur FrameworkBundle:Template:template va simplement afficher le template que vous aurez dfinit comme paramtre template. Vous pouvez aussi, bien sr, utiliser cette astuce lors du rendu de contrleurs imbriqus dans un template. Le but de rendre un controleur depuis un template est typiquement de prparer des donnes pour un contrleur personalis. C'est probablement utile, uniquement, si vous voulez mettre partiellement en cache cette page (voir Mettre en cache les templates statiques). 1 {{ render(url('acme_privacy')) }} PDF brought to you by generated on June 26, 2014 Chapter 85: Comment rendre un template sans passer par un contrleur | 302 Listing 85-3 Mettre en cache les templates statiques New in version 2.2: La possibilit de mettre en cache les templates rendu par FrameworkBundle:Template:template est nouvelle en Symfony 2.2. Puisque les templates affichs par cette mthode sont souvent statiques, il pourrait tre judicieux de les mettre en cache. Par chance, c'est facile! En configurant quelques variables dans votre route, vous pourrez finement contrler la faon dont vos pages sont mise en cache: 1 2 3 4 5 6 7 acme_privacy: path: /privacy defaults: _controller: FrameworkBundle:Template:template template: 'AcmeBundle:Static:privacy.html.twig' maxAge: 86400 sharedMaxAge: 86400 Les valeurs de maxAge et sharedMaxAge sont utilises pour modifier l'objet Response cr dans le contrleur. Pour plus d'informations sur la mise en cache, voir /book/http_cache. Il existe galement un paramtre private (non prsent ici). Par dfaut, la rponse est faite de manire public (la rponse peut tre mise en cache, la fois par les caches privs et les caches publics) tant que les paramtres maxAge ou sharedMaxAge sont renseigns. Si elle est dfinie true la rponse devient priv (la rponse concerne un unique utilisateur et ne doit pas tre stocke dans les caches publics). PDF brought to you by generated on June 26, 2014 Chapter 85: Comment rendre un template sans passer par un contrleur | 303 Listing 86-1 Listing 86-2 Listing 86-3 Chapter 86 Comment simuler une authentification HTTP dans un Test Fonctionnel Si votre application requiert une authentification HTTP, vous pouvez transmettre le nom d'utilisateur et le mot de passe comme variable serveurs la mthode createClient(): 1 2 3 4 $client = static::createClient(array(), array( 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word', )); Vous pouvez aussi les surcharger directement dans l'objet requte: 1 2 3 4 $client->request('DELETE', '/post/12', array(), array(), array( 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word', )); Quand votre application utilise un form_login, vous pouvez effectuer vos tests plus simplement en permettant la configuration de test d'utiliser l'authentification HTTP. De cette manire, vous pouvez utiliser le code dcrit ci-dessus pour vous authentifier dans les tests, mais nanmoins conserver le fait que vos utilisateurs doivent se connecter via l'usuel form_login. Pour cela, dans la configuration de test vous devez inclure la cl http_basic dans votre pare-feu Symfony, l'intrieur du conteneur traitant le form_login : 1 2 3 4 5 # app/config/config_test.yml security: firewalls: your_firewall_name: http_basic: PDF brought to you by generated on June 26, 2014 Chapter 86: Comment simuler une authentification HTTP dans un Test Fonctionnel | 304 Listing 87-1 Chapter 87 Comment simuler une authentification avec un token dans un test fonctionnel Les requtes d'authentification dans les tests fonctionnels peuvent ralentir la suite de tests. Cela peut devenir un problme particulirement lorsque le form_login est utilis, puisque cela requiert des requtes additionnelles pour remplir et soumettre le formulaire. L'une des solutions est de configurer votre firewall utiliser http_basic en environnement de test comme expliqu dans Comment simuler une authentification HTTP dans un Test Fonctionnel. Une autre faon de faire serait de crer un token vous-mme et de le stocker dans une session. En faisant cela, vous devez vous assurer que le cookie appropri est envoy avec la requte. L'exemple suivant vous montre comment faire 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php namespace Acme\DemoBundle\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; class DemoControllerTest extends WebTestCase { private $client = null; public function setUp() { $this->client = static::createClient(); } public function testSecuredHello() { $this->logIn(); $this->client->request('GET', '/demo/secured/hello/Fabien'); $this->assertTrue($this->client->getResponse()->isSuccessful()); PDF brought to you by generated on June 26, 2014 Chapter 87: Comment simuler une authentification avec un token dans un test fonctionnel | 305 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $this->assertGreaterThan(0, $crawler->filter('html:contains("Hello Fabien")')->count()); } private function logIn() { $session = $this->client->getContainer()->get('session'); $firewall = 'secured_area'; $token = new UsernamePasswordToken('admin', null, $firewall, array('ROLE_ADMIN')); $session->set('_security_'.$firewall, serialize($token)); $session->save(); $cookie = new Cookie($session->getName(), $session->getId()); $this->client->getCookieJar()->set($cookie); } } La technique dcrite dans Comment simuler une authentification HTTP dans un Test Fonctionnel est plus propre et de ce fait prfre. PDF brought to you by generated on June 26, 2014 Chapter 87: Comment simuler une authentification avec un token dans un test fonctionnel | 306 Listing 88-1 Listing 88-2 Chapter 88 Comment tester les interactions de multiples clients Si vous avez besoin de simuler des interactions entre diffrents clients (pour un chat par exemple), crez plusieurs clients: 1 2 3 4 5 6 7 8 $harry = static::createClient(); $sally = static::createClient(); $harry->request('POST', '/say/sally/Hello'); $sally->request('GET', '/messages'); $this->assertEquals(201, $harry->getResponse()->getStatus()); $this->assertRegExp('/Hello/', $sally->getResponse()->getContent()); Cependant cela ne fonctionnera que si vous ne maintenez pas dans votre application un tat global et si aucune des bibliothques tierces n'utilise de techniques requrant des tats globaux. Dans ces cas vous devrez isoler les clients: 1 2 3 4 5 6 7 8 9 10 11 $harry = static::createClient(); $sally = static::createClient(); $harry->insulate(); $sally->insulate(); $harry->request('POST', '/say/sally/Hello'); $sally->request('GET', '/messages'); $this->assertEquals(201, $harry->getResponse()->getStatus()); $this->assertRegExp('/Hello/', $sally->getResponse()->getContent()); Les clients isols excute de manire transparente leur requte dans un processus PHP ddi et sain , et vitent donc quelconque effet de bord. PDF brought to you by generated on June 26, 2014 Chapter 88: Comment tester les interactions de multiples clients | 307 Comme un client isol est plus lent, vous pouvez garder un client dans le processus principal et n'isoler que les autres. PDF brought to you by generated on June 26, 2014 Chapter 88: Comment tester les interactions de multiples clients | 308 Listing 89-1 Chapter 89 Comment utiliser le Profiler dans un test fonctionnel Il est grandement recommand qu'un test fonctionnel ne teste que l'objet Response. Cependant si vous crivez des tests qui surveillent vos serveurs de productions, vous dsirerez peut-tre crire des tests sur les donnes provenant du profiler. En effet ce dernier fournit des mthodes efficaces, permettant de contrler de nombreux comportements et d'appliquer certaines mtriques. Le Profiler de Symfony2 collecte de nombreuses informations chaque requte. Utilisez celle-ci pour vrifier le nombre d'appels la base de donnes, le temps pass dans l'excution du framework, ... Avant de pouvoir crire des assertions, vous devez activer le profiler et vous assurez qu'il est disponible (il est activ par dfaut dans l'environnement de test ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class HelloControllerTest extends WebTestCase { public function testIndex() { $client = static::createClient(); // Active le profiler pour la prochaine requte // (cela ne fait rien si le profiler n'est pas disponible) $client->enableProfiler(); $crawler = $client->request('GET', '/hello/Fabien'); // Ecrire des assertions concernant la rponse // Vrifier que le profiler est activ if ($profile = $client->getProfile()) { // Vrifier le nombre de requtes $this->assertLessThan(10, $profile->getCollector('db')->getQueryCount()); // Vrifier le temps utilis par le framework $this->assertLessThan((500, $profile->getCollector('time')->getDuration()); } PDF brought to you by generated on June 26, 2014 Chapter 89: Comment utiliser le Profiler dans un test fonctionnel | 309 Listing 89-2 23 24 } } Si un test choue cause des donnes du profilage (trop d'appels la base de donnes par exemple), vous pouvez utiliser le Profiler Web afin danalyser la requte aprs que les tests soient termins. Cela est ralisable simplement en intgrant le token suivant dans les messages d'erreurs: 1 2 3 4 5 $this->assertLessThan( 30, $profile->get('db')->getQueryCount(), sprintf('Checks that query count is less than 30 (token %s)', $profile->getToken()) ); Le profiler conserve des donnes diffrentes suivant l'environnement utilis (particulirement si vous utilisez SQLite, ce qui est la configuration par dfaut). Les informations du profiler sont disponibles mme si vous isolez un client ou utilisez une couche HTTP particulire pour vos tests. L'API intgre vous permet de connatre les mthodes et les interfaces disponibles : data collectors. PDF brought to you by generated on June 26, 2014 Chapter 89: Comment utiliser le Profiler dans un test fonctionnel | 310 Listing 90-1 Chapter 90 Comment tester du code interagissant avec une base de donnes Si votre code interagit avec une base de donnes, si par exemple il lit ou enregistre des donnes, vous devez ajuster vos tests pour prendre en compte ceci. Il y a de nombreuses faons de grer ce cas. Dans un test unitaire, vous pouvez crer un bouchon (mock en anglais) pour un Repository et l'utiliser pour retourner les objets attendus. Dans un test fonctionnel, vous devriez avoir besoin de prparer une base de donnes de test avec des valeurs prdfinies pour assurer que votre test fonctionne toujours avec les mmes donnes. Si vous voulez tester directement vos requtes, consultez Comment tester les dpts Doctrine. Mocker le Repository dans un test unitaire Si vous souhaitez tester du code dpendant d'un repository Doctrine tout en l'isolant, vous aurez besoin de mocker ce Repository. Normalement, vous injectez l' EntityManager dans votre classe et l'utilisez pour rcuprer le repository. Cela rend les choses un peu plus difficile puisque vous avez besoin de mocker et l' EntityManager et votre classe de repository. Il est possible (et c'est une bonne ide) d'injecter votre repository directement en l'enregistrant comme un service factory. Cela demande un peu plus de travail pour le paramtrage, mais cela rend les tests plus faciles puisque vous n'aurez besoin de mocker que le repository. Supposons que la classe que vous voulez tester ressemble ceci: 1 2 3 namespace Acme\DemoBundle\Salary; use Doctrine\Common\Persistence\ObjectManager; PDF brought to you by generated on June 26, 2014 Chapter 90: Comment tester du code interagissant avec une base de donnes | 311 Listing 90-2 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class SalaryCalculator { private $entityManager; public function __construct(ObjectManager $entityManager) { $this->entityManager = $entityManager; } public function calculateTotalSalary($id) { $employeeRepository = $this->entityManager->getRepository('AcmeDemoBundle::Employee'); $employee = $employeeRepository->find($id); return $employee->getSalary() + $employee->getBonus(); } } Puisque l' ObjectManager est inject dans la classe travers le constructeur, il est facile de passer un objet mock dans un test 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 use Acme\DemoBundle\Salary\SalaryCalculator; class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase { public function testCalculateTotalSalary() { // Premirement, mockez l'objet qui va tre utilis dans le test $employee = $this->getMock('\Acme\DemoBundle\Entity\Employee'); $employee->expects($this->once()) ->method('getSalary') ->will($this->returnValue(1000)); $employee->expects($this->once()) ->method('getBonus') ->will($this->returnValue(1100)); // Maintenant, mocker le repository pour qu'il retourne un mock de l'objet emloyee $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository') ->disableOriginalConstructor() ->getMock(); $employeeRepository->expects($this->once()) ->method('find') ->will($this->returnValue($employee)); // Et enfin, mockez l'EntityManager pour qu'il retourne un mock du repository $entityManager = $this->getMockBuilder('\Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() ->getMock(); $entityManager->expects($this->once()) ->method('getRepository') ->will($this->returnValue($employeeRepository)); $salaryCalculator = new SalaryCalculator($entityManager); $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1)); PDF brought to you by generated on June 26, 2014 Chapter 90: Comment tester du code interagissant avec une base de donnes | 312 Listing 90-3 } } Dans cet exemple, vous construisez les mocks de l'intrieur vers l'extrieur, en crant premirement l'employee qui est retourn par le Repository, qui lui-mme est retourn par l' entityManager. De cette faon, aucune vraie classe n'est implique dans le test. Changer le paramtrage de la base de donnes pour les tests fonctionnels Si vous avez des tests fonctionnels, vous souhaitez qu'ils interagissent avec une vraie base de donnes. La plupart du temps, vous souhaitez une base de donnes ddie durant le dveloppement de l'application ainsi qu'tre capable de nettoyer la base de donnes avant chaque test. Pour faire cela, il vous est possible de spcifier la configuration de la base de donnes, qui remplacera la configuration par dfaut: 1 2 3 4 5 6 7 8 # app/config/config_test.yml doctrine: # ... dbal: host: localhost dbname: testdb user: testdb password: testdb Assurez vous que votre base de donnes tourne sur localhost, qu'elle a une base de donnes dfinie et que les droits utilisateurs sont configurs. PDF brought to you by generated on June 26, 2014 Chapter 90: Comment tester du code interagissant avec une base de donnes | 313 Listing 91-1 Chapter 91 Comment tester les dpts Doctrine Les tests unitaires des dpts Doctrines lintrieur d'un projet Symfony ne sont pas recommands. Lorsque vous travaillez avec un dpt, vous travaillez avec quelque chose qui est rellement sens tre test avec une vritable connexion une base de donnes. Heureusement, vous pouvez facilement tester vos requtes avec une vraie base de donnes, comme dcrit ci-dessous Tests fonctionnels Si vous avez besoin de tester lexcution d'une requte, vous devez dmarrer le kernel afin d'obtenir une connexion valide. Dans ce cas, votre classe doit hriter de WebTestCase, une classe qui simplifiera les processus de test: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // src/Acme/StoreBundle/Tests/Entity/ProductRepositoryFunctionalTest.php namespace Acme\StoreBundle\Tests\Entity; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class ProductRepositoryFunctionalTest extends WebTestCase { /** * @var \Doctrine\ORM\EntityManager */ private $em; public function setUp() { static::$kernel = static::createKernel(); static::$kernel->boot(); $this->em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager'); } public function testSearchByCategoryName() { PDF brought to you by generated on June 26, 2014 Chapter 91: Comment tester les dpts Doctrine | 314 22 23 24 25 26 27 28 29 $products = $this->em ->getRepository('AcmeStoreBundle:Product') ->searchByCategoryName('foo') ; $this->assertCount(1, $products); } } PDF brought to you by generated on June 26, 2014 Chapter 91: Comment tester les dpts Doctrine | 315 Listing 92-1 Listing 92-2 Listing 92-3 Chapter 92 Comment personnaliser le processus de bootstrap avant les tests Parfois, lorsque vous lancez des tests, vous avez besoin d'ajouter des choses au processus d'initialisation (bootstrap) juste avant de les lancer. Par exemple, si vous lancez un test fonctionnel et que vous avez introduit une nouvelle ressource de traduction, alors vous devrez vider votre cache avant de lancer ces tests. Cet article vous explique comment faire D'abord, ajouter le fichier suivant: 1 2 3 4 5 6 7 8 9 10 // app/tests.bootstrap.php if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) { passthru(sprintf( 'php "%s/console" cache:clear --env=%s --no-warmup', __DIR__, $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'] )); } require __DIR__.'/bootstrap.php.cache'; Remplacer le fichier de test bootstrap bootstrap.php.cache dans app/phpunit.xml.dist par tests.bootstrap.php : 1 2 <!-- app/phpunit.xml.dist --> bootstrap = "tests.bootstrap.php" Maintenant, vous pouvez dfinir pour quel environnement vous voulez vider le cache dans votre fichier phpunit.xml.dist : 1 2 3 4 <!-- app/phpunit.xml.dist --> <php> <env name="BOOTSTRAP_CLEAR_CACHE_ENV" value="test"/> </php> PDF brought to you by generated on June 26, 2014 Chapter 92: Comment personnaliser le processus de bootstrap avant les tests | 316 Cela devient maintenant une variable d'environnement (c-a-d $_ENV) qui est disponible dans votre fichier bootstrap personnalis (tests.bootstrap.php). PDF brought to you by generated on June 26, 2014 Chapter 92: Comment personnaliser le processus de bootstrap avant les tests | 317 Listing 93-1 Chapter 93 Comment crer une Contrainte de Validation Personnalise Vous pouvez crer une contrainte personnalise en tendant la classe Constraint 1 . A titre d'exemple, nous allons crer un simple validateur qui vrifie qu'une chane de caractres ne contient que des caractres alphanumriques. Crer une classe de contrainte Tout d'abord, vous devez crer une classe de contrainte qui tend la classe Constraint 2 : 1 2 3 4 5 6 7 8 9 10 11 12 // src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumeric.php namespace Acme\DemoBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation */ class ContainsAlphanumeric extends Constraint { public $message = 'La chane "%string%" contient un caractre non autoris : elle ne peut contenir que des lettres et des chiffres.'; } L'annotation @Annotation est ncessaire pour cette nouvelle contrainte afin de la rendre disponible via les annotations dans les autres classes. Les options de votre contrainte sont reprsentes par des proprits publiques dans la classe de contrainte. 1. http://api.symfony.com/2.3/Symfony/Component/Validator/Constraint.html 2. http://api.symfony.com/2.3/Symfony/Component/Validator/Constraint.html PDF brought to you by generated on June 26, 2014 Chapter 93: Comment crer une Contrainte de Validation Personnalise | 318 Listing 93-2 Listing 93-3 Listing 93-4 Crer le validateur en lui-mme Comme vous pouvez le voir, une classe contrainte est minimale. La validation est ralise par un autre validateur de contrainte. La classe de validation est dfinie par l'implmentation de la mthode validatedBy(), incluant une logique prdfinie: 1 2 3 4 5 // dans la classe de base Symfony\Component\Validator\Constraint public function validatedBy() { return get_class($this).'Validator'; } Si vous crez une contrainte personnalise (ex: MyConstraint), Symfony2 recherchera automatiquement une autre classe, MyConstraintValidator lorsqu'il effectue la validation. La classe validatrice ne requiert qu'une mthode : validate: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumericValidator.php namespace Acme\DemoBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class ContainsAlphanumericValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { if (!preg_match('/^[a-zA-Za0-9]+$/', $value, $matches)) { $this->context->addViolation($constraint->message, array('%string%' => $value)); } } } La mthode validate ne retourne pas de valeur; la place, elle ajoute des violations de contraintes la proprit context du validateur grce l'appel la mthode addViolation dans le cas d'erreurs de validation. Par consquent, une valeur peut tre considre comme valide si aucune violation de contrainte n'est ajoute au contexte. Le premier paramtre de l'appel addViolation est le message d'erreur utilis pour la violation. New in version 2.1: La mthode isValid est dprcie au profit de validate dans Symfony 2.1. La mthode setMessage est galement dprcie, en faveur de l'appel la mthode addViolation du contexte. Utiliser le nouveau validateur Utiliser un validateur personnalis est trs facile, tout comme ceux fournis par Symfony2 lui-mme : 1 2 3 # src/Acme/BlogBundle/Resources/config/validation.yml Acme\DemoBundle\Entity\AcmeEntity: properties: PDF brought to you by generated on June 26, 2014 Chapter 93: Comment crer une Contrainte de Validation Personnalise | 319 Listing 93-5 Listing 93-6 Listing 93-7 Listing 93-8 4 5 6 name: - NotBlank: ~ - Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~ Si votre contrainte contient des options, alors elles devraient tre des proprits publiques de la classe de contrainte personnalise que vous avez cre plus tt. Ces options peuvent tre configures comme toutes les options des contraintes de Symfony. Contraintes de validation avec dpendances Si votre validateur possde des dpendances, comme une connexion une base de donnes, il faudra le configurer comme un service dans le conteneur d'injection de dpendances. Ce service doit inclure le tag validator.constraint_validator et un attribut alias : 1 2 3 4 5 services: validator.unique.your_validator_name: class: Fully\Qualified\Validator\Class\Name tags: - { name: validator.constraint_validator, alias: alias_name } Votre classe contrainte devrait maintenant utiliser cet alias afin de rfrencer le validateur appropri: 1 2 3 4 public function validatedBy() { return 'alias_name'; } Comme mentionn prcdemment, Symfony2 recherchera automatiquement une classe nomme d'aprs le nom de la contrainte et suffixe par Validator. Si votre validateur de contrainte est dfini comme un service, il est important de surcharger la mthode validatedBy() afin qu'elle renvoie l'alias utilis pour dfinir le service ; autrement, Symfony2 n'utilisera pas le service de validation, et instanciera la classe, sans injecter les dpendances requises. Contrainte de validation de classe Outre la validation d'une proprit de classe, une contrainte peut avoir une porte de classe en renseignant une cible: 1 2 3 4 public function getTargets() { return self::CLASS_CONSTRAINT; } Avec ceci, la mthode validate() du validateur prend un objet comme premier argument: 1 2 3 4 5 6 7 8 class ProtocolClassValidator extends ConstraintValidator { public function validate($protocol, Constraint $constraint) { if ($protocol->getFoo() != $protocol->getBar()) { $this->context->addViolationAt('foo', $constraint->message, array(), null); } PDF brought to you by generated on June 26, 2014 Chapter 93: Comment crer une Contrainte de Validation Personnalise | 320 Listing 93-9 9 10 } } Notez bien qu'une contrainte de validation de classe est applique la classe elle-mme, et pas la proprit : 1 2 3 4 # src/Acme/BlogBundle/Resources/config/validation.yml Acme\DemoBundle\Entity\AcmeEntity: constraints: - ContainsAlphanumeric PDF brought to you by generated on June 26, 2014 Chapter 93: Comment crer une Contrainte de Validation Personnalise | 321 Listing 94-1 Chapter 94 Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 Configurer un contrleur afin qu'il agisse comme un serveur est ralis simplement avec quelques outils. Vous devez, bien sr, avoir install l'extension PHP SOAP 1 . Comme l'extension PHP SOAP ne peut actuellement pas gnrer un WSDL, vous devez soit en crer un, soit utiliser un gnrateur provenant d'une bibliothque tierce. Il existe de nombreuses implmentations de serveur SOAP disponibles compatibles avec PHP. Zend SOAP 2 et NuSOAP 3 en sont deux exemples. Bien que nous utilisions l'extension PHP SOAP dans nos exemples, l'ide gnrale devrait tre applicable aux autres implementations. SOAP fonctionne en exposant les mthodes d'un objet PHP une entit externe (c'est--dire le programme utilisant le service SOAP). Pour commencer, crez une classe - HelloService - qui reprsente les fonctionnalits que vous mettrez disposition dans votre service SOAP. Dans ce cas, le service SOAP permettra un client d'appeler la mthode nomme hello, qui engendrera l'envoi d'un courriel: 1 2 3 4 5 6 7 8 9 10 11 12 13 // src/Acme/SoapBundle/Services/HelloService.php namespace Acme\SoapBundle\Services; class HelloService { private $mailer; public function __construct(\Swift_Mailer $mailer) { $this->mailer = $mailer; } public function hello($name) 1. http://php.net/manual/fr/book.soap.php 2. http://framework.zend.com/manual/fr/zend.soap.server.html 3. http://sourceforge.net/projects/nusoap PDF brought to you by generated on June 26, 2014 Chapter 94: Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 | 322 Listing 94-2 Listing 94-3 14 15 16 17 18 19 20 21 22 23 24 25 26 { $message = \Swift_Message::newInstance() ->setTo('[email protected]') ->setSubject('Hello Service') ->setBody($name . ' dit bonjour !'); $this->mailer->send($message); return 'Bonjour, '.$name; } } Ensuite, vous devrez permettre Symfony de crer une instance de cette classe. Comme la classe envoie un courriel, le service prendra comme argument une instance Swift_Mailer. En utilisant le conteneur de service, nous pouvons configurer Symfony et lui permettre de construire l'objet HelloService adquat : # app/config/config.yml services: hello_service: class: Acme\SoapBundle\Services\HelloService arguments: [@mailer] Voici un exemple de contrleur capable de grer une requte SOAP. Si indexAction() est accessible via la route /soap, alors le document WSDL peut tre atteint via /soap?wsdl. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace Acme\SoapBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class HelloServiceController extends Controller { public function indexAction() { $server = new \SoapServer('/path/to/hello.wsdl'); $server->setObject($this->get('hello_service')); $response = new Response(); $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1'); ob_start(); $server->handle(); $response->setContent(ob_get_clean()); return $response; } } Notez les appels ob_start() et ob_get_clean(). Ces mthodes contrlent le tampon de sortie 4 qui vous permettent d'intercepter les flux de sortie de la mthode $server->handle(). Cela est ncessaire car Symfony attend de votre contrleur un objet Response contenant ce flux. Rappelez vous aussi de dfinir l'entte HTTP Content-Type comme text/xml puisque c'est ce quoi le client s'attendra. Vous utilisez donc ob_start() pour commencer la mise en tampon de STDOUT et utilisez 4. http://php.net/manual/fr/book.outcontrol.php PDF brought to you by generated on June 26, 2014 Chapter 94: Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 | 323 Listing 94-4 Listing 94-5 ob_get_clean() pour mettre la sortie dans le contenu de la Rponse et vider le tampon de sortie. Finalement, vous tes prt retourner l'objet Response. Voici un exemple qui appelle un service en utilisant le client NuSOAP 5 . Cet exemple suppose que le indexAction prsent dans le contrleur ci-dessus est accessible via la route /soap: 1 2 3 $client = new \Soapclient('http://example.com/app.php/soap?wsdl', true); $result = $client->call('hello', array('name' => 'Scott')); Un exemple d'un flux WSDL : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?xml version="1.0" encoding="ISO-8859-1"?> <definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="urn:arnleadservicewsdl" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="urn:helloservicewsdl"> <types> <xsd:schema targetNamespace="urn:hellowsdl"> <xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" /> <xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" /> </xsd:schema> </types> <message name="helloRequest"> <part name="name" type="xsd:string" /> </message> <message name="helloResponse"> <part name="return" type="xsd:string" /> </message> <portType name="hellowsdlPortType"> <operation name="hello"> <documentation>Hello World</documentation> <input message="tns:helloRequest"/> <output message="tns:helloResponse"/> </operation> </portType> <binding name="hellowsdlBinding" type="tns:hellowsdlPortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="hello"> <soap:operation soapAction="urn:arnleadservicewsdl#hello" style="rpc"/> <input> <soap:body use="encoded" namespace="urn:hellowsdl" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> </input> <output> <soap:body use="encoded" namespace="urn:hellowsdl" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> </output> </operation> </binding> <service name="hellowsdl"> <port name="hellowsdlPort" binding="tns:hellowsdlBinding"> 5. http://sourceforge.net/projects/nusoap PDF brought to you by generated on June 26, 2014 Chapter 94: Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 | 324 46 47 48 49 <soap:address location="http://example.com/app.php/soap" /> </port> </service> </definitions> PDF brought to you by generated on June 26, 2014 Chapter 94: Comment crer des web services SOAP l'intrieur d'un contrleur Symfony2 | 325 Listing 95-1 Chapter 95 Comment crer et stocker un projet Symfony2 dans git Bien que cet article soit spcifique git, les mmes principes gnriques s'appliqueront si vous stockez votre projet dans Subversion. Une fois que vous aurez lu La cration de pages avec Symfony2 et que vous deviendrez familier avec l'usage de Symfony, vous serez sans aucun doute prt dmarrer votre propre projet. Dans cet article du Cookbook, vous allez apprendre la meilleure faon qui soit de dmarrer un nouveau projet Symfony2 stock dans le systme de gestion de contrle de source git 1 . Configuration Initiale du Projet Pour dmarrer, vous aurez besoin de tlcharger Symfony et d'initialiser votre dpt local git : 1. Tlcharger Symfony2 Standard Edition 2 sans les vendors . 2. Dzippez/dtarez la distribution. Cela va crer un dossier nomm Symfony avec votre nouvelle structure de projet, les fichiers de configuration, etc. Renommez-le en ce que vous voulez. 3. Crez un nouveau fichier nomm .gitignore la racine de votre nouveau projet (par exemple : ct du fichier deps) et coller ce qui suit dedans. Les fichiers correspondants ces patterns seront ignors par git : 1 2 3 4 /web/bundles/ /app/bootstrap* /app/cache/* /app/logs/* 1. http://git-scm.com/ 2. http://symfony.com/download PDF brought to you by generated on June 26, 2014 Chapter 95: Comment crer et stocker un projet Symfony2 dans git | 326 Listing 95-2 Listing 95-3 Listing 95-4 5 6 /vendor/ /app/config/parameters.yml Vous pouvez aussi avoir un fichier .gitignore qui peut tre utilis sur tout votre systme, dans ce cas, vous pourrez trouver plus d'informations ici : Github .gitignore 3 . De cette manire, vous pouvez exclure les fichiers/dossiers souvent utiliss par votre IDE pour l'ensemble de vos projets. 4. Copiez app/config/parameters.yml vers app/config/parameters.yml.dist. Le fichier parameters.yml est ignor par git (voir ci-dessus) afin que les paramtres spcifiques la machine comme les mots de passe de base de donnes ne soient pas committs. En crant le fichier parameters.yml.dist, les nouveaux dveloppeurs peuvent rapidement cloner le projet, copier ce fichier vers parameters.yml, l'adapter, et commencer dvelopper. 5. Initialisez votre dpt git : 1 $ git init 6. Ajoutez tous les fichiers initiaux dans git : 1 $ git add . 7. Crez un commit initial avec votre nouveau projet : 1 $ git commit -m "Initial commit" 8. Finalement, tlchargez toutes les bibliothques tierces en utilisant composer. vous pourrez trouver plus d'informations sur Mettre jour les Vendors. A ce point, vous disposez d'un projet Symfony2 totalement fonctionnel qui est correctement committ sous git. Vous pouvez immdiatement commencer dvelopper, en committant les nouveaux changements dans votre dpt git. Vous pouvez continuer en lisant le chapitre La cration de pages avec Symfony2 pour en apprendre davantage sur comment configurer et dvelopper votre application en interne. L'Edition Standard Symfony2 est fournie avec des exemples d'utilisation. Pour supprimer le code de dmonstration, suivez les instructions contenues dans le fichier Readme de la Standard Edition 4 . Grer les bibliothques vendors avec bin/vendors et deps Comment a marche ? Chaque projet Symfony utilise des bibliothques tierces regroupes sous l'appellation vendors . D'une faon ou d'une autre, le but est de vous permettre de tlcharger ces bibliothques dans le rpertoire vendor/ et de pouvoir obtenir pour chacune la version dsire. Par dfaut, ces bibliothques sont tlcharges en excutant le script de tlchargement php bin/ vendors install. Ce script utilise le fichier deps` situ la racine de votre projet. Ce fichier au format 3. https://help.github.com/articles/ignoring-files 4. https://github.com/symfony/symfony-standard/blob/master/README.md PDF brought to you by generated on June 26, 2014 Chapter 95: Comment crer et stocker un projet Symfony2 dans git | 327 Listing 95-5 INI contient la liste des bibliothques ncessaires votre projet, ainsi que le rpertoire cible o chacune doit tre tlcharge, et (de manire optionnelle) la version tlcharger. Le script bin/vendors` utilise git pour ce tlchargement, car la plupart des bibliothques sont gres et disponibles via git. Le script bin/ vendors se base galement sur le fichier deps.lock, lequel vous permet d'arrter la version une rvision prcise. Nota bene : ces bibliothques vendor ne sont pas gres dans votre dpt; ce sont de simples fichiers installs (et ignors du dpt git) dans le rpertoire vendor/ par le script bin/vendors. Or, puisque toute l'information ncessaire pour tlcharger ces fichiers est disponible dans deps et deps.lock (qui, eux, sont grs par votre dpt), tout autre dveloppeur peut utiliser votre projet, lancer php bin/vendors install, et obtenir les mmes bibliothques (et version). Vous contrlez donc exactement le contenu de chaque bibliothque, sans avoir besoin de le versionner dans votre dpt. Ainsi, lorsqu'un dveloppeur veut travailler sur votre projet, il lui suffit de lancer le script php bin/ vendors install pour s'assurer que toutes les bibliothques ncessaires soient tlcharges. Mettre jour Symfony Puisque Symfony est une bibliothque tierce de votre projet et qu'elle est donc gre entirement via deps et``deps.lock``, mettre jour Symfony signifie simplement mettre jour ces deux fichiers afin qu'ils correspondent la dernire version de l'dition standard de Symfony. Bien sr, si vous avez ajout des entres aux fichiers deps ou deps.lock, veillez ne remplacer que les parties originales afin de ne pas supprimer les entres supplmentaires. Hacker les bibliothques vendor Parfois, il est ncessaire de tlcharger une version prcise (branche, tag) d'une bibliothque. Vous pouvez le faire directement dans le fichier deps : 1 2 3 4 [AcmeAwesomeBundle] git=http://github.com/johndoe/Acme/AwesomeBundle.git target=/bundles/Acme/AwesomeBundle version=ma-version-trop-bien L'option git dfinit l'URL de la bibliothque. Elle peut utiliser divers protocoles, comme http:// ou git://. L'option target dfinit le rpertoire cible, le rpertoire dans lequel la bibliothque va tre tlcharge. Par convention, les bundles devraient aller dans le rpertoire vendor/bundles/ Acme, les autres bibliothques iront dans vendor/ma-biblio-trop-bien, le rpertoire vendor est galement le rpertoire cible par dfaut. L'option version vous permet de dfinir une rvision spcifique. Vous pouvez lui attribuer un nom de tag (version=origin/0.42) ou de branche (refs/remotes/origin/ma-branche- trop-bien). Version par dfaut: origin/HEAD. Flux de mise jour lors de l'xcution de php bin/vendors install, le script vrifie d'abord si le rpertoire cible existe pour chaque bibliothque. Si ce n'est pas le cas, il effectue un git clone. Puis, il effectue un git fetch origin, puis git reset --hard ma-version-trop-bien. Le rpertoire sera donc clon une seule et unique fois. Pour faire des changements lis au dpt distant il est ncessaire d'effacer le rpertoire cible lui-mme, et non seulement son contenu. PDF brought to you by generated on June 26, 2014 Chapter 95: Comment crer et stocker un projet Symfony2 dans git | 328 Vendors et Submodules Au lieu d'utiliser le systme composer.json pour grer les bibliothques vendor, vous pourriez choisir la place le systme natif git submodules 5 . Il n'y a rien d'incorrect dans cette approche, bien que le systme composer.json soit la manire officielle de rsoudre ce problme et qu'il peut tre parfois difficile de travailler avec les git submodules. Stocker votre projet sur un serveur distant Vous disposez maintenant d'un projet totalement fonctionnel stock dans git. Cependant, dans la plupart des cas, vous voudrez aussi stocker votre projet sur un serveur distant que ce soit pour des raisons de sauvegardes mais aussi afin que d'autres dveloppeurs puissent collaborer au projet. La faon la plus facile de stocker votre projet sur un serveur distant est via GitHub 6 . Les dpts publics sont gratuits, cependant vous devrez payer un abonnement mensuel pour hberger des dpts privs. Une autre alternative est de stocker votre dpt git sur n'importe quel serveur en crant un dpt barebones 7 et en pushant ce dernier. Une bibliothque qui aide grer ceci est Gitolite 8 . 5. http://git-scm.com/book/en/Git-Tools-Submodules 6. https://github.com/ 7. http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository 8. https://github.com/sitaramc/gitolite PDF brought to you by generated on June 26, 2014 Chapter 95: Comment crer et stocker un projet Symfony2 dans git | 329 Listing 96-1 Chapter 96 Comment crer et stocker un projet Symfony2 dans Subversion Cette article est spcifique Subversion et bas sur des principes que vous trouverez dans l'article Comment crer et stocker un projet Symfony2 dans git. Une fois que vous avez lu La cration de pages avec Symfony2 et que vous tes devenu familier avec l'usage de Symfony, vous serez sans aucun doute prt dmarrer votre propre projet. La mthode prfre pour grer des projets Symfony2 est d'utiliser git 1 mais certains prfrent utiliser Subversion 2 , ce qui ne pose aucun problme! Dans cet article du Cookbook, vous allez apprendre comment grer votre projet en utilisant svn 3 de la mme manire que si vous l'aviez fait avec git 4 . Ceci est une mthode parmi tant d'autres de stocker votre projet Symfony2 dans un dpt Subversion. Il y a plusieurs manires de faire cela et celle-ci en est simplement une qui fonctionne. Le Dpt Subversion Pour cet article, nous supposerons que votre schma de dpt suit la structure standard et rpandue : 1 2 3 4 myproject/ branches/ tags/ trunk/ 1. http://git-scm.com/ 2. http://subversion.apache.org/ 3. http://subversion.apache.org/ 4. http://git-scm.com/ PDF brought to you by generated on June 26, 2014 Chapter 96: Comment crer et stocker un projet Symfony2 dans Subversion | 330 Listing 96-2 Listing 96-3 Listing 96-4 Listing 96-5 La plupart des hbergements subversion devraient suivre cette pratique standard. C'est le schma recommand par Contrle de version avec Subversion 5 et utilis par la plupart des hbergements gratuits (voir Solutions d'hbergement Subversion). Configuration Initiale du Projet Pour dmarrer, vous aurez besoin de tlcharger Symfony2 et d'effectuer la configuration basique de Subversion : 1. Tlchargez Symfony2 Standard Edition 6 avec ou sans les vendors . 2. Dzippez/dtarez la distribution. Cela va crer un dossier nomm Symfony avec votre nouvelle structure de projet, les fichiers de configuration, etc. Renommez-le en ce que vous voulez. 3. Effectuez un checkout du dpt Subversion qui va hberger ce projet. Disons qu'il est hberg sur Google code 7 et nomm myproject : 1 $ svn checkout http://myproject.googlecode.com/svn/trunk myproject 4. Copiez les fichiers du projet Symfony2 dans le dossier subversion : 1 $ mv Symfony/* myproject/ 5. Ecrivons maintenant les patterns pour les fichiers ignorer. Tout ne doit pas tre stock dans votre dpt subversion. Quelques fichiers (comme le cache) sont gnrs et d'autres (comme la configuration de la base de donnes) sont destins tre adapts sur chaque machine. Ainsi, nous utilisons la proprit svn:ignore afin de pouvoir ignorer ces fichiers spcifiques. 1 2 3 4 5 6 7 8 9 10 11 12 $ cd myproject/ $ svn add --depth=empty app app/cache app/logs app/config web $ svn propset svn:ignore "vendor" . $ svn propset svn:ignore "bootstrap*" app/ $ svn propset svn:ignore "parameters.yml" app/config/ $ svn propset svn:ignore "*" app/cache/ $ svn propset svn:ignore "*" app/logs/ $ svn propset svn:ignore "bundles" web $ svn ci -m "commit basic symfony ignore list (vendor, app/bootstrap*, app/ config/parameters.yml, app/cache/*, app/logs/*, web/bundles)" 6. Le reste des fichiers peut maintenant tre ajout et committ dans le projet : 1 2 $ svn add --force . $ svn ci -m "add basic Symfony Standard 2.X.Y" 5. http://svnbook.red-bean.com/ 6. http://symfony.com/download 7. http://code.google.com/hosting/ PDF brought to you by generated on June 26, 2014 Chapter 96: Comment crer et stocker un projet Symfony2 dans Subversion | 331 Listing 96-6 7. Copiez app/config/parameters.yml vers app/config/parameters.yml.dist. Le fichier parameters.yml est ignor par svn (voir ci-dessus) afin que les paramtres spcifiques la machine comme les mots de passe de base de donnes ne soient pas committs. En crant le fichier parameters.yml.dist, les nouveaux dveloppeurs peuvent rapidement cloner le projet, copier ce fichier vers parameters.yml, l'adapter, et commencer dvelopper. 8. Finalement, tlchargez toutes les bibliothques tierces : 1 $ php bin/vendors install git 8 doit tre install pour pouvoir excuter la commande bin/vendors ; c'est le protocole utilis pour aller rcuprer les bibliothques vendor. Cela signifie seulement que git est utilis comme outil pour aider au tlchargement des bibliothques dans le rpertoire vendor/. A ce point, vous avez un projet Symfony2 entirement fonctionnel stock dans votre dpt Subversion. Le dveloppement peut dmarrer avec des commits dans ce dernier. Vous pouvez continuer en lisant le chapitre La cration de pages avec Symfony2 pour en apprendre plus sur comment configurer et dvelopper votre application en interne. L'Edition Standard Symfony2 vient avec des exemples de fonctionnalits. Pour supprimer le code de dmonstration, suivez les instructions du fichier Readme de la Standard Edition 9 . Grer les bibliothques vendors avec bin/vendors et deps Comment a marche ? Chaque projet Symfony utilise des bibliothques tierces regroupes sous l'appellation vendors . D'une faon ou d'une autre, le but est de vous permettre de tlcharger ces bibliothques dans le rpertoire vendor/ et de pouvoir obtenir pour chacune la version dsire. Par dfaut, ces bibliothques sont tlcharges en excutant le script de tlchargement php bin/ vendors install. Ce script utilise le fichier deps` situ la racine de votre projet. Ce fichier au format INI contient la liste des bibliothques ncessaires votre projet, ainsi que le rpertoire cible o chacune doit tre tlcharge, et (de manire optionnelle) la version tlcharger. Le script bin/vendors` utilise git pour ce tlchargement, car la plupart des bibliothques sont gres et disponibles via git. Le script bin/ vendors se base galement sur le fichier deps.lock, lequel vous permet d'arrter la version une rvision prcise. Nota bene : ces bibliothques vendor ne sont pas gres dans votre dpt; ce sont de simples fichiers installs (et ignors du dpt git) dans le rpertoire vendor/ par le script bin/vendors. Or, puisque toute l'information ncessaire pour tlcharger ces fichiers est disponible dans deps et deps.lock (qui, eux, sont grs par votre dpt), tout autre dveloppeur peut utiliser votre projet, lancer php bin/vendors install, et obtenir les mmes bibliothques (et version). Vous contrlez donc exactement le contenu de chaque bibliothque, sans avoir besoin de le versionner dans votre dpt. Ainsi, lorsqu'un dveloppeur veut travailler sur votre projet, il lui suffit de lancer le script php bin/ vendors install pour s'assurer que toutes les bibliothques ncessaires soient tlcharges. 8. http://git-scm.com/ 9. https://github.com/symfony/symfony-standard/blob/master/README.md PDF brought to you by generated on June 26, 2014 Chapter 96: Comment crer et stocker un projet Symfony2 dans Subversion | 332 Listing 96-7 Mettre jour Symfony Puisque Symfony est une bibliothque tierce de votre projet et qu'elle est donc gre entirement via deps et``deps.lock``, mettre jour Symfony signifie simplement mettre jour ces deux fichiers afin qu'ils correspondent la dernire version de l'dition standard de Symfony. Bien sr, si vous avez ajout des entres aux fichiers deps ou deps.lock, veillez ne remplacer que les parties originales afin de ne pas supprimer les entres supplmentaires. Hacker les bibliothques vendor Parfois, il est ncessaire de tlcharger une version prcise (branche, tag) d'une bibliothque. Vous pouvez le faire directement dans le fichier deps : 1 2 3 4 [AcmeAwesomeBundle] git=http://github.com/johndoe/Acme/AwesomeBundle.git target=/bundles/Acme/AwesomeBundle version=ma-version-trop-bien L'option git dfinit l'URL de la bibliothque. Elle peut utiliser divers protocoles, comme http:// ou git://. L'option target dfinit le rpertoire cible, le rpertoire dans lequel la bibliothque va tre tlcharge. Par convention, les bundles devraient aller dans le rpertoire vendor/bundles/ Acme, les autres bibliothques iront dans vendor/ma-biblio-trop-bien, le rpertoire vendor est galement le rpertoire cible par dfaut. L'option version vous permet de dfinir une rvision spcifique. Vous pouvez lui attribuer un nom de tag (version=origin/0.42) ou de branche (refs/remotes/origin/ma-branche- trop-bien). Version par dfaut: origin/HEAD. Flux de mise jour lors de l'xcution de php bin/vendors install, le script vrifie d'abord si le rpertoire cible existe pour chaque bibliothque. Si ce n'est pas le cas, il effectue un git clone. Puis, il effectue un git fetch origin, puis git reset --hard ma-version-trop-bien. Le rpertoire sera donc clon une seule et unique fois. Pour faire des changements lis au dpt distant il est ncessaire d'effacer le rpertoire cible lui-mme, et non seulement son contenu. Solutions d'hbergement Subversion La plus grosse diffrence entre git 10 et svn 11 est que Subversion a besoin d'un dpt central pour fonctionner. Vous avez donc plusieurs solutions : Hbergement par vos soins : crez votre propre dpt et accdez-y soit grce au systme de fichiers, soit via le rseau. Pour vous aider dans cette tche, vous pouvez lire Contrle de version avec Subversion 12 . 10. http://git-scm.com/ 11. http://subversion.apache.org/ 12. http://svnbook.red-bean.com/ PDF brought to you by generated on June 26, 2014 Chapter 96: Comment crer et stocker un projet Symfony2 dans Subversion | 333 Hbergement via une entit tierce : il y a beaucoup de solutions d'hbergement gratuites et srieuses disponibles comme GitHub 13 , Google code 14 , SourceForge 15 ou Gna 16 . Certaines d'entre elles offrent aussi des possibilits d'hbergement git. 13. https://github.com/ 14. http://code.google.com/hosting/ 15. http://sourceforge.net/ 16. http://gna.org/ PDF brought to you by generated on June 26, 2014 Chapter 96: Comment crer et stocker un projet Symfony2 dans Subversion | 334