Les fondamentaux de Symfony 3
Ce Post et très ancien, j’ai commencé la prise de note en 2016. Je trouve étrange que je ne partage pas même si ce n’est pas bien organisé. Actuellement on parle de Symfony4 et Symfony5 mais il y a toujours des gens qui utilisent Symfony3 donc profitons ce cet article très riche au niveau d’informations
Architecture de Symfony 3
- Le répertoire /app : contient toute la configuration de l’application (sauf le code source)
- Le répertoire /bin : contient les fichiers exécutables dont nous allons nous servir durant le développement
- Le répertoire /src : contient le code source
- Le répertoire /tests : contient les test de l’application
- Le répertoire /var : contient tout ce que symfony va écrire durant son process, logs, cahces
- Le répertoire /vendor : contient les bibliothèques externes à notre application (Symfony, Doctrine, etc…)
- Le répertoire /web : contient les fichiers destinés aux visiteurs : images, js, css. C’est le seul dossier qui devrait être accessible aux visiteurs
Le contrôleur frontal
Il s’agit du point d’entrée de l’application. C’est le fichier par lequel passent toutes les pages. Il se limite donc à appeler le noyau (kernel )de Symfony en disant « on vient de recevoir une requête, transforme-la en réponse stp ».
Les bundles de la communauté : http://knpbundles.com/
- La classe app/AppKernel permet de définir quels bundles charger pour l’application
- Le rôle du Routeur est de déterminer quel contrôleur exécuter en fonction de l’URL appelée.
- Le rôle d’une route est d’associer une URL à une action du contrôleur
- Le rôle du contrôleur est de retourner au noyau un objet Response qui contient la réponse HTTP à envoyer à l’internaute
Schéma de développement sous Symfony :
Le cache est constitué de fichiers PHP prêts à être exécutés, contenant tout le nécessaire pour faire tourner Symfony sous une forme plus rapide. Pensez par exemple à la configuration dans les fichiers YAML : quand Symfony génère une page, il va compiler cette configuration dans un array php.
Après certains modifications, le cache peut ne plus être à jour, ce qui entrainer des erreurs
Routing
- On appelle l’URL /platform/advert/5
- Le routeur essaie de faire correspondre cette URL avec le
path
de la première route. Ici,/platform/advert/5
ne correspond pas du tout à/platform
(lignepath
de la première route). -
Le routeur passe donc à la route suivante. Il essaie de faire correspondre
/platform/advert/5
avec/platform/advert/{id}
. Nous le verrons plus loin, mais{id}
est un paramètre, une sorte de joker « je prends tout ». Cette route correspond, car nous avons bien :-
/platform/advert
(URL) =/platform/advert
(route) ; -
5
(URL) ={id}
(route).
-
- Le routeur s’arrête donc, il a trouvé sa route.
- Il demande à la route : « Quels sont tes paramètres de sortie ? », la route répond : « Mes paramètres sont 1/ le contrôleur
OCPlatformBundle:Advert:view
, et 2/ la valeur$id = 5
. » - Le routeur renvoie donc ces informations au Kernel (le noyau de Symfony).
- Le noyau va exécuter le bon contrôleur avec les bons paramètres !
N.B : les paramètres sont par défaut obligatoires
Special Routing Parameters
Each routing parameter or default value is available as an argument in the controller method. Additionally, there are three parameters that are special
- _controller : it is used to determine which controller is executed when the route is matched
- _format : Used to set the request format (json/html/xml)
- _locale :Used to set the locale on the request
Pour générer une URL absolue, lorsqu’on l’envoie par e-mail par exemple, il faut définir le 3eme argument de la méthode generate
<?php
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$url = $this->get(‘router’)->generate(‘oc_platform_home’, array(), UrlGeneratorInterface::ABSOLUTE_URL);
Controlleur
Symfony s’est inspiré des concepts du protocole HTTP. Il existe dans Symfony les classes Request et Response
Les paramètres de la requête
Il existe deux types :
- Les paramètres contenus dans les routes
- Les paramètres hors routes
- /platform/advert/12?tag=lead : Il nous faut un moyen pour récupérer ce paramètre tag. C’est ici qu’intervient l’objet Request.
- Pour récupérer la requête depuis un contrôleur, il faut ajouter un argument à la méthode concernée avce le type Request
- L’objet Request ne se limite pas à la récupération de paramètres : http://api.symfony.com/3.0/Symfony/Component/HttpFoundation/Request.html
Manipuler l’objet Response
- Généralement on préfère que la réponse soit contenue dans une vue tel que le préconise l’architecture MVC. Le service templating dispose d’un raccourci : la méthode renderResponse(). Elle prend en paramètre le nom du template et ses variables puis s’occupe de tout :
- Créer la réponse
- Passer le contenu du template
- Retourner la réponse
- Le contrôleur lui même dispose d’un raccourci, il s’agit de la méthode render qui s’utilise exactement de la même façon
- <?phppublic function viewAction($id, Request $request)
{
// On récupère notre paramètre tag
$tag = $request->query->get(‘tag’);return $this->render(‘OCPlatformBundle:Advert:view.html.twig’, array(
‘id’ => $id,
‘tag’ => $tag,
));
} - Pour faire une réponse HTTP de type redirection, il existe l’objet RedirectResponse qui étend l’objet Response en lui ajoutant l’entête HTTP Location
- Des fois on souhaites savoir ce qu’il se passe sur la page avant la redirection, Symfony a la réponse : il faut mettre la valeur du paramètre intercept_redirects à true
- Des fois on veut changer le Content type de la réponse
Manipuler la session
Une des actions classiques d’un contrôleur, c’est de manipuler la session. Dans Symfony il existe un Objet Session qui permet de gérer la session, il se récupère depuis la requête.
L’objet session offre ce qu’on appelle les « messages flash ». C’est une astuce très utile pour les formulaires par exemple : La page qui traite le formulaire définit un message flash « annonce bien enregistrée » puis redirige vers la page de visualisation de l’annonce nouvellement créée. Sur cette page, le message flash s’affiche et est détruit de la session.
Composer
Composer est un outil pour gérer les dépendances en PHP. les dépendances, dans un projet, ce sont toutes les bibliothèques dont votre projet dépend pour fonctionner.
Il est évident que ce système de gestion ne peut fonctionner que si on peut centraliser les informations de chaque bibliothèque. C’est le rôle du site www.packagist.org
Mais comment Composer donne à Symfony les informations pour l’autoload ?
Composer s’occupe de tout et de déclarer les namespaces pour l’autoload :
- vendor/composer/autoload_namespaces.php : ce fichier contient tous les namespaces nécessaires pour notre projet. C’est lui que Symfony inclut déjà
Gérer l’autoload d’une bibliothèque manuellement
Il se peut que vous ayez besoin d’utiliser une bibliothèque qui n’est pas référencée sur Packagist. Composer ne peut pas gérer entièrement cette bibliothèque. Par contre si on souhaites la charger automatiquement grâce à l’autoload PHP. :
- Il faut ajouter les informations à la section « autoload » du fichier « composer.json«
Composer ne mettre pas son nez dans cette section pour tout ce qui est installation et mises à jour. Par contre il l’inclura dans son fichier d’autoload que Symfony2 charge
Les services
Les services sont utilisés partout dans Symfony.
Une application PHP utilise beaucoup d’objets PHP. Un objet remplit une fonction comme envoyer un e-mail, enregistrer des informations dans une BDD, etc. Une application est en réalité un moyen de faire travailler tous ces objets ensemble. Dans bien des cas, un objet a besoin d’un ou plusieurs autres objets pour réaliser sa fonction. Se pose alors la question de savoir comment organiser l’instanciation de tous ces objets. Si chaque objet a besoin d’autres objets, par lequel commencer ?
Qu’est ce qu’in service ?
Un service est simplement un objet PHP qui remplit une fonction, associé à une configuration.
Un service est un objet PHP qui a pour vocation d’être accessible depuis n’importe où dans le code
Quant à la configuration d’un service, c’est juste un moyen de l’enregistrer dans le conteneur de services. On lui donne un nom, on précise quelle est sa classe et ainsi le conteneur a la carte d’identité du service.
L’intérêt réel des services réside dans leur association avec le conteneur de services. Ce conteneur de services (services container) est une sorte de super-objet qui gère tous les services. Ainsi pour accéder à un service, il faut passer par le conteneur.
L’intérêt principal du conteneur est d’organiser et d’instancier nos services. L’objectif est de simplifier la récupération des services depuis le code source (contrôleur ou autre)
Voici un exemple
Le conteneur de service n’est pas figé, il dépend en réalité de notre configuration
Comment définir les dépendances entre services ?
Comment dire au conteneur que le service2 doit être instancié avant le service1 ?
- Cela se fait grâce à la configuration dans Symfony, l’idée est définir pour chaque service :
- Son nom, qui permettra de l’identifier au sein du conteneur ;
- Sa classe, qui permettra au conteneur d’instancier le service ;
- Les arguments dont il a besoin. Un argument peut être un autre service, mais aussi un paramètre défini dans le fichier parameters.yml
Pour voir la liste des services disponibles : php bin/console debug:container
La première étape du compilation du container est la lecture des bundles enregistrées, vous déclarez vos services dans des fichiers de configuration que vous référencez dans une extension du container. Ce dernier charge chaque bundle les uns après les autres dans l’ordre dans lequel vous les avez enregistré (AppKernel). Puis la phase de compilation, le container applique les passes de compilation. A cette étape, la totalité des services de votre application sont accessibles. Cette étape est classiquement utilisée pour lister les services taggés. Du coup, rien ne vous empêche dans ces passes de compilation de modifier les définitions des services à la volée et ainsi de pouvoir :
- changer la classe instancié
- ajouter, modifier ou retirer des arguments
- ajouter des calls à l’instanciation
Création d’un service
La seule convention à respecter, de façon générale dans Symfony, c’est de mettre notre classe dans un namespace correspondant au dossier où est le fichier. C’est la norme PSR-0 (zero) pour l’autoload. Par exemple la classe OC\PlatformBundle\Antispam\OCAntispam doit se trouver dans le répertoire src/OC/PlatformBundle/AntiSpam/OCAntispam.php
Création de la configuration du service
Une fois créé la classe, il faut la signaler au conteneur de services,un service est définit par sa classe ainsi que sa configuration, pour cela nous pouvons utiliser le fichier
- src/OC/PlatformBundle/Ressources/config/services.yml : ce fichier est chargé automatiquement par la méthode load qui se trouve dans le fichier DependencyInjection/OCPlatformExtension.php
- La méthode « load » est automatiquement exécutée par Symfony lorsque le bundle est chargé
- Le chargement du fichier de configuration services.yml permet d’enregistrer la définition des services qu’il contient dans le conteneur de services
- // Faire attention
Un tag est une information que l’on appose à un ou plusieurs services afin que le conteneur de services les identifie comme tels. Cela permet d’ajouter des fonctionnalités à un composant sans en modifier le code
- Exemple :
- Le moteur de templates Twig dispose nativement de plusieurs fonctions pratiques pour vos vues. Seulement, il serait intéressant de pouvoir ajouter nos propres fonctions qu’on pourra utiliser dans nos vues, et ce sans modifier le code même de Twig.
- L’idée est que Twig définit un tag, dans notre cas twig.extension, et une interface. Ensuite, Twig récupère tous les services qui ont ce tag
- Celui qui va récupérer les services d’un certain tag attend un certain comportement de la part des services qui ont ce tag. Il faut donc les faire implémenter une interface ou étendre une classe de base.
- Sachez que tous les tags ne nécessitent pas forcément que votre service implémente une certaine interface
Il existe pas mal de tags prédéfinis dans Symfony, qui permettent d’ajouter des fonctionnalités à droite et à gauche. https://symfony.com/fr/doc/current/reference/dic_tags.html
Les services peuvent être utilisés avec le gestionnaire d’événements, via plusieurs tags. Dans ce cas, les différents tags permettent d’exécuter un service à des moments précis dans l’exécution d’une page
Templating (Twig)
How does Twig work ?
- Load the template : If the template is already compiled, load it and go to the evaluation step, otherwise
- First, the Lexer tokenizes the template source code into small pieces (token stream) for easier processing
- each token is an instance of Twig_Token
- stream is an instance of Twig_TokenStream
- Then, the parser converts the token stream into a meaningful tree of nodes ( The Abstract Syntax Tree )
- AST is an instance of Twig_Node_Module
- Eventually, the compiler transforms the AST into PHP code
- First, the Lexer tokenizes the template source code into small pieces (token stream) for easier processing
- Evaluate the template : It basically means calling the display() method of the compiled template and passing ot the context
L’héritage de template nous permet de résoudre la problématique : »J’ai un seul design et n’ai pas l’envie de le répéter sur chacun de mes templates »
Le principe : nous avons un template père qui contient le design de notre site ainsi que quelques trous (blocs) et des templates fils qui vont remplir ces blocs. Les fils vont donc venir hériter du père en remplaçant certains éléments par leur propres contenu.
Le modèle « Triple héritage »
Pour bien organiser ses templates, une bonne pratique est là pour nous. Il s’agit de faire l’héritage de templates sur trois niveaux
- Layout général : c’est le design de notre site, indépendamment de bundles. Il contient la structure de notre site
- Layout du bundle : Il hérite de layout général et contient les parties communs à toutes les pages d’un même bundle, par exemple : un menu, des liens utiles du bundle
- Template de page : il hérite du layout du bundle et contient le contenu central de notre page
- l’inclusion de templates : {{ include(« OCPlatformBundle:Advert:form.html.twig ») }}
- l’inclusion de contrôleurs : {{ render(controller(« OCPlatformBundle:Advert:menu »)) }}
ENTITY
Une entity est un simple objet avec des commentaires (annotations).
Grâce à ces annotations, Doctrine2 (ORM) dispose de toutes les informations nécessaires pour utiliser notre objet,
- Créer la table, l’enregistrer
- Définir un identifiant
- Nommes les colonnes
Ces informations se nomment les metadata de notre entité. (mapper un objet : c’est à dire faire le lien entre notre objet et la représentation physique qu’utilise Doctrine2
l’annotation Entity : s’applique sur une classe, il faut la déplacer avant la définition de la classe, Elle définit un objet comme étant une entité, et donc persisté par Doctrine : @ORM\Entity
- Un repository sert à récupérer nos entités depuis la base de données
l’annotation Table: s’applique à une classe également, c’est une annotation facultative, Elle permet de personnaliser le nom de la table qui sera créée dans la base de données @ORM\Table
l’annotation Column: s’applique sur un attribut de classe. Cette annotation permet de définir les caractéristiques de la colonne concernée.@ORM\Column
- Les types de colonnes qu’on peut définir en annotation sont des types Doctrine. et uniquement Doctrine
Création des tables et la base de données
- php bin/console doctrine:database:create
- php bin/console doctrine:schema:update –dump-sql
- générer les tables à l’intérieur de la base de données (juste afficher les requetes SQL )
- php bin/console doctrine:schema:update –force
- exécuter les requêtes SQL
- php bin/console doctrine:generate:entities OCPlatformBundle:Advert
- re-générer l’entity avec la prise en compte de modification
Les services Doctrine2
- Le service Doctrine gère la persistance de nos objets, il est accessible depuis le contrôleur
- $doctrine = $this->get(‘doctrine’); : ce service s’occupe de deux choses :
- Les différentes connexions à des base de données. C’est la partie DBAL de Doctrine2
- Les différentes gestionnaires d’entités, ou EntityManager. C’est la partie ORM de Doctrine2 ; $doctrine->getManager($name) qui récupère un ORM à partir de son nom
- $doctrine->getManager() permet de récupérer l’EntityManager par défaut en omettant l’argument $name.
- $doctrine = $this->get(‘doctrine’); : ce service s’occupe de deux choses :
- Le service EntityManager : Il permet de dire à Doctrine « Persister cet objet », c’est lui qui va exécuter les requêtes SQL, la seule chose qu’il ne sait pas, c’est de récupérer les entités depuis la base de données. Pour cela on va utiliser des Repository
- $em = $this->getDoctrine()->getManager();
- $em = $this->get(‘doctrine.orm.entity_manager’);
Les repositories
Se sont des objets, qui utilisent un EntityManager, il existe un repository par entité
<?php
$em = $this->getDoctrine()->getManager();
$advertRepository = $em->getRepository(‘OCPlatformBundle:Advert’);
Les différentes types de relations
Il y a plusieurs façons de lier des entités entre elles :
- OneToOne
- OneToMany
- ManyToMany
- Notion de propriétaire et d’inverse : dans une relation entre deux entités, il y a toujours une entité dite propriétaire et une dite inverse
- L’entité propriétaire est celle qui contient la référence à l’autre entité ; exemple la table comment avec la table advert
- Notion d’unidirectionnalité et de bidirectionnalité : une relation peut être à sens unique ou à double sens
- sens unique : $entiteProprietaire->getEntiteInverse() ($comment->getAdvert(); )
- Doctrine ne récupère pas toutes les entités qui lui sont liées (les commentaires d’une annonce)
- Doctrine utilise ce qu’on appelle le Lazy Loading, c’est à dire qu’il ne charge les entités à l’autre bout de la relation que si vous voulez accéder à ces entités
Relation One-To-One
- @ORM\OneToOne(targetEntity= »name_space_entity », cascade={« persist »})
- OneToOne : définit une relation vers une autre entité
- cascade : on peut cascader des opérations de suppression, mais aussi de persistance. On a vu qu’il fallait persister une entité avant d’exécuter le flush(). afin de dire à Doctrine qu’il doit enregistrer l’entité en BDD. Cependant, dans le cas d’entités liées, si on fait un $em->persist($advert), qu’est ce que Doctrine doit faire pour l’entité Image contenue dans l’entité Advert ?
- Soit en faisant manuellement un persist() sur l’annonce et l’image
- Soit en définissant dans l’annotation de la relation qu’un persist() sur Advert doit se propager sur l’Image liée
Relation Many-To-One
La relation Many-To-One, ou n…1, est une relation qui permet à une entité A d’avoir une relation avec plusieurs entités B.
Une annonce peut contenir plusieurs candidatures, alors qu’une candidature n’appartient qu’à une seule annonce
- C’est le côté Many d’une relation Many-To-One qui est le propriétaire
- @ORM\ManyToOne(targetEntity= »OC\PlatformBundle\Entity\Advert »)
Relation Many-To-Many
La relation Many-To-Many ou n…n correspond à une relation qui permet à plein d’objets d’être en relation avec plein d’autres
- Doctrine va devoir créer une table intermédiaire avec ce genre de relation, en effet avec la méthode traditionnelle en base de données, nous devons créer une table intermédiaire qui fait la laison entre les deux tables
- Un ArrayCollection est un objet utilisé par Doctrine2 qui a toutes les propriétés d’un tableau. Il dispose juste de quelques méthodes supplémentaires
- Les fixtures Doctrine permettent de remplir la BDD avec un jeu de données
Les relations bidirectionnelles
L’inconvénient d’une relation unidirectionnelle est de ne pas pourvoir récupérer l’entité propriétaire depuis l’entité inverse « $entiteInverse->getEntiteProprietaire() »
Un rappel : le propriétaire d’une relation Many-To-One est toujours le côté Many,
- La paramètre mappedBy correspond à l’attribut de l’entité propriétaire qui pointe vers l’entité inverse, il faut le renseigner pour que l’entité inverse soit au courant des caractéristiques de la relation
- Il faut également adapter l’entité propriétaire, pour lui dire que maintenant la relation est de type bidirectionnelle et non plus unidirectionnelle, pour cela il faut rajouter le paramètre inversedBy dans l’annotation Many-To-One.
- Le seul truc
REPOSITORY
L’une des principales fonctions de la couche Modèle dans une application MVC, c’est la récupération des données.
Un repository centralise tout ce qui touche à la récupération de vos entités. i.e on ne peut pas faire la moindre requête SQL ailleurs que dans un repository. Rappelez-vous, il existe un repository par entité
Les reporitories héritent de la classe « EntityRepository » qui propose quelques méthodes très utiles pour récupérer des entités
- find($id)
- findAll()
- findBy() : pareil que findAll sauf qu’elle est capable d’effectuer un filtre pour ne retourner que es entités correspondant à un ou plusieurs critère(s)
- findOneBy() : pareil que findBy sauf qu’elle ne retourne qu’une seule entité
- grâce au méthode magique __call() on peut nous même créer nos propres méthodes
- findByX($valeur) : X : le nom d’une propriété de notre entité
- findOneByX($valeur) : Idem
- Toutes ces méthodes sont utiles, elles montrent rapidement leur limites lorsqu’on doit faire des jointures ou effectuer des conditions plus complexes
Deux façons pour construire les requêtes de récupération des entités
Depuis un repository, il existe deux façons de récupérer les entités : en utilisant du DQL et en utilisant le QueryBuider
- Le Doctrine Query Language (DQL) : Le DQL n’est rien d’autre que du SQL adapté à la vision par objets que Doctrine utilise : SELECT a FROM OCPlatformBundle:Advert a
- Le QueryBuilder : il sert à construire une requête par étape
Les méthodes de récupération personnelles (QueryBuilder)
Pour cela il faut distinguer trois types d’objets qui vont nous servir
- QueryBuilder
- Query
- Result
Pour l’objet Query :
- getResult() : exécute la requête et retourne un tableau d’objets
- getArrayResult() : exécute la requête et retourne un tableau des tableaux
- getScalarResult() : exécute la requête et retourne un tableau sous forme de valeurs
- getOneOrNullResult() : exécute la requête et retourne un seul résultat ou null si pas de résultat
- getSingleResult() : exécute la requête et retourne un seul résultat
- getSingleScalarResult() : exécute la requête et retourne une seule valeur
- execute() : Exécute la requête. Cette méthode est utilisée pour exécuter des requêtes qui ne retournent pas de résultats
Les méthodes de récupération personnelles (DQL)
LE DQL permet de faire des requêtes un peu à l’ancienne en écrivant une requête en chaîne de caractères (en opposition au QueryBuilder). On utilise seulement l’objet Query et la méthode pour récupérer les résultats sera la même. Il est possible avec Doctrine2 de tester les requêtes en ligne de commandes
- php bin/console doctrine:query:dql « SELECT a FROM OCPlatformBundle:Advert a »
Utiliser les jointures dans les requêtes
- Lorsqu’on fait la syntaxe $entiteA->getEntiteB(), Doctrine exécute une requête afin de charger les entités B qui sont liées à l’entité A.
- Il faut toujours éviter une requête par itération dans une boucle car on explose un nombre de requêtes sur une seule page. Pour éviter cela on fait les jointures
- On ne peut faire une jointure que si l’entité du From possède un attribut vers l’entité à joindre
Les événements Doctrine
Dans certains cas, on peut avoir besoin d’effectuer des actions juste avant ou juste après la création update ou la suppression d’une entité. C’est ici qu’interviennent les événements Doctrine. un Callback est une méthode de votre entité et on va dire à Doctrine de l’exécuter à certains moments
Liste des événements du cycle de vie
- PrePersist
- PostPersist
- PreUpdate
- PostUpdate
- PreRemove
- PostRemove
- PostLoad
Utiliser des services pour écouter les événements Doctrine
- Les callbacks définis directement dans les entités sont pratiques, Cependant, leurs limites sont vite atteintes car les callbacks n’ont accès à aucune information de l’extérieur.
- Heureusement, il est possible de dire à Doctrine d’exécuter des services Symfony pour chaque événement du cycle de vie des entités. l’idée est la même mais au lieu d’une méthode callback dans une entité, on va utiliser un service défini hors de l’entité
- Le seul point qui diffère des callbacks, c’est que nos services seront exécutés pour un événement (PreUpdate for example) pour toutes les entités et non attaché à une seule entité
Les extensions Doctrine
Dans la gestion des entités d’un projet, il y a des comportements assez communs que nous souhaitons implémenter. Par exemple générer des slugs pour nos annonces. Plutôt que réinventer tout le comportement nous-même, nous allons utiliser les extensions Doctrine. Doctrine est très flexible et la communauté a déjà créé une série d’extensions très pratiques afin de faciliter des tâches usuelles liées aux entités par exemple le bundle StofDoctrineExtensionBundle, ce bundle intègre la bibliothèque DoctrineExtensions qui est celle qui inclut réelement les extensions Doctrine
Pour cela il faut activer les extensions dans le fichier de configuration app.yml
# Stof\DoctrineExtensionsBundle configuration
stof_doctrine_extensions:
orm:
default:
sluggable: true
Concrètement, l’utilisation des extensions se fait grâce à de judicieuses annotations
Liste des extensions Doctrine
- Tree
- Translatable
- Sluggable
- Timestampable
- Blameable
- Loggable
- Sortable
- Softdeleteable
- Uploadable
- IpTraceable
FORM
Symfony est livré avec un par défaut (form_div_layout.html.twig) qui définit chaque fragment nécessaire pour rendre chaque partie d’un formulaire
En Twig : {{ form.age }}, Rendu :
<input type="number" id="form_age" name="form[age]" required="required" value="33" /> En interne, Symfony utilise le fragment integer_widget pour rendre le champ. C'est parce que le type de champ est un entier qu'il vous rendra son widget
Les formulaires imbriqués
C’est souvent le cas lorsque nous avons des relations entres nos objets : On souhaite ajouter un objet A mais en même temps un autre objet B qui est lié au premier. Le but est d’ajouter tout ça sur la même page
Commande pour générer un formulaire :
- php bin/console doctrine:generate:form OCPlatformBundle:Image
Il existe deux façons d’imbriquer ce formulaire :
- Avec une relation simple où l’imbrique une seule fois un sous-formulaire dans le formulaire principal (Advert avec une seule Image)
- Il faut que l’entité du formulaire principal ait une relation One-To-One ou Many-To-One avec l’entité dont on veut imbriquer
- Avec une relation multiple où l’imbrique plusieurs fois le sous-formulaire dans le formulaire pincipal (Client avec plusieurs Adresse)
- On imbrique un même formulaire plusieurs fois lorsque deux entités sont en relation Many-To-Many ou One-To-Many
L’héritage des formulaires : l’utilité de l’héritage dans le cadre des formulaires, c’est de pouvoir construire des formulaires différents, mais ayant la même base. On donne un exemple l’ajout et de modification d’une Advert. Imaginons que le formulaire d’ajout comprenne tous les champs, mais que pour l’édition il soit possible de modifier la date par exemple. Comme nous sommes en présence de deux formulaires distincts, on va faire deux XxxType distincts :
- AdvertType pour l’ajout
- AdvertEditType pour la modification
Seulement il est hors de question de répéter la définition de tous les champs dans le AdvertEditType, tout d’abord c’est long, mais aussi si un champ change, on devra modifier à la fois AdvertType et AdvertEditType. La solution est de faire en sorte que AdvertEditType hérite AdvertType. Le processus est le suivant :
- Copiez-collez le fichier AdvertType.php et renommez la copie en AdvertEditType.php
- Modifiez le nom de la classe en AdvertEditType
- Ajouter une méthode getParent qui retourne la classe du formulaire parent, AdvertType::class
- Remplacez la définition manuelle de tous les champs par une simple ligne pour supprimer le champ date par exemple $builder->remove(‘date’)
- Enfin, supprimez la méthode configureOptions qu’il ne sert à rien
La différence entre l’héritage natif PHP et ce qu’on appelle l’héritage de formulaires réside dans la méthode getParent() qui retourne le formulaire parent. Lors de la construction de ce formulaire le composant Form exécutera d’abord la méthode buildForm du formulaire parent, ici AdvertType, avant d’exécuter la courante.
A retenir
- D’une part, si vous avez besoin de plusieurs formulaires, faites plusieurs XxxType, ça évite de faire du code impropre derrière en mettant des conditions hasardeuses; Le raisonnement est simple : si le formulaire que vous voulez afficher à votre internaute est différent (champ en moins, champ en plus) alors côté Symfony c’est un tout autre formulaire, qui mérite son propre XxxType
- D’autre part, pensez à bien utiliser l’héritage de formulaires pour éviter de dupliquer du code. Centralisez donc la définition de vos champs dans un formulaires, et utilisez l’héritage pour le propager aux autres
Construire un formulaire différemment selon des paramètres
Un autre besoin qui se fait retenir lors de l’élaboration de formulaires, c’est la modulation d’un formulaire en fonction de certains paramètres
On prend un exemple : On pourrait empêcher de dé-publier une annonce une fois qu’elle est publiée.
- Si l’annonce n’est pas encore publiée, on peut modifier sa valeur de publication lorsqu’on modifie l’annonce
- Si l’annonce est déjà publiée, on ne peut plus modifier sa valeur de publication lorsqu’on modifie l’annonce
Pour cela il faut utiliser les événements de formulaire. Ce sont des événements que le formulaire déclenche à certains moments de sa construction. Il existe l’événement PRE_SET_DATA qui est déclenché juste avant que les champs ne soient remplis avec les valeurs de l’objet ( les valeurs par défaut )
Le type Champ File
Un champ FileType de formulaire ne retourne pas du texte, mais une instance de la classe UploadeFile. Or nous allons stocker dans la base de données seulement l’adresse du fichier, donc du texte. Pour cela il faut ajouter un attribut dans l’entity Image (l’entity sous-jacent au formulaire)
VALIDATOR
Pour définir les règles de validation, il existe deux moyens :
- Utiliser les annotations (au sein des entités)
- Utiliser le YAML, XML ou PHP (hors des entités)
On peut mettre les règles de validation sur n’importe quel objet, qui n’est pas forcément une entité
- @Assert\Contrainte(valeur de l’option par défaut)
- @Assert\Contrainte(option1= »valeur1″, option2= »valeur2″, …)
- @Assert\Length(min=10, minMessage= »Le titre doit faire au moins {{ limit }} caractères. »)
- {{ limit }} est la longueur minimum définie dans l’option « min »
- Il est possible de mettre plusieurs contraintes sur un même attribut
- Toutes les contraintes disposent de l’option message, qui est le message à afficher lorsque la contrainte est violée
- Liste des contraintes
- Les noms de contraintes sont sensibles à la casse. Cela signifie que la contrainte DateTime existe, mais que Datetime ou datetime n’existent pas
Encore plus de règle de validation
Le composant Validation accepte les contraintes sur les attributs, mais également sur :
- Validation depuis un getter
- Valider intelligemment un attribut objet
- Lorsque je valide un objet A, comment valider un objet B en attribut, il faut utiliser la contrainte Valid qui va déclencher la validation du sous-objet B selon les règles de validation de cet objet B
- Valider depuis un Callback
- Vous pouvez parfois avoir besoin de valider des données selon votre propre logique
- L’exemple classique est la censure de mots non désirés dans un attribut texte
- Aussi par exemple interdire le pseudo dans un mot de passe
- L’avantage du Callback c’est de pouvoir ajouter plusieurs erreurs à la fois
- Valider un champ unique (UniqueEntity)
- Cette contrainte permet de valider que la valeur d’un attribut est unique parmi toutes les entités existantes. Pratique pour vérifier qu’une adresse e-mail n’existe pas déjà dans la BDD
- Cette contrainte ne se trouve pas dans le composant Validator, mais dans le bridge entre Doctrine st Symfony (ce qui fait le lien entre ces deux bibliothèques)
- use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
Valider selon nos propres contraintes
L’avantage d’avoir sa propre contrainte est double :
- D’une part, c’est une contrainte réutilisable sur vos différents objets
- D’autre part, cela permet de placer le code de validation dans un objet externe, et surtout dans un service
Une contrainte est toujours liée à un validateur, qui va être en mesure de valider la contrainte
N.B : Créer une classe qui hérite de la classe Contrainte et ajouter @Annotation cela veut dire que cette annotation soit disponible via des annotations dans les autres classes
<?php // src/OC/PlatformBundle/Validator/Antiflood.php namespace OC\PlatformBundle\Validator; use Symfony\Component\Validator\Constraint; /** * @Annotation */ class Antiflood extends Constraint { public $message = "Vous avez déjà posté un message il y a moins de 15 secondes, merci d'attendre un peu."; }
Les options de l’annotation correspondent en réalité aux attributs publics de la classe d’annotation
- @Antiflood(message= »mon message personnalisé »)
Un validateur contient une méthode validate() qui permet de valider ou non la valeur. Son argument $value correspond à la valeur de l’attribut sur laquelle on a défini l’annotation par exemple si on a fait :
/** * @Antiflood() */ private $content;
Alors c’est tout logiquement le contenu de l’attribut $contenu au moment de la validation qui sera injecté en tant qu’argument $value
La méthode validate() ne doit pas renvoyer true ou false pour confirmer que la valeur est valide ou non. Elle doit juste lever une Violation si la valeur est invalide
SÉCURITÉ ET GESTION DES UTILISATEURS
Symfony a bien séparé deux mécanismes différents : l’authentification et l’autorisation
l’authentification est le processus qui va définir qui vous êtes, en tan que visiteur. Ce qui gère l’authentification dans Symfony s’appelle un firewall. Ainsi on peut sécuriser des parties du site juste en forçant le visiteur à être un membre authentifié. Si le visiteur l’est, le firewall va le laisser passer, sinon redirection vers la page d’identification
l’autorisation est le processus qui va déterminer si vous avez le droit d’accéder à la page demandée. Il agit donc après le firewall. Ce qui gère l’autorisation dans Symfony s’appelle access control
La sécurité dans Symfony vient du bundle « SecurityBundle »
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Un encodeur est un objet qui encode les mots de passe de vos utilisateurs. Cette section permet de modifier l’encodeur utilisé pour vos utilisateurs
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
Cette section de la configuration dresse la hiérarchie des rôles, le rôle ROLE_USER est compris dans le rôle ROLE_ADMIN
security:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
in_memory:
memory:
users:
user: {password: userpass, roles: [‘ROLE_USER’]}
admin: {password: adminpass, roles: [‘ROLE_ADMIN’]}
Un provider est un fournisseur d’utilisateurs. Les firewalls s’adressent aux providers pour récupérer les utilisateurs et les identifier
security:
access_control :
Il y a différents moyens d’utiliser les contrôles d’accès :
- Soit depuis la configuration, en appliquant des règles sur des URL
- Soit directement dans les contrôleurs, en appliquant des règles sur les méthodes des contrôleurs
Les erreurs courantes
Il y a quelques pièges à connaître quand vous travaillerez plus avec la sécurité, en voici quelques-uns :
- Ne pas oublier la définition des routes (login, login_check et logout), se sotn ds routes obligatoires
- Les pare-feu ne partagent pas :
- Si vous utilisez plusieurs pare-feu, sachez qu’ils ne partagent rien le uns avec les autres. Ainsi, si vous êtes authentifiés sur l’un, vous ne le serez pas forcément sur l’autre, et inversement
- Bien mettre /login_check derrière le pare-feu
- Ne pas sécuriser le formulaire de connexion
Gestion des autorisations avec les rôles
Il existe quatre méthodes pour faire tester les rôles de l’utilisateur :
- Les annotations
- le service security.authorization_checker
- Twig
- Les contrôles d’accès
Symfony offre plusieurs moyens de sécuriser vos ressources (méthode de contrôleur, affichage, URL). N’hésitez pas à vous servir de la méthode la plus appropriée pour chacun de vos besoins. C’est la complémentarité des méthodes qui fait l’efficacité de la sécurité avec Symfony.
Utiliser des utilisateurs de la base de données
Dans Symfony, un utilisateur est un objet qui implémente l’interface UserInterface
Si vous définissez votre entity User, cette dernière doit implémenter l’interface UserInterface afin que Symfony l’accepte comme classe utilisateur de la couche sécurité
Qu’est-ce qu’un fournisseur d’utilisateurs (user provider)
- Il s’agit d’une classe qui implémente l’interafce UserProviderInterface
- Symfony dispose déjà trois types de fournisseurs qui implémente l’interface précédente
- memory : utilise les utilisateurs définis dans la configuration
- entity : utilise une entité pour fournir les utilisateurs
- id : permet d’utiliser un service quelconque en tant que fournisseur
FOSUserBundle
Lorsqu’un bundle A hérite du bundle B, cela signifie entre autre ;
- Si une vue du bundle A a le même nom qu’une vue du bundle B, c’est la vue A qui sera utilisée lorsque vous faites « BundleB::myView.html.twig »
- Si un contrôleur du bundle A a le même nom qu’un contrôleur du bundle B, c’est le contrôleur A qui sera utilisée lorsque vous faites « BundleB::myController:myAction »
Le bundle FOSUserBundle ne définit pas vraiment l’entité user, il définit une mapped-superclass, entité abstraire qu’il faut hériter pour en faire une vraie entité, on peu lui ajouter des attributs selon nos besoins)
Manipuler les utilisateurs avec FOSUserBundle
Si les utilisateurs sont gérés par FOSUserBundle, ils ne restent que des entités Doctrine2 classiques. Vous pourriez très bien créer un repository comme vous le savez le faire. Cependant, profitons du fait que le bundle intègre un UserManager (c’est une sorte de repository avancé). Ainsi vois des exemples :
RETOUR SUR LES SERVICES
Les tags : Les tags sont une fonctionnalité très importante des services. Cela permet d’ajouter des fonctionnalités à un composant sans en modifier le code. Un tag est une information que l’on appose à un ou plusieurs services afin que le conteneur de services les identifie comme tels. Ainsi, il devient possible de récupérer tous les services qui possèdent un certain tag.
Il est possible d’associer plusieurs tags à un même service
Sachez que tous les tags ne nécessitent pas forcément que votre service implémente une certaine interface.
- Il existe pas mal de tags prédéfinis dans Symfony, qui permettent d’ajouter des fonctionnalités
- Les services peuvent être utilisés avec le gestionnaire d’événements, via le tag kernel.event_listener
- Aussi avec le tag form.type permet de définir un nouveau type de champ de formulaire
Les dépendances optionnelles
L’injection de dépendances dans le constructeur est un très bon moyen de s’assurer que la dépendance sera bien disponible. Mais parfois vous pouvez avoir des dépendances optionnelles. Ce sont des dépendances qui peuvent être ajoutées au milieu de l’exécution de la page, grâce à des setters
Les calls sont un moyen d’exécuter des méthodes de votre service juste après sa création
L’utilité des calls : En plus du principe de dépendance optionnelle, l’utilité des calls est également remarquable pour l’intégration des bibliothèques externes (Zend Framework, GeSHI, …), qui ont besoin d’exécuter quelques méthodes en plus du constructeur
Les services courants de Symfony
- doctrine.orm.entity_manager
- event_dispatcher
- kernel
- logger
- mailer
- request_stack
- router
- security.token_storage
- service_container
- twig
- templating
LE GESTIONNAIRE D’EVENEMENTS
Qu’est ce qu’un événement:
Un événement correspond à un moment clé dans l’exécution d’une page. il existe plusieurs par exemple kernel.request qui est déclenché avant que le contrôleur ne soit exécuté, l’événement security.interactive_login qui correspond à l’identification d’un utilisateur
Qu’est ce que le gestionnaire d’événements ?
Voici un schéma explicatif
- Dans un premier temps, des services se font connaître du gestionnaire d’événements pour écouter tel ou tel événement. ils deviennent des listeners, des observateurs
- Dans un 2eme temps, quelqu’un (qui que ce soit) déclenche un événement. i.e qu’il prévient le gestionnaire d’événements (Subject) qu’un certain événement vient de se produire. à partir de là, le gestionnaire d’événement exécute chaque service (listener ou observateur) qui s’est préalablement inscrit pour écouter cet événement précis => c’est l’implémentation du design pattern Subjet/Observer
Donc le rôle de tout listener : un objet capable de décider s’il faut ou non appeler un autre objet qui remplira une certaine fonction. La fonction du listener n’est que de décider quand appeler l’autre objet. Parfois, il faut l’appeler à chaque fois qu’un événement est déclenché ; parfois cela dépend d’autres conditions.
Ecouter un événement
Pour que notre listener écoute quelques chose, il faut le présenter au gestionnaire d’événements. Il existe deux manières de faire :
- Manipuler directement le GE (depuis un contr^leur par exemple)
- $betaListener = new BetaListener(‘date’);
- $dispatcher = $this->get(‘event_dispatcher’);
- $dispatcher->addListener(‘kernel.response’, array($betaListener, ‘processBeta’));
- Passer par les services
- services:
oc_platform.beta.listener:
class: OC\PlatformBundle\EventListener\BetaListener
arguments:
– « @oc_platform.beta.html_adder »
– « 2017-09-01 »
tags:
– { name: kernel.event_listener, event: kernel.response, methode: processBeta }
– { name: kernel.event_listener, event: kernel.controller, methode: ignoreBeta }
- services:
A partir de maintenant, notre listener est accessible via le conteneur de services, en tant que service tout simple. Il faut définir le tag kernel.event_listener sur ce service. Le processus est le suivant :
- Une fois le gestionnaire d’événements instancié par le conteneur de services
- Il va récupérer tous services qui ont ce tag et exécuter le code de la méthode 1 afin d’enregistrer les listeners dans lui-même (GE). Tout cela se fait automatiquement
Le listener permet également de passer les bons arguments à notre service BetaHTMLAdder, En effet, lorsque le gestionnaire d’événements exécute ses listeners, il ne se préoccupe pas de leurs arguments. le 1er argument qu’il leur donne est un objet Symfony\Component\EventDispatcher\Event, représentant l’événement en cours
Symfony déclenche déjà quelques événements dans son processus interne.
Les événements Symfony
- kernel.request : cet événement est déclenché très tôt dans l’exécution d’une page, avant même que le choix du contrôleur à exécuter ne soit fait.
- Il permet à un listener de retourner immédiatement une réponse
- Il permet de modifier la requête en y rajoutant des attributs par exemple
- La classe de l’événement donné en argument par GE est GetResponseEvent
- kernel.controller : cet événement est déclenché après que le contrôleur à exécuter ait été défini, mais avant de l’exécuter effectivement.
- Il permet à un listener de modifier le contrôleur à exécuter
- La classe de l’événement donné en argument par GE est FilterContronllerEvent
- kernel.view : cet événement est déclenché lorsqu’un contrôleur n’a pas retourné d’objet Response
- Il permet à un listener d’attraper le retour du contrôleur pour soit construire une réponse lui-même, soit personnaliser l’erreur levée
- La classe de l’événement donné en argument par GE est GetResponseForContronllerResultEvent
- kernel.response : cet événement est déclenché après qu’un contrôleur a retourné un objet Response
- Il permet à un listener de modifier la réponse générée par le contrôleur avant de l’envoyer à l’internaute
- La classe de l’événement donné en argument par GE est FilterResponseEvent
- kernel.exception : cet événement est déclenché lorsqu’une exception est levée
- Il permet à un listener de modifier la réponse à envoyer à l’internaute, ou modifier l’exception
- La classe de l’événement donné en argument par GE est GetResponseForExceptionEvent
- security.interactive_login : cet événement est déclenché lorsqu’un utilisateur s’identifie via le formulaire de connexion
- Il permet à un listener d’archiver une trace de l’identification par exemple
- La classe de l’événement donné en argument par GE est InteractiveLoginEvent
- security.authentification.success : cet événement est déclenché lorsqu’un utilisateur s’identifie avec succès (quelque soit le moyen utilisé (formulaire de connexion, cookies, remember_me)
- Il permet à un listener d’archiver une trace de l’identification par exemple
- La classe de l’événement donné en argument par GE est AuthenticationEvent
- security.authentification.failure : cet événement est déclenché lorsqu’un utilisateur effectue une tentative d’identification échouée, quelque soit le moyen utilisé
- Il permet à un listener d’archiver une trace de la mauvaise identification par exemple
- La classe de l’événement donné en argument par GE est AuthenticationFailureEvent
- Créer nos propres événements : les événements Symfony couvrent la majeure partis de processus d’exécution d’une page, ou de l’identification d’un utilisateur. Mais des fois selon des besoins spécifiques on souhaites créer nos propres événements
- déléguer l’indexation dans un moteur de recherche
- recevoir des notifications à chaque message posté
Les souscripteurs d’événements
Se sont assez semblables aux listeners. La seule différence est la suivante :
- Au lieu d’écouter toujours le même événement défini dans un fichier de configuration, un souscripteur peut écouter dynamiquement un ou plusieurs événéments
- C’est l’objet souscripteur lui-même qui va dire au GE les différents événements qu’il veut écouter. Pour cela, un souscripteur doit implémenter l’interface EventSubscriberInterface
- Dans la déclaration de ce souscripteur au GE, ce n’est plus le tag kernel.event_listener qu’il faut utiliser, mais kernel.event_subscriber
- services:
oc_platform.bigbrother.message_listener:
class: OC\PlatformBundle\Bigbrother\MessageListener
arguments:
– « @oc_platform.bigbrother.message_notificator »
– [« alexandre », « marine », « pierre »]
tags:
– { name: kernel.event_subscriber }
- services:
La priorité des listeners
# src/OC/PlatformBundle/Resources/config/services.yml
services:
oc_platform.beta.listener:
class: OC\PlatformBundle\Beta\BetaListener
arguments:
– « @oc_platform.beta.html_adder »
– « 2017-06-01 »
tags:
– { name: kernel.event_listener, event: kernel.response, method: processBeta, priority: 2 }
La propagation des événements
Tous les événements qu’on a vu avaient deux méthodes en commun : stopPropagation et isPropagationStopped
la 1ere méthode permet à un listener de stopper la propagation de l’événement en cours, la conséquence est donc directe : tous les autres listeners qui écoutaient l’événement et qui ont une priorité plus faible ne seront pas exécutés
TRADUCTION
La traduction est possible dans Symfony dans deux endroits :
- Les contrôleur (services)
- $translator = $this->get(‘translator’);
- $texteTraduit = $translator->trans(‘Mon message à inscrire dans les logs’);
- les vues
- {{ ‘string’|trans }}
- {% trans %}…… {%endtrans%}
Les formats de catalogue
- Le format XLIFF
- Le format YAML
- Le format PHP
- php bin/console translation:update –force fr OCPlatformBundle
ParamConverters
CONVERTIR LES PARAMETRES DE REQUETES
L’objectif des ParamConverters est de vous faire gagner du temps. Il s’agit de transformer automatiquement un paramètre de route, comme {id} par exemple, en un objet
- Un ParamConverter convertit les paramètres de votre route au format que vous préférez, depuis la route, vous ne pouvez pas tellement agir sut vos paramètres, à part les appliquer des expressions régulières. Les ParamConverters agissant après le routeur, mais avant le contrôleur pour venir transformer à souhait ces paramètres.
- Le résultat des ParamConverters est stocké dans les attributs de requête, i.e qu’on peut les injecter dans les arguments de l’action du contrôleur
- DoctrineParamConverter : ce ParamConverter convertit les paramètres directement en entités Doctrine
- Un ParamConverter est en réalité un simple listener, qui écoute l’événement kernel.controller
- Pour déterminer le type de variable, le ParamConverter a deux solutions :
- Regarder la signature de la méthode du contrôleur
- Utiliser une annotation @ParamConverter, ce que nous permet de définir nous-même les informations dont il a besoin
- Exemple :
- oc_platform_view:
path: /advert/{advert_id}
defaults:
_controller: OCPlatformBundle:Advert:view
requirements:
advert_id: \d+ - <?php
// src/OC/PlatformBundle/Controller/AdvertController.phpuse Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;/**
* @ParamConverter(« advert« , options={« mapping »: {« advert_id« : « id« }})
*/
public function viewAction(Advert $advert)
- oc_platform_view:
- Il est possible de récupérer une entité grâce à plusieurs attributs
- <?php
// src/OC/PlatformBundle/Controller/AdvertController.phpuse Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;// La route serait par exemple :
// /platform/advert/{advert_id}/{skill_id}/**
* @ParamConverter(« advertSkill« , options={« mapping »: {« advert_id« : « advert« , « skill_id« : « skill« }})
*/
public function viewAction(AdvertSkill $advertSkill)
- <?php
- Grâce au annotation, il est possible d’appliquer plusieurs ParamConverters à plusieurs arguments
- # src/OC/PlatformBundle/Resources/config/routing.ymloc_platform_view:
path: /advert/{advert_id}/applications/{application_id}
defaults:
_controller: OCPlatformBundle:Advert:view - <?php
/**
* @ParamConverter(« advert« , options={« mapping »: {« advert_id« : « id« })
* @ParamConverter(« application« , options={« mapping »: {« application_id« : « id« })
*/
public function viewAction(Advert $advert, Application $application)
- # src/OC/PlatformBundle/Resources/config/routing.ymloc_platform_view:
- Le ParamConverter Datetime
- # src/OC/PlatformBundle/Resources/config/routing.ymloc_platform_list:
path: /list/{date}
defaults:
_controller: OCPlatformBundle:Advert:viewList - <?php
// src/OC/PlatformBundle/Controller/AdvertController.phpuse Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;/**
* @ParamConverter(« date« , options={« format »: « Y-m-d »})
*/
public function viewListAction(\Datetime $date)
- # src/OC/PlatformBundle/Resources/config/routing.ymloc_platform_list:
Créer ses propres ParamConverters
Il y un listener, il s’agit https://github.com/sensio/SensioFrameworkExtraBundle/blob/master/EventListener/ParamConverterListener.php
Ce listener écoute l’événement kernel.controller, ce qui lui permet de connaître le contrôleur qui va être exécuté. L’idée est qu’il parcourt les différents ParamConverters pour exécuter celui qui convient le premier, un exemple :
<?php
foreach ($converters as $converter) {
if ($converter->supports($configuration)) {
if ($converter->apply($request, $configuration)) {
return;
}
}
}
Comment symfony trouve tous les convertisseurs
Symfony utilise le mécanisme des tags des services, donc un convertisseur est avant tout un service, sur lequel on a appliqué le tag request.param_converter
- Un ParamConverter vous permet de créer un attribut de requête que vous récupérez ensuite en argument de vos méthodes de contrôleur
- Il existe deux ParamConverters par défaut avec Symfony : Doctrine et Datetime
PERSONNALISER LES PAGES D’ERREUR
Le comportement par défaut du noyau consiste à appeler un contrôleur particulier intégré à Symfony
TwigBundle:Exception:show
Les vues de ces pages d’erreur se situent dans le bundle TwigBundle
vendor\symfony\symfony\src\Symfony\Bundle\TwigBundle\Resources\views\Exception
Remplacer les vues d’un bundle
Il est très simple de remplacer les vues d’un bundle quelconque par les nôtres. Il suffit de créer le répertoire :
- app/Resources/NomDUBundle/views/ et d’y placer nos vues à nous
- nos vues doivent porter les mêmes noms que celles qu’elles remplacent
ASSETIC POUR GERER LES CODES CSS ET JS
Symfony intègre un bundle nommé Assetic qui va s’occuper de gérer ces ressources efficacement, il va permettre d’optimiser au maximum le chargement de ces ressources pour nos visiteurs
A propos du nombre de requêtes HTTP d’une page web
- Le temps d’envoi de la requête au serveur lors du clic
- Le temps d’exécution de la page côté serveur, le temps PHP
- Le temps d’envoi du code HTML par le serveur vers la navigateur
- A partir de cette page HTML que le navigateur reçoit, ce dernier doit tout recommencer pour chaque fichier CSS, chaque fichier JS et chaque image
- Par exemple , 5 CSS et 3 JS et 15 images, cela fait un total de 23 requêtes HTTP à traiter par votre navigateur pour vous afficher l’intégralité de la page donc du temps incompréhensibles
- Les seules solutions côté Front sont
- diminuer le nombre de requête, grouper ces fichiers (Assetic va intervenir )
- diminuer la taille du fichier ( outil comme YUI Compressor ): minifier
- php bin/console assets:install –symlink
Le filtre cssrewrite
Lorsque le fichier CSS était placé dans web/bundles/xxxx/css, ce chemin relatif pointait bien vers web/bundles/xxx/img, là où sont nos images. Or maintenant, du point de vue navigateur,le fichier CSS est dans app_dev.php/csss, du coup le chemin relatif vers les images n’est plus bon
C’est ici qu’intervient le filtre cssrewrite
{% stylesheets filter=’cssrewrite’
‘bundles/ocplatform/css/main.css’
‘bundles/ocplatform/css/platform.css’ %}
<link rel= »stylesheet » href= »{{ asset_url }} » type= »text/css » />
{% endstylesheets %}
Ce filtre permet de réécrire tous les chemins relatifs contenus dans les fichiers CSS, afin de prendre en compte la modification du répertoire du CSS
Les filtres scssphp et jsqueeze
Ces filtres sont très utiles, ce sont ceux qui « minifient » les fichiers
- {% stylesheets filter=’cssrewrite, scssphp’
… %} - {% javascripts filter=’jsqueeze’
… %} - {% stylesheets filter=’?scssphp’
… %}
Explication
Lors du mode dev, Assetic passe directement par un contrôleur pour générer à la volée nos ressources. Mais aussi « minifier » et regrouper des fichiers à la volée et ce pour chaque requête, cela prend beaucoup de temps. Si en mode dev on peut se permettre, on ne le peut pas en mode prod
Du coup l’astuce pour le mode prod est d’exporter en dur, une bonne fois pour toutes, les fichiers CSS et JS dont on a besoin. Il faut utiliser la commande d’Assetic :
-
php bin/console assetic:dump --env=prod
- Cette commande va lire toutes nos vues pour trouver les balises {% stylesheets %} et {% javascripts %}
- Puis elle va exporter en dur dans les fichiers /web/css/xxx.css et /web/js/xxx.js
Composant Console
Chaque commande est définie dans une classe PHP distincte, que l’on place dans le dossier Command des bundles, ces classes comprennent entres autres deux méthodes:
- configure() : qui définit le nom, les arguments et la description de la commande
- execute() : qui exécute la commande
Il existe un bundle qui donne la possibilité de travailler avec la console le consoel depuis le navigateur, il s’agit du bunde : CoreSphereConsoleBundle
DÉPLOYER SON SITE EN PRODUCTION
La méthodologie est la suivante :
- Uploader votre code à jour sur le serveur de production
- Mettre à jour vos dépendances via Composer
- Mettre à jour votre base de données
- Vider le cache
- Mais avant, il faut s’assurer qu’elle soit parfaite déjà en local
Préparer son application en local
- Vider le cache, tout le cache (prod et dev)
- Test l’environnement de production
- // web/app.php$kernel = new AppKernel(‘prod’, true); // Définissez ce 2e argument à true
- Pensez à bien remettre ce paramètre à false lorsque vous avez fini vos tests
- Soigner ses pages d’erreur
- Installer une console sur navigateur
- Vérifier la qualité de votre code source : insight.sensiolabs.com
- Vérifier la sécurité de vos dépendances : security.sensiolabs.org
- Soit envoyer manuellement votre fichier composer.lock sur l’interface en ligne de l’outil
- Soit utiliser la ligne de commande : security:check
Vérifier et préparer le serveur de production
- Vérifier la compatibilité du serveur
- Symfony intègre le fichier web/config.php qui fait toutes les vérifications de compatibilité nécessaires
- Vous n’avez pas encore d’hébergeur et en cherchez un compatible
- La version de PHP >= 5..5.9
- L’extension SQLite 3 doit être activée
- L’extension JSON doit être activée
- L’extension Ctype doit être activée
- Le paramètre date.timezone doit être défini dans le php.ini
Déployer votre application
Il y a deux cas pour déployer votre application sur votre serveur :
- Vous n’avez pas accès en SSH à votre serveur (hébergements mutualisés), dans ce cas vous devez envoyer vos fichier à la main
- Vider le cache à la main
- Envoyer les fichiers sur le seveur
- Si vous avez accès à Composer sur votre serveur, alors n’envoyer pas vos vendors à la main
- Envoyer les deux fichier composer.json et composer.lock
- Ensuite sur votre serveur exécutez la commande :
- php composer.phar install remarquez bien install et non update
- Régler les droits sur le dossier /var
- Vous avez accès en SSH à votre serveur (serveur dédiés, VPS), dans ce cas il vous faut utiliser Capifony, un outil fait pour automatiser le déploiement, un outil Ruby
Les derniers préparatifs
- S’autoriser l’environnement de développement
- Pour exécuter les commandes Symfony, notamment celles pour créer la base de données, il nous faut avoir accès à l’environnement de développement
- Mettre en place la base de donneés
- Modifier le fichier app/config/parameters.yml afin d’adapter les valeurs des paramètres database_*
- Généralement sur un hébergement mutualisé vous n’avez pas le choix de la base de données, il y a pas mal de restrictions, alors il faut créer la base de données que vous avez dans le parameters.yml
- php bin/console doctrine:database:create
- php bin/console doctrine:schema:update –force
- S’assurer que tout fonctionne
- Avoir de belles URL
- à ce moment vous avez des urls de genre http://www.abetari.be/Symfony/web/
- Pour ce la il faut utiliser l’URL Rewriting, une fonctionnalité du serveur web Apache
- L’objectif est que les requêtes /platform et css/style.css arrivent respectivement sur /web/platform et /web/css/style.css
- Méthode .htaccess
- Méthode VirtualHost
- L’objectif est que les requêtes /platform et css/style.css arrivent respectivement sur /web/platform et /web/css/style.css
- A chaque modification de code source que vous envoyer sur la production, vous devez obligatoirement vider le cache
- Les mises à jour de la base de données
- En ce qui concerne la base de données c’est un peu pus compliqué : il faut faire attention aux données que vous avez déjà, c’est pour cela la commande doctrine:schema:update est à bannir
- Par contre il existe une bibliothèque pour Doctrine appelée DoctrineMigration et bien sûr son bundle correspondant
- Lorsque vous modifiez ou ajoutez une entité, vous créez en même temps un fichier de migration
- Ce fichier reflète les changements que vous avez effectués : il contient les requêtes SQL permettant de mettre à jour la base de données pour coller à vos changements
- Une fois les fichiers sont en production, vous exécutez alors tous les fichiers de migrations depuis la dernière mise en production
- N.B : Les fichiers de migrations sont automatiquement exécutés par Capifony lors des mises en production
- Une checklist pour vos déploiements http://www.symfony2-checklist.com/fr
HTTP Cache et Symfony2
Le moyen le plus efficace pour améliorer les performances d’une application est de mettre en cache la sortie complète de la page, puis de contourner l’application entièrement à chaque demande ultérieure.
- Etape 1: Cache passerelle (gateway cache) ou un proxy inverse est une couche indépendante qui se trouve en face de la demande client. Le reverse proxy cache les réponses retournées par l’application et répond aux demandes par ces réponses mises en cache. Symfony2 fournit son propre proxy inverse, mais tout autre peut être used
- Etape 2 :Cache HTTP en-têtes sont utilisés pour communiquer avec le cache de la passerelle entre l’application et le client. Symfony2 fournit par défaut une interface puissante pour interagir avec les en-têtes de cache
- Etape 3 :Modèle HTTP d’expiration et de validation, se sont les deux modèles utilisés pour déterminer si le contenu mis en cache est frais ou périmé
- Etape 4 : Edge Side Includes (ESI) pour permettre au cache HTTP d’être utilisé à des fragments de page du cache indépendamment. Avec ESI, vous pouvez même mettre en cache une page entière pour 60 minutes, avec une barre latérale intégrée pour seulement 5 minutes.
Types de caches
Les en-têtes de cache HTTP envoyées par l’application sont consommés et interprétés par un maximum de trois différents types de caches :
- Cache navigateur : Chaque navigateur contient son propre cache local. C’est un cache privé
- Cache proxy : Il est généralement installé par les grandes entreprises afin de réduire la latence et le trafic réseau
- Cache de passerelles : comme un proxy, c’est aussi un cache partagé, mais coté serveur
Configuration Monolog
Configuration des logs Monolog
Les logs vous permettent de garder une trace de ce qui se passe sur l’application. Symfony permet de gérer simplement la rotation des fichiers de log pour ne pas perdre en espace disque.
Monolog utilise plusieurs niveaux ( du moins alertant au plus critique)
- DEBUG / INFO / NOTICE / WARNNING / ERROR / CRITICAL / ALERT / EMERGENCY
- monolog:
- handlers:
- main:
- type: fingers_crossed
- action_level: error
- handler: nested
- nested:
- type: stream
- path: « %kernel.logs_dir%/%kernel.environment%.log »
- level: debug
- console:
- type: console
- main:
- handlers:
Ici main, nested et console sont appelés des handlers (gestionnaires), le nom donné est arbitraire. Pour chaque handler, on définit un type
Chaque handler est ensuite appelé dans l’ordre défini (on a parfois des handlers imbriqués, ils ne sont pas appelés par défaut, c’est le cas ici puisque nested est imbriqué dans main).
Dans cet exemple, nous avons un FingersCrossedHandler (handler qui en déclenche un autre, ici il s’agit de main) qui se déclenche seulement lorsque le niveau de log attendu est atteint (ici on attend un log de type error). Ce handler, une fois déclenché, appelle le handler nested. Ce dernier est de type stream (handler qui écrit les logs) qui va écrire les logs dans un fichier à partir d’un level défini (debug dans ce cas). Le handler console est déclenché quant à lui pour tous les logs.
Différentes type de handlers
- finders_crossed : Ce handler stocke dans un buffer tout les logs qui passe. Lorsqu’un des logs dépasse le niveau minimum requis, il appelle un autre handler avec tous les logs contenus dans son buffer
- stream : Ce handler écrit le log qu’il reçoit dans un fichier si son niveau dépasse le niveau mimimum requis
- rotating_file : Ce handler fait la même chose que stream mais fait une rotation des fichiers pour effacer les logs anciens
- group : Ce handler envois le log reçu à plusieurs handlers
- buffer : Il stock dans un buffer tout les logs qu’il reçoit puis envois le buffer à un handler à la fin de l’exécution de la requête.
- swit_mailer : Il envoie par mail les logs (souvent passé par un handler de type buffer)
- console : Il permet de définir les niveaux d’affichage de log dans la console.
Des Exemples
- Envoyer les alertes par mail
- monolog
- handlers:
- mail:
- type : fingers_crossed
- action_level: critical
- handler: buffered
- buffered:
- type: buffer
- handler: swift
- swift:
- type: swift_mail
- from_email: contact@domaine.com
- to_email : error@domaine.com
- subject: Un erreur critique est survenue
- level: info
- mail:
- handlers:
- monolog
Ici on attend un log de niveau critical pour déclencher le handler buffered. Une fois déclenché, le handler bufferd va stocker tout les logs et les passer à la fin d’exécution de la requête du client. au handler swift. Ce dernier va envoyer un mail en triant les logs reçus et en ne gardant que ceux de niveau miminum info
N.B : Un handler de type buffer appelle un handler une seule fois avec le contenu de son buffer alors que finger_crossed appelle un autre handler pour chaque log contenu dans son buffer
- Rotation des logs
- monolog:
- handlers:
- main:
- type: rotating_file
- max_files: 10
- path: « %kernel.logs_dir%/%kernel.environment%.log »
- level : debug
- main:
- handlers:
- monolog:
Ici on écrit tous les logs de niveau supérieur à debug dans un fichier en rajoutant la date du jour dans le nom du fichier. Au bout de 10 fichiers créés, le plus vieux est supprimé automatiquement dès qu’un nouveau est créé et ainsi de suite.
- Les channels
- monolog:
- handlers:
- main:
- type: stream
- path: /var/log/symfony.log
- channels: [!doctrine, !security]
- doctrine:
- type: stream
- path: /var/log/doctrine.log
- channels: doctrine
- login:
- type: stream
- path: /var/log/auth.log
- channels: security
- main:
- handlers:
- monolog:
Les logs utilisent des channels pour s’identifier. Par exemple, les logs de doctrine sont sur le channel « doctrine »
Ici on écrit tous les logs qui ne viennent pas de doctrine, ni de security dans un fichier symfony.log. On écrit tous les logs de doctrine dans doctrine.log et tout ceux de security dans un auth.log
- Appeler plusieurs handler à partir d’un seul
Imaginons que vous souhaitez que lorsqu’un log de type critical est lu, un mail vois soit envoyé et qu’il soit écrit dans un fichier avec rotation.
- monolog:
- handlers:
- main_critical:
- type: fingers_crossed
- action_level: critical
- handler: grouped
- grouped:
- type: group
- members: [streamed, buffered]
- streamed:
- type: rotating_file
- max_files: 15
- path: %kernel.logs_dir%/%kernel.environnement%.critical.log
- level: info
- buffered:
- type: buffer
- handler: swift
- swift:
- type: swift_mailer
- from_email : %email.from%
- to_email: %email.super_admin%
- subject: Critical Error Ocuured
- level: error
- main_critical:
- handlers:
Dès qu’in log critical est lu, le handler main_critical appelle le handler grouped pour chaque log de son buffer. Le handler grouped qui est de type group va envoyer chaque log vers le handler streamed et buffred en même temps. Ces deux handler vont ensuite remplir leurs fonctions : l’un écrire avec rotation de fichier et l’autre stocke dans un buffer avant de l’envoyer au handler qui envoit un mail.