Académique Documents
Professionnel Documents
Culture Documents
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Introduction
2. Symfony : Un projet PHP
a. Préconisation d’installation
b. Installation sous Linux
c. Installation sous Windows
3. L’installeur Symfony
4. Composer
a. Installer Composer
5. Les environnements de développement pour Symfony
a. Un IDE pour Symfony !
b. PHPStorm
c. Eclipse IDE for PHP Developers
d. Visual Studio Code
e. Conclusion
L’outillage nécessaire
1. Introduction
Avant de se lancer dans la création d’un projet Symfony, il est nécessaire de
s’équiper ! Un certain nombre d’outils sont nécessaires pour mettre en place le projet
et pour développer, leur mise en place correcte est détaillée ci-après. Certains de ces
outils sont indispensables pour tout développeur Symfony, d’autres sont optionnels ou
possèdent des alternatives. Il est essentiel de bien comprendre leur rôle et de les
installer à bon escient avant d’aller plus loin dans la construction de son projet.
Pour cette commande de sécurisation de MySQL, il est nécessaire d’être attentif aux
questions posées pour sécuriser correctement votre serveur.
L’ultime étape d’installation consiste à fournir le support de PHP dans le serveur web
Apache, la commande suivante permet de tous les installer :
sudo apt install php libapache2-mod-php php7.4-mysql php7.4-common
php7.4-mysql php7.4-xml php7.4-xmlrpc php7.4-curl php7.4-gd
php7.4-imagick php7.4-cli php7.4-dev php7.4-imap php7.4-mbstring
php7.4-opcache php7.4-soap php7.4-zip php7.4-intl -y
Sur une distribution CentOS, c’est MariaDB, la variante libre de MySQL, qui est
proposée dans les paquets ; son installation se fait avec la commande :
sudo yum install mariadb-server mariadb -y
Installation complémentaire
Pour compléter et finaliser cette procédure en environnement Linux, vous
aurez également besoin d’un outil d’administration graphique pour
MySQL/MariaDB ; nous vous conseillons d’installer par exemple MySQL
Workbench qui est une application de bureau, ou bien phpMyAdmin qui est une
application web. Ces deux outils populaires offrent sensiblement les mêmes
fonctionnalités quant à la création et la manipulation de bases de données et sont
présents dans les dépôts de paquets des distributions Linux.
c. Installation sous Windows
Une plateforme Apache, PHP, MySQL s’installe très facilement sous Windows à
condition d’utiliser un paquet prêt à l’emploi ; ces paquets sont spécialement conçus
pour installer rapidement un environnement de développement autour de PHP.
Plusieurs produits existent sur le marché, en voici quelques-uns, cette liste n’est pas
exhaustive :
XAMPP : https://www.apachefriends.org/
Wamp Server : https://www.wampserver.com/
Laragon : https://laragon.org/
AMPPS : http://www.ampps.com/
L’installation de ces différentes solutions passe par un installeur qui met en place la
plateforme rapidement. Les produits se contrôlent et se configurent ensuite par un
panneau de contrôle accessible depuis la zone de notification de Windows.
3. L’installeur Symfony
L’installeur Symfony est un outil en ligne de commande qui constitue un
premier moyen de créer un projet basé sur le framework. L’utilisation de cet outil
n’est pas obligatoire pour développer avec Symfony, mais il apporte quelques petites
facilités non négligeables, comme par exemple un serveur web embarqué pour
exécuter les applications, ou bien encore un utilitaire de vérification des prérequis
avant de démarrer un projet.
L’installeur est téléchargeable à l’adresse https://symfony.com/download.
Vous devriez voir apparaître une liste de commandes disponibles pour Composer.
Si cette commande ne fonctionne pas, essayez de relancer un nouveau terminal.
Support de Symfony
Pour ajouter le support de Symfony dans Visual Studio Code, il faut cliquer sur
l’icône Extensions dans la barre d’icônes se trouvant sur la gauche de la fenêtre
principale, puis saisir Symfony dans le champ de recherche. Une multitude de
résultats de recherche sont retournés car plusieurs extensions sont disponibles pour
Symfony. L’une des plus complètes est le Symfony Extension Pack, qui regroupe
plusieurs extensions PHP/Symfony ; les autres sont pertinentes également et libre à
vous de les essayer.
Sélectionnez le bouton Install sur Symfony Extension Pack.
e. Conclusion
D’autres outils de développement existent pour PHP et Symfony, les outils présentés
ci-dessus ne constituent que des exemples d’usage. Vous pouvez tout à fait choisir de
continuer à utiliser vos outils habituels de développement PHP si vous ne souhaitez
pas modifier vos habitudes.
Précédent
Quiz
Suivant
Création d’un projet Symfony
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Tout déplier | Tout replier
Informations générales
Titre, auteur...
Introduction
Avant-propos
Public visé
Prérequis
Objectifs du livre
Le développement avec les frameworks
Symfony
Quiz
Mise en place d’un projet Symfony
L’outillage nécessaire
Création d’un projet Symfony
1. Prérequis
2. Création via l’installeur Symfony
3. Création via Composer
4. Configurer son serveur web
a. Serveur web PHP
b. Apache et Nginx
Structure de l’application
Quiz
Architecture du framework
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Le résultat affiché à l’écran peut vous indiquer des erreurs dans la configuration de
PHP qu’il faudra corriger avant d’aller plus loin. En plus des erreurs, il peut
également y avoir des préconisations d’optimisations faites par l’outil, mais tant que
le résultat est « OK », vous pouvez continuer.
La création du projet se fera ensuite grâce à la commande :
symfony new mon_projet --version=4.4 --full
Cette commande est beaucoup plus verbeuse lors de la création du projet puisqu’elle
affiche la liste complète des dépendances Composer installées ; elle affiche également
des conseils à la fin de la création du projet. Contrairement à l’installeur Symfony,
elle ne crée pas de dépôt Git pour le projet, mais elle le suggère.
Pour plus d’informations sur la commande create-project, vous pouvez exécuter la
commande suivante : composer help create-project.
La fin de l’affichage généré par la commande create-project de Composer et les
étapes suivantes pour démarrer avec son nouveau projet.
symfony/website-skeleton est un paquet. Il est composé d’un préfixe (symfony), puis
d’un nom (website-skeleton), le préfixe permettant d’éviter les conflits de nommage
entre différentes organisations.
Concrètement, un paquet est un projet PHP : une librairie ou un framework par
exemple. Ici, nous installons un squelette d’application Symfony, avec des librairies
préinstallées, dont nous pouvons nous servir de base pour nos projets.
Composer est préconfiguré avec un dépôt, Packagist (https://packagist.org). Par
défaut, seuls les paquets listés dans ce dépôt sont installables.
Les paquets installés pour votre projet sont définis au sein du fichier composer.json,
à la racine du projet, dans les sections require et require-dev.
Votre application est maintenant accessible ; ouvrez votre navigateur favori et entrez
l’URL suivante : http://localhost:8000
Par défaut, Symfony utilisera le port 8000, mais si celui-ci ne vous convient pas, vous
pouvez tout de même en spécifier un autre :
php bin/console server:run localhost:8080
127.0.0.1 localhost
Ce fichier est utilisé par le système d’exploitation pour la résolution DNS (système de
noms de domaine) et il contient par défaut une entrée, pour localhost.
Cette entrée signifie que, lorsque la machine aura besoin de transcrire le nom de
domaine localhost en adresse IP, elle choisira 127.0.0.1 directement, sans avoir
besoin d’envoyer une requête vers un quelconque serveur DNS.
C’est pour cette raison que, lorsque vous saisissez localhost dans la barre d’adresse de
votre navigateur web, et dans la mesure où un serveur web est installé sur votre
machine, vous pouvez accéder à du contenu local.
Voici le contenu du fichier hosts après avoir ajouté le nom de domaine
local monjournal.local :
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
127.0.0.1 localhost
127.0.0.1 monjournal.local
Maintenant que le domaine local est configuré, il ne reste plus qu’à configurer le
serveur web.
Nos configurations se basent sur un projet Symfony installé au sein du
répertoire /var/www/monjournal mais vous devrez adapter cette valeur selon
l’emplacement dans lequel votre projet est installé.
N’oubliez pas d’ajouter le répertoire public après celui-ci. Sous Windows, un chemin
vers votre projet pourrait être C:\wamp\www\monjournal par exemple.
Apache
Voici la configuration minimale d’un VirtualHost pour Apache :
<VirtualHost *:80>
ServerName monjournal.local
DocumentRoot /var/www/monjournal/public
<Directory /var/www/monjournal/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Installation du apache-pack
Symfony facilite la configuration d’Apache avec un fichier .htaccess au sein du sous-
répertoire public. Avec Symfony 4, ce fichier n’est plus présent par défaut et doit être
installé avec une recette Flex (cette notion sera abordée dans un autre chapitre) ; pour
ce faire, exécutez la commande suivante depuis le répertoire de l’application
Symfony :
composer require apache-pack
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Arborescence du projet
2. Règles et conventions d’organisation du projet
a. Le standard PSR-4
b. Conventions de nommage
3. La configuration d’une application Symfony
a. Les annotations
b. Le format YAML
c. Le format XML
d. Le format PHP
e. Choisir son format de configuration
Quiz
Architecture du framework
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Structure de l’application
1. Arborescence du projet
Une application Symfony contient différents répertoires et chacun d’eux a un rôle
précis.
bin : ce dossier contient les exécutables de votre projet ou de ses dépendances. Il
contient notamment la console Symfony.
config : ce répertoire contient majoritairement les fichiers de configuration de
l’application.
migrations : lorsque vous concevez les différentes classes d’objets qui
seront persistés en base de données, les outils Symfony (la console !) génèreront
des scripts PHP dans ce répertoire, permettant ainsi de mettre à jour le schéma de
base de données.
public : ce dossier comporte toutes les ressources publiques. Ces fichiers sont
donc accessibles directement depuis le Web, via l’URL
http://mon_projet.local/<nom_du_fichier>. Les fichiers de ce répertoire sont
souvent statiques (feuilles de style CSS, fichiers de scripts JavaScript, etc.), à une
exception près, le contrôleur frontal (cf. chapitre Architecture du framework -
Architecture de Symfony), c’est-à-dire le fichier index.php.
src : ce répertoire contient vos classes métiers, contrôleurs, etc. Pour faire simple,
c’est le cœur de votre projet, c’est dans ce dossier que se déroule la quasi-totalité
du développement.
templates : l’emplacement de stockage des templates et des gabarits Twig. Les
templates constituent les écrans (ou encore les vues) de votre application ; ils
mélangent code HTML et langage Twig pour le rendu dynamique des écrans.
tests : les différents tests logiciels, unitaires, d’intégration ou fonctionnels seront
stockés dans ce répertoire afin de pouvoir être exécutés par PHPUnit.
translations : si vous souhaitez internationaliser votre application, vous devez
fournir des fichiers de langues (ou fichiers de traductions) pour chaque langue
supportée par celle-ci. Ces fichiers devront résider dans ce répertoire.
var : ce dossier contient le cache et les logs (historiques des événements) de votre
application.
vendor : ce répertoire a été généré par Composer et il contient tous les
paquets (librairies ou bundles) dont votre projet dépend. Vous pouvez ignorer ce
dossier, mais vous ne devez en aucun cas le versionner car Composer est chargé
de le générer pour chaque installation/déploiement de votre projet.
class MaClasse
{
public function maMethode()
{
// ...
}
// ...
}
Veillez à ne pas utiliser de tiret bas (underscore) dans vos noms de classes car le tiret
bas était utilisé comme alternative aux espaces de noms avant leur apparition avec
PHP 5.3.0. Il n’est donc pas pris en charge par la norme PSR-4 et provoquerait des
résultats inattendus.
b. Conventions de nommage
Symfony préconise de respecter un certain nombre de conventions de nommage au
sein d’un projet. Ces conventions sont déjà appliquées pour les fichiers présents dans
l’application générée et le seront pour le code généré automatiquement par la console.
Les respecter permet d’harmoniser le code dans l’application et de faciliter la lecture
et la compréhension de celui-ci.
La convention Camel Case pour le code
La convention Camel Case est la convention utilisée par la grande majorité des
langages objets. Elle s’applique aux identificateurs de classes, méthodes, attributs et
constantes.
Classes : le nom d’une classe doit commencer par une majuscule et chaque mot
qui constitue le nom de la classe commence également par une majuscule.
Exemple : IdentificationUtilisateurService.
Méthodes : le nom d’une méthode doit commencer par une minuscule et chaque
mot qui constitue le nom de la classe commence par une majuscule.
Exemple : maMethodeDeService().
Attributs : les attributs suivent les mêmes conventions que les méthodes.
Exemple : monAttribut.
Constantes : les constantes s’écrivent en majuscules, les mots sont séparés par
des caractères tiret bas (underscore). Exemple : NOMBRE_DE_ROUES.
La convention Snake Case
La convention Snake Case utilise des minuscules exclusivement. Elle permet de
représenter des niveaux hiérarchiques séparés par le point (.) et les mots sont séparés
par le caractère underscore (_).
Cette convention s’utilise dans Symfony pour :
les variables Twig. Exemple : http_status_code ;
les paramètres de configuration. Exemple : framework.csrf_protection.
Le nommage des éléments du modèle objets
Au-delà des classes, PHP utilise également la notion d’interfaces, de classes
abstraites, de traits. Des conventions sont appliquées pour ces éléments spécifiques du
modèle objet.
Les interfaces sont suffixées par Interface. Exemple : UserInterface.
Les traits sont suffixés par Trait. Exemple : AuthMethodsTrait.
Les classes d’exceptions personnalisées sont suffixées par Exception.
Exemple : UserLoginErrorException.
Les classes abstraites sont suffixées par Abstract.
Exemple : AbstractUserService.
Le nommage des fichiers
À cause de la norme PSR-4, les fichiers PHP doivent suivre les conventions de
nommage des classes, c’est-à-dire la convention Camel Case des classes :
IdentificationUtilisateurService est une classe qui devra se trouver dans le
fichier IdentificationUtilisateurService.php
Les templates Twig et les fichiers statiques (images, css, JavaScript) seront nommés
en utilisant la convention Snake Case.
Exemples :
Un template Twig : article.html.twig ;
Un fichier CSS : main.css.
Autres conventions d’écriture
De nombreuses conventions sont encore applicables pour les projets Symfony, vous
en trouverez la liste exhaustive à
l’adresse : https://symfony.com/doc/current/contributing/code/standards.html
b. Le format YAML
YAML est un langage simple qui décrit les données. C’est un acronyme récursif
signifiant YAML Ain’t Markup Language (YAML n’est pas un langage de
balisage) pour faire un pied de nez à la verbosité des langages de balisage comme
XML. YAML a été créé par Clark Evans en 2001.
YAML est très présent dans Symfony car c’était le langage par défaut de
configuration du framework dans la toute première version. Il est aujourd’hui
toujours autant utilisé, notamment dans les fichiers existant par défaut dans le dossier
config d’une application Symfony 4.
Comme PHP, il a une syntaxe pour les types simples comme les chaînes, les booléens,
les décimaux ou les entiers. Mais contrairement à PHP, il fait une différence entre les
tableaux (séries) et les hachages (mappings).
Exemple de configuration
Dans cet exemple, nous illustrons la définition d’une route permettant
l’exécution d’une méthode hello() dans une classe de contrôleur
nommée WelcomeController.
ma_route:
path: /hello/world
controller: App\Controller\WelcomeController::hello
c. Le format XML
XML est un format de prédilection depuis de nombreuses années pour la définition de
données de configuration applicatives. Son principal avantage est de pouvoir être
associé à des grammaires de validation, permettant ainsi de vérifier la structure et les
données d’un document. Néanmoins, il reste plus verbeux que YAML ou les
annotations, par exemple.
Exemple de configuration
Dans cet exemple, nous illustrons la définition d’une route permettant
l’exécution d’une méthode hello() dans une classe de contrôleur
nommée WelcomeController.
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="ma_route"
path="/hello/world"
controller="App\Controller\WelcomeController::hello" />
</routes>
d. Le format PHP
Une dernière possibilité reste l’utilisation de PHP pour configurer son application
Symfony. Le fait d’utiliser directement PHP peut permettre un (très) léger gain en
performance par rapport au fait d’utiliser des fichiers YAML, XML, ou bien des
annotations. Mais la faible lisibilité du format est clairement un frein.
Exemple de configuration
Dans cet exemple, nous illustrons la définition d’une route permettant
l’exécution d’une méthode hello() dans une classe de contrôleur
nommée WelcomeController.
return function (RoutingConfigurator $routes) {
$routes->add('ma_route', '/hello/world)
->controller([WelcomeController::class, 'hello'])
;
};
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Définitions et responsabilités
a. La vue
b. Le modèle
c. Le contrôleur
2. En pratique
a. Le contrôleur frontal
b. Le routage
c. Le contrôleur et le modèle
d. La vue
e. En synthèse
Architecture de Symfony
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
2. En pratique
En parcourant les documentations de plusieurs frameworks pour applications web, on
remarque différentes politiques quant à leur compatibilité avec ce modèle de
conception. Tandis que certains s’en targuent fièrement en page d’accueil, d’autres en
parlent très peu, voire ne l’évoquent pas du tout. En réalité, la quasi-totalité le sont.
En effet, ce modèle de conception désigne trois couches assez abstraites. Il n’est ni un
choix architectural fort, ni un qualificatif suffisant pour désigner l’organisation d’un
projet. C’est le minimum qu’un framework se doit de proposer (même si ce modèle de
conception MVC n’est pas obligatoire ou configuré par défaut).
Examinons maintenant le fonctionnement d’un framework MVC au travers d’un
exemple, une requête HTTP donnée :
GET /articles/introduction.html
Par défaut, en recevant cette requête, un serveur web essayerait de mettre en relation
l’URL avec le système de fichiers du serveur. Apache par exemple, irait chercher le
fichier DocumentRoot/articles/introduction.html.
DocumentRoot est le chemin vers le dossier public de l’application, c’est-à-dire celui
dont les fichiers sont accessibles par les utilisateurs.
Ce comportement implique la création d’un fichier pour chaque URL du site, qui
devra contenir le même code nécessaire au démarrage de l’application.
Ceci est incompatible avec le fonctionnement des frameworks, car ils embrassent le
principe du Don’t Repeat Yourself (en français : « ne pas se répéter »).
Répéter le même code sur tous les fichiers complique la maintenance du site web : si
une partie de ce code doit être modifiée, vous devrez mettre à jour beaucoup de
fichiers.
La solution à ce problème est d’utiliser un seul fichier pour gérer les échanges entre le
serveur web et le framework, qui agira en tant qu’intermédiaire entre ces deux
entités : le contrôleur frontal.
a. Le contrôleur frontal
C’est le point d’entrée de l’application ; il contient le code nécessaire à
l’amorçage (bootstrap) de l’application.
Notre requête a donné lieu à l’interprétation de ce fichier et non
de DocumentRoot/articles/introduction.html.
Ceci est rendu possible par les modules de réécriture d’URL proposés par les serveurs
web. Ils sont capables d’analyser la requête HTTP entrante et de la réécrire à la volée
pour qu’elle soit orientée vers un autre fichier.
b. Le routage
Une fois l’application démarrée par le contrôleur frontal, le routage doit décider de
l’action à effectuer pour satisfaire la requête du client, à savoir une requête GET sur la
ressource /articles/introduction.html.
Ici, le routage décide qu’il faut exécuter l’action « voir » du contrôleur article. Cette
décision a été prise car des règles de routage ont préalablement été définies par le
développeur. Nous reviendrons sur ces dernières au cours du chapitre Routage et
contrôleur.
c. Le contrôleur et le modèle
Les actions sont des tâches. Elles sont regroupées par thèmes au sein de contrôleurs.
Concrètement, un contrôleur est une classe et chacune de ses méthodes publiques est
une action. Ici, l’utilisateur souhaite voir un article, c’est-à-dire exécuter
l’action voir du contrôleur Article.
« Exécuter l’action voir du contrôleur Article » se traduit en PHP par « invoquer la
méthode voir de la classe ArticleControleur ».
Voici ce à quoi pourrait ressembler ce contrôleur :
<?php
class ArticleControleur
{
public $modele;
public $vue;
e. En synthèse
Bien qu’il ne s’agisse ici que de « pseudo code » et en aucun cas du code Symfony, il
n’en illustre pas moins le principe du modèle MVC tel qu’il est mis en œuvre dans les
frameworks. Les frameworks PHP tels que Symfony, Zend Framework ou encore
Laravel, utilisent ce principe et seule la syntaxe d’appel diffère, le principe reste le
même.
Une bonne compréhension du principe de fonctionnement du modèle MVC est
aujourd’hui une compétence essentielle pour appréhender les frameworks de
développement, et pas seulement Symfony ou bien les autres frameworks PHP.
Précédent
Quiz
Suivant
Architecture de Symfony
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Le contrôleur frontal
3. Le Service Container
4. Le modèle MVC dans Symfony ?
5. L’approche par composant
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Architecture de Symfony
1. Présentation
Nous allons maintenant découvrir les principales entités du framework participant au
traitement d’une requête, et en découvrir deux qui sont spécifiques à Symfony :
le Kernel et le Service Container.
Avant de schématiser cette architecture, voici un bref descriptif de ces deux entités :
Le Kernel (noyau, en français) est le cœur du framework. C’est un composant
interne et il n’est pas obligatoire de connaître son existence pour développer sous
Symfony. Cependant, comprendre son fonctionnement et savoir écouter les
signaux qu’il émet est essentiel pour bénéficier pleinement des possibilités
offertes par le framework.
Le Service Container est un composant incontournable ; c’est une sorte de boîte
à outils, dans laquelle vous trouverez ce qu’on appelle des « services ». Ces
services sont divers et variés. Certains vous permettent de requêter des bases de
données, d’autres de « serializer » (linéariser) des objets. Vous aurez même le
droit de créer vos propres services.
2. Le contrôleur frontal
Il existe un troisième composant important : le contrôleur frontal.
Le contrôleur frontal n’est pas véritablement une spécificité de Symfony ; il
correspond à une approche standard du modèle MVC qui part du principe que tout le
traitement bas niveau des requêtes se fait par un seul et unique point d’entrée. Ce
contrôleur frontal est notamment chargé de transformer les requêtes et réponses HTTP
en objets PHP.
Avant d’approfondir, découvrons au travers d’un schéma le traitement d’une requête
dans une application Symfony :
Flux applicatifs entre une requête et une réponse dans une application Symfony.
Le Kernel reçoit la requête du client, il interroge le « router » dans le but de
savoir quel contrôleur il doit invoquer.
Une fois ce contrôleur invoqué, le Kernel attend de lui qu’il retourne une
réponse HTTP, peu importe la manière dont elle est générée.
De ce fait, le contrôleur est libre d’invoquer différents services, comme le modèle ou
la vue, mis à sa disposition au sein du Service Container. De ces
différentes interactions naît une réponse HTTP, qui est renvoyée au Kernel.
Finalement, le Kernel transmet cette réponse au client.
3. Le Service Container
Le Service Container contient des « services » ; sur le schéma précédent, on y aperçoit
par exemple les services « Modèle » ou « Vue ».
Sur le schéma, nous décrivons uniquement « Modèle » et « Vue », mais il existe bien
d’autres services ; certains sont présents par défaut, d’autres sont créés par le
développeur.
Les services ne sont pas des concepts abstraits : chaque service est un objet PHP.
Le Service Container est la seule entité mise à disposition du contrôleur pour l’aider à
générer sa requête HTTP, c’est donc un élément central de l’application.
En installant Symfony, nous nous rendons compte qu’il est préconfiguré avec un
certain nombre de services, chacun répondant à un besoin particulier (gérer les
templates, communiquer avec des bases de données, etc.).
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Fonctionnement de Symfony Flex
3. Les recettes Flex
a. Un exemple concret
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Symfony Flex
1. Présentation
Symfony Flex représente la nouvelle approche pour installer des dépendances et donc
des fonctionnalités supplémentaires dans un projet Symfony. Il est introduit avec la
version 3.3 de Symfony mais il requiert cependant la structure d’un projet Symfony 4,
c’est donc dans cette version qu’il est pleinement exploitable.
Symfony Flex rend obsolète l’ancien système de « bundle », jugé finalement trop
contraignant pour installer de nouvelles dépendances. Dans cet ancien système, en
plus de devoir installer une nouvelle dépendance, il était souvent nécessaire d’agir
manuellement sur la configuration d’un nouveau bundle ; avec Symfony Flex, tout est
pris en charge.
L’idée fondamentale qui fait la différence avec les bundles, c’est que Flex ajoute une
configuration par défaut immédiatement opérationnelle en plus de la dépendance, et,
dans la majorité des cas, cette configuration n’a pas besoin d’être modifiée.
Toutes les tâches que Flex doit exécuter sont décrites dans un fichier au format JSON
(JavaScript Object Notation) appelé Recette Flex.
Dans cette recette, on voit clairement la déclaration des alias pour cette dernière, ainsi
qu’une action copy-from-recipe qui consiste à copier des fichiers depuis la recette
vers le projet. Le fichier .php-cs-fixer.dist.php mentionné ici fait partie des fichiers
de la recette et sera copié dans le projet. Ainsi, en exécutant la commande :
composer require cs-fixer
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Principe et apports
2. Les fichiers de configuration
3. Dans le contexte HTTP
4. Dans le contexte CLI (Command Line Interface)
Les environnements
1. Principe et apports
Un projet de site ou d’application web dispose généralement de plusieurs copies. Il est
par exemple installé sur le serveur de production, sur les postes des différents
développeurs y participant ou sur des serveurs de test, de préproduction, de recette,
etc.
Ces différentes copies n’ont pas les mêmes attentes et il est important de pouvoir y
répondre. Elles n’utilisent pas les mêmes bases de données, ne nécessitent pas le
même type de débogage et possèdent potentiellement des configurations applicatives
différentes.
Les environnements sont une fonctionnalité majeure de Symfony. Ils permettent au
projet de réagir différemment selon le « mode » sous lequel vous choisissez de
l’exécuter.
Historiquement, Symfony définit les environnements prod, dev et test. Ils
correspondent à des noms prédéfinis depuis Symfony2, mais Symfony 4 permet de
choisir le nom souhaité pour un environnement. Le fichier .env, situé à la racine du
projet est la clé de ce système d’environnement et décrit, dans ses commentaires, un
ordre de chargement de fichiers d’environnement. Ce fichier contient également une
variable d’environnement APP_ENV (par défaut, elle est positionnée à dev)
permettant de spécifier l’environnement actif dans le projet.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le standard PSR-4
2. Mécanismes alternatifs
3. Application aux applications Symfony
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Le chargement automatique de
classes
Le chargement automatique des classes (ou autoload) est un concept clé de Symfony.
L’organisation d’un projet Symfony est massivement orientée objet : hormis les
contrôleurs frontaux et la console, les fichiers PHP contenant du code procédural sont
quasi-inexistants. En pratique, chaque fichier PHP d’un projet contient une classe et
ce fichier est placé à un endroit précis, de manière à faciliter le chargement de la
classe qu’il contient.
Cette organisation n’est pas spécifique à Symfony, mais à Composer. Composer est
capable d’autocharger toutes vos classes, grâce à la configuration de la
section autoload du fichier composer.json.
1. Le standard PSR-4
Par défaut, dans un projet Symfony, la section autoload du
fichier composer.json contient la configuration suivante :
{
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
2. Mécanismes alternatifs
Au-delà de la norme PSR-4, Composer prend également en charge d’autres
mécanismes d’autochargement des classes, comme classmap ou files.
Cependant, ces derniers sont souvent utilisés pour des projets ne respectant pas la
norme PSR-4, ce que nous vous déconseillons fortement. Pour vos projets, un
autoload de type PSR-4 devrait être capable de charger toutes vos classes.
Si vous souhaitez tout de même vous documenter sur les mécanismes
d’autoload alternatifs, nous vous renvoyons vers la page
suivante : http://getcomposer.org/doc/04-schema.md#autoload
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Les commandes
a. Lister les commandes disponibles
b. Exécuter une commande
3. Les options
4. Les arguments
5. L’aide sur les commandes
6. Exécuter rapidement des commandes
La console
1. Présentation
La console est l’outil permettant d’interagir avec une application Symfony en ligne de
commandes (CLI ou Command Line Interface).
Contrairement à l’interface web, accessible au travers du contrôleur frontal, la console
est un outil du développeur et n’est en aucun cas destinée à être utilisée par les
utilisateurs de l’application.
La console est située dans le dossier bin et elle correspond au fichier console. C’est
un fichier PHP, la console peut donc être exécutée de cette manière (dans un
interpréteur de commandes) :
php bin/console
Dans ce cas-là, assurez-vous d’avoir, en plus des droits de lecture, les droits
d’exécution sur ce fichier.
2. Les commandes
La console est utilisée pour lancer des commandes. C’est son seul et unique usage.
Chaque commande est chargée d’effectuer une tâche donnée. Ces tâches sont par
exemple le vidage du cache, l’assistance au développeur en
générant automatiquement certains fichiers pour lui, en mettant à jour la base de
données, ou en débogant certaines parties de l’application.
Les commandes disponibles sont trop nombreuses pour pouvoir être listées et décrites
ici, mais vous pouvez facilement obtenir des informations sur celles-ci (cf. section
Lister les commandes disponibles ci-après et section L’aide sur les commandes, plus
loin dans ce chapitre).
a. Lister les commandes disponibles
La console, exécutée sans argument, ou avec l’argument list, affiche la liste des
commandes disponibles :
php bin/console list
3. Les options
L’exécution d’une commande peut être complétée par des options. Celles-ci sont
utilisées pour modifier le comportement de la commande. Elles sont préfixées par
deux tirets (« -- ») et peuvent être placées n’importe où dans l’instruction (mais après
« bin/console »).
Illustrons les options avec un exemple : Symfony utilise un répertoire de cache
différent pour chaque environnement (dev, prod, etc.). Lorsque vous exécutez la
commande cache:clear sans aucun argument, c’est le cache de
l’environnement dev qui est vidé. L’environnement peut être modifié grâce à
l’option --env :
php bin/console cache:clear --env="prod"
Toutes les instructions ci-dessus sont équivalentes. Comme vous pouvez le constater,
les options peuvent être placées à n’importe quel endroit après bin/console et sont
assez souples sur la syntaxe, certaines disposant même de raccourcis (ici, -e). Un
raccourci d’option ne comporte qu’une seule lettre et est préfixé d’un seul tiret.
Certaines options sont globales tandis que d’autres sont spécifiques à une commande.
4. Les arguments
Les arguments de commande permettent de modifier le comportement d’une
commande. Contrairement aux options, les arguments ne sont pas nommés ; ils sont
identifiés par leur position. Ils doivent être placés après le nom de la commande :
php bin/console commande argument_1 argument_2
Les options peuvent tout à fait être utilisées conjointement aux arguments :
php bin/console -e prod commande --option_1 argument_1 argument_2
--option_2=valeur
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Fonctionnement de Symfony Flex
3. Les recettes Flex
a. Un exemple concret
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Symfony Flex
1. Présentation
Symfony Flex représente la nouvelle approche pour installer des dépendances et donc
des fonctionnalités supplémentaires dans un projet Symfony. Il est introduit avec la
version 3.3 de Symfony mais il requiert cependant la structure d’un projet Symfony 4,
c’est donc dans cette version qu’il est pleinement exploitable.
Symfony Flex rend obsolète l’ancien système de « bundle », jugé finalement trop
contraignant pour installer de nouvelles dépendances. Dans cet ancien système, en
plus de devoir installer une nouvelle dépendance, il était souvent nécessaire d’agir
manuellement sur la configuration d’un nouveau bundle ; avec Symfony Flex, tout est
pris en charge.
L’idée fondamentale qui fait la différence avec les bundles, c’est que Flex ajoute une
configuration par défaut immédiatement opérationnelle en plus de la dépendance, et,
dans la majorité des cas, cette configuration n’a pas besoin d’être modifiée.
Toutes les tâches que Flex doit exécuter sont décrites dans un fichier au format JSON
(JavaScript Object Notation) appelé Recette Flex.
Dans cette recette, on voit clairement la déclaration des alias pour cette dernière, ainsi
qu’une action copy-from-recipe qui consiste à copier des fichiers depuis la recette
vers le projet. Le fichier .php-cs-fixer.dist.php mentionné ici fait partie des fichiers
de la recette et sera copié dans le projet. Ainsi, en exécutant la commande :
composer require cs-fixer
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Principe et apports
2. Les fichiers de configuration
3. Dans le contexte HTTP
4. Dans le contexte CLI (Command Line Interface)
Les environnements
1. Principe et apports
Un projet de site ou d’application web dispose généralement de plusieurs copies. Il est
par exemple installé sur le serveur de production, sur les postes des différents
développeurs y participant ou sur des serveurs de test, de préproduction, de recette,
etc.
Ces différentes copies n’ont pas les mêmes attentes et il est important de pouvoir y
répondre. Elles n’utilisent pas les mêmes bases de données, ne nécessitent pas le
même type de débogage et possèdent potentiellement des configurations applicatives
différentes.
Les environnements sont une fonctionnalité majeure de Symfony. Ils permettent au
projet de réagir différemment selon le « mode » sous lequel vous choisissez de
l’exécuter.
Historiquement, Symfony définit les environnements prod, dev et test. Ils
correspondent à des noms prédéfinis depuis Symfony2, mais Symfony 4 permet de
choisir le nom souhaité pour un environnement. Le fichier .env, situé à la racine du
projet est la clé de ce système d’environnement et décrit, dans ses commentaires, un
ordre de chargement de fichiers d’environnement. Ce fichier contient également une
variable d’environnement APP_ENV (par défaut, elle est positionnée à dev)
permettant de spécifier l’environnement actif dans le projet.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le standard PSR-4
2. Mécanismes alternatifs
3. Application aux applications Symfony
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Le chargement automatique de
classes
Le chargement automatique des classes (ou autoload) est un concept clé de Symfony.
L’organisation d’un projet Symfony est massivement orientée objet : hormis les
contrôleurs frontaux et la console, les fichiers PHP contenant du code procédural sont
quasi-inexistants. En pratique, chaque fichier PHP d’un projet contient une classe et
ce fichier est placé à un endroit précis, de manière à faciliter le chargement de la
classe qu’il contient.
Cette organisation n’est pas spécifique à Symfony, mais à Composer. Composer est
capable d’autocharger toutes vos classes, grâce à la configuration de la
section autoload du fichier composer.json.
1. Le standard PSR-4
Par défaut, dans un projet Symfony, la section autoload du
fichier composer.json contient la configuration suivante :
{
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
2. Mécanismes alternatifs
Au-delà de la norme PSR-4, Composer prend également en charge d’autres
mécanismes d’autochargement des classes, comme classmap ou files.
Cependant, ces derniers sont souvent utilisés pour des projets ne respectant pas la
norme PSR-4, ce que nous vous déconseillons fortement. Pour vos projets, un
autoload de type PSR-4 devrait être capable de charger toutes vos classes.
Si vous souhaitez tout de même vous documenter sur les mécanismes
d’autoload alternatifs, nous vous renvoyons vers la page
suivante : http://getcomposer.org/doc/04-schema.md#autoload
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Les commandes
a. Lister les commandes disponibles
b. Exécuter une commande
3. Les options
4. Les arguments
5. L’aide sur les commandes
6. Exécuter rapidement des commandes
La console
1. Présentation
La console est l’outil permettant d’interagir avec une application Symfony en ligne de
commandes (CLI ou Command Line Interface).
Contrairement à l’interface web, accessible au travers du contrôleur frontal, la console
est un outil du développeur et n’est en aucun cas destinée à être utilisée par les
utilisateurs de l’application.
La console est située dans le dossier bin et elle correspond au fichier console. C’est
un fichier PHP, la console peut donc être exécutée de cette manière (dans un
interpréteur de commandes) :
php bin/console
Dans ce cas-là, assurez-vous d’avoir, en plus des droits de lecture, les droits
d’exécution sur ce fichier.
2. Les commandes
La console est utilisée pour lancer des commandes. C’est son seul et unique usage.
Chaque commande est chargée d’effectuer une tâche donnée. Ces tâches sont par
exemple le vidage du cache, l’assistance au développeur en
générant automatiquement certains fichiers pour lui, en mettant à jour la base de
données, ou en débogant certaines parties de l’application.
Les commandes disponibles sont trop nombreuses pour pouvoir être listées et décrites
ici, mais vous pouvez facilement obtenir des informations sur celles-ci (cf. section
Lister les commandes disponibles ci-après et section L’aide sur les commandes, plus
loin dans ce chapitre).
a. Lister les commandes disponibles
La console, exécutée sans argument, ou avec l’argument list, affiche la liste des
commandes disponibles :
php bin/console list
Pour une meilleure organisation, certaines commandes sont réparties en espaces de
noms. À titre d’exemple, les commandes liées à la base de données sont préfixées par
« doctrine ».
b. Exécuter une commande
Pour exécuter une commande, il suffit de passer son nom en tant
qu’argument, comme nous l’avons fait précédemment avec list. L’exemple ci-dessous
exécute la commande cache:clear, chargée de vider le cache de Symfony :
php bin/console cache:clear
3. Les options
L’exécution d’une commande peut être complétée par des options. Celles-ci sont
utilisées pour modifier le comportement de la commande. Elles sont préfixées par
deux tirets (« -- ») et peuvent être placées n’importe où dans l’instruction (mais après
« bin/console »).
Illustrons les options avec un exemple : Symfony utilise un répertoire de cache
différent pour chaque environnement (dev, prod, etc.). Lorsque vous exécutez la
commande cache:clear sans aucun argument, c’est le cache de
l’environnement dev qui est vidé. L’environnement peut être modifié grâce à
l’option --env :
php bin/console cache:clear --env="prod"
Toutes les instructions ci-dessus sont équivalentes. Comme vous pouvez le constater,
les options peuvent être placées à n’importe quel endroit après bin/console et sont
assez souples sur la syntaxe, certaines disposant même de raccourcis (ici, -e). Un
raccourci d’option ne comporte qu’une seule lettre et est préfixé d’un seul tiret.
Certaines options sont globales tandis que d’autres sont spécifiques à une commande.
4. Les arguments
Les arguments de commande permettent de modifier le comportement d’une
commande. Contrairement aux options, les arguments ne sont pas nommés ; ils sont
identifiés par leur position. Ils doivent être placés après le nom de la commande :
php bin/console commande argument_1 argument_2
Les options peuvent tout à fait être utilisées conjointement aux arguments :
php bin/console -e prod commande --option_1 argument_1 argument_2
--option_2=valeur
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le profiler Symfony
2. La fonction dump()
Quiz
Routage et contrôleur
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Le menu de navigation situé sur la gauche permet d’inspecter les autres informations
associées à la requête, comme les données de formulaire, le routage ou bien les
statistiques de performance.
En phase de développement tout comme en phase de mise au point, le profiler
Symfony est un outil essentiel.
2. La fonction dump()
Dans le processus de développement, l’une des principales difficultés est de pouvoir
connaître l’état des données lors de l’invocation d’une requête sur l’application. Il est
toujours possible d’utiliser un débogueur sur son application, mais une alternative
intéressante est par exemple d’afficher dans la réponse la structure de données
obtenue par l’appel d’une fonctionnalité métier.
La fonction dump() de Symfony permet d’afficher dans le profiler la structure, et ce
récursivement, d’un objet PHP. Par exemple, si dans un contrôleur vous invoquez une
fonctionnalité renvoyant un tableau de données ou bien un objet, le passer à la
fonction dump() vous permet d’en consulter le contenu, et ce même si une erreur est
générée.
Par exemple, si la variable $listeDesArticles contient un tableau d’objets PHP, on
peut visualiser la structure complète du tableau et des objets en utilisant la
fonction dump($listeDesArticles) dans le code du contrôleur comme ceci :
public function index(ArticleService $articleService): Response
{
$listeDesArticles = $articleService->rechercherTousLesArticles();
dump($listeDesArticles);
...
}
La fonction dump() est donc un très bon complément au débogueur présent dans
votre environnement de développement intégré, et un très bon substitutif à la
fonction var_dump() de PHP.
Précédent
La console
Suivant
Quiz
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Définition
2. Le répertoire public et le contrôleur frontal
3. Une requête, une action
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
kernel:
resource: ../../src/Kernel.php
type: annotation
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
kernel:
resource: ../../src/Kernel.php
type: annotation
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Configurer le path
1. Illustration par l’exemple : /hello/world
La règle de routage la plus importante est le path ; elle correspond à la variable
superglobale $_SERVER[’PATH_INFO’].
Configurons une route pour un path donné :/hello/world.
Annotations
Selon les définitions du chapitre Architecture du framework - Le modèle de
conception MVC, les actions sont des méthodes de classe dites « contrôleur » et c’est
donc au-dessus des actions que nous devons placer notre règle de routage sous forme
d’annotation :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Ici, l’action hello sera exécutée pour toute requête dont le path est /hello/world, cette
règle de routage étant définie grâce à l’annotation @Route.
Pour que le routage du contrôleur soit effectif, il faut qu’il soit activé dans le
fichier config/routes/annotations.yaml. Pour nous faciliter la tâche,
Symfony autorise la configuration de tous les contrôleurs d’une application en
référençant un dossier en tant que ressource ; c’est d’ailleurs la configuration par
défaut présente dans ce fichier :
controllers:
resource: ../../src/Controller/
type: annotation
Le nom des routes est un élément primordial. Pour générer des URL, c’est au travers
du nom que nous référençons une route.
YAML
Les routes peuvent également être définies directement depuis le
fichier config/routes.yaml :
ma_route:
path: /hello/world
controller: App\Controller\WelcomeController::hello
Ici, une route nommée ma_route est définie. Elle est l’équivalent en YAML de la
route configurée en annotation dans la section précédente et
son path est /hello/world.
À la différence des annotations, où l’action à exécuter pour la route est implicite (c’est
la méthode en dessous de l’annotation), en YAML, il faut la référencer. Cela se fait
via la section controller.
XML
Pour utiliser le format XML, il faut créer un fichier config/routes.xml. Voilà la
définition de notre même route :
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd"
>
<route id="ma_route"
path="/hello/world"
controller="App\Controller\WelcomeController::hello />
</routes>
Par rapport au YAML, le fichier est certes moins lisible, mais le format XML a pour
avantage d’être plus structuré, grâce notamment au schéma de validation XSD (XML
Schema Definition) mis à notre disposition. C’est également un langage historique et
toujours massivement utilisé dans l’univers informatique.
PHP
Une dernière possibilité pour configurer ses routes est l’utilisation de PHP. Pour cela,
un fichier config/routes.php est nécessaire et voici, pour la même route que
précédemment, sa structure :
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use App\Controller\WelcomeController;
2. La notation du contrôleur
Pour la plupart des formats de configuration (à l’exception des annotations), il faut
spécifier un paramètre controller. Son nom peut être assez déstabilisant car il ne
correspond pas à un contrôleur uniquement, mais à l’action d’un contrôleur qu’il
faudra exécuter pour une route donnée.
Ce paramètre doit être défini selon une convention de codage spécifique à Symfony, à
savoir :
Espace_De_Noms\Nom_De_La_Classe_De_Contrôleur::action
À titre d’exemple, App\Controller\AccueilController::index correspond à la
méthode index() du contrôleur AccueilController, de l’espace de noms App\
Controller. Concrètement, le fichier en question serait :
src/Controller/AccueilController.php
Les noms des sections ont peu d’importance (app_admin) ; il faut simplement veiller
à ce qu’ils soient uniques au sein de chaque fichier.
XML
<!-- Dans le fichier config/routes.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd"
>
PHP
// Dans le fichier config/routes.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd"
>
<import
resource="admin-routes.xml"
prefix="/ma-section"
/>
</routes>
PHP
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
Annotations
Avec les annotations, une fonctionnalité supplémentaire est disponible : le préfixe
peut être ajouté au niveau du contrôleur, au-dessus de la classe.
/**
* @Route("/admin")
*/
class WelcomeAdminController extends AbstractController
{
/**
* @Route("/{name}")
*/
public function hello($name)
{
return new Response('Hello '.$name.'!');
}
}
Ici, toutes les actions créées auront par défaut le préfixe /admin.
6. Les paramètres de substitution des routes
Nous savons comment configurer un path statique (par exemple /hello/world) mais
cela se révèle cependant assez limité : les applications web sont dynamiques et leurs
URL sont donc amenées à transmettre des données aux applications.
Il serait beaucoup trop fastidieux de configurer manuellement toutes les
possibilités. Dans le cas d’une URL contenant un identifiant correspondant à une
entrée en base de données, il faudrait rajouter une route à chaque insertion en base !
Heureusement, le framework permet la gestion de ce type de routes, notamment grâce
aux paramètres de substitution.
Nous allons repartir de notre path précédent (/hello/world) et y placer un
paramètre de substitution dynamique :
Annotations
class WelcomeController extends AbstractController
{
/**
* @Route("/hello/{name}")
*/
public function hello($name)
{
return new Response('Hello '.$name.'!');
}
}
Ici, l’action hello est exécutée pour le path/hello/world. Le résultat est a priori le
même que pour l’exemple précédent : une page dont le contenu est « Hello world! ».
Cependant, ce path est dorénavant une des infinies possibilités pour cette route.
Ici, world peut être remplacé par n’importe quel autre
nom : /hello/sam ou /hello/bruno sont autant d’autres paths qui satisfont cette règle
de routage.
Cela est rendu possible grâce au paramètre de substitution {name}, qui rend la règle
de routage dynamique. Cette route pourrait être comparée à l’expression régulière
(Regex) ^/hello/(.+)$.
Le paramètre de substitution (name) est ensuite disponible depuis l’action. La
manière la plus simple pour le récupérer est de l’indiquer en paramètre de la méthode
correspondant à l’action. C’est un « paramètre magique », nous en parlons plus loin
dans ce chapitre.
En réalité, la Regex utilisée pour un paramètre de substitution n’est pas (.+) mais ([^/]
+). En effet, Symfony considère le slash (/) comme un caractère spécial : il agit en
tant que délimiteur entre les différents paramètres. Nous utilisons (.+) par souci de
simplicité pour le lecteur, il est toutefois important de noter qu’un paramètre de
substitution ne peut pas comporter de slash. C’est le comportement par défaut du
framework, cela permet d’éviter les conflits entre différentes routes.
YAML
ma_route:
path: /hello/{name}
controller: App\Controller\WelcomeController::hello
XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd"
>
<route id="ma_route"
path="/hello/{name}"
controller="App\Controller\WelcomeController::hello" />
</routes>
PHP
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use App\Controller\WelcomeController;
XML
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd"
>
<route id="ma_route"
path="/hello/{name}"
controller="App\Controller\WelcomeController::hello"
methods="GET"
>
<requirement key="name">sam|bruno</requirement>
</route>
</routes>
PHP
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use App\Controller\WelcomeController;
Annotations
class WelcomeController extends AbstractController
{
/**
* @Route(
* "/hello/{name}",
* requirements={"name"="sam|bruno"},
* methods="{GET}"
* )
*/
public function hello($name)
{
return new Response('Hello '.$name.'!');
}
}
Précédent
Définition des routes
Suivant
Routage par nom de domaine
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Prérequis
2. Exemple de mise en œuvre
3. Explications
Le contrôleur
Quiz
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
ma_route:
path: /bonjour/{name}
host: {_locale}.mon-projet.local
controller: App\Controller\WelcomeController::hello
requirements:
_locale: fr
my_route:
path: /hello/{name}
host: {_locale}.mon-projet.local
controller: App\Controller\WelcomeController::hello
requirements:
_locale: en
XML
<!-- Dans le fichier config/routes.xml -->
<route id="my_route"
path="/hello/{name}"
host="{_locale}.mon-projet.local"
controller="App\Controller\WelcomeController:hello">
<requirement key="_locale">en</requirement>
</route>
</routes>
PHP
<?php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use App\Controller\WelcomeController;
$routes->add('my_route', '/hello/{name}')
->controller([WelcomeController::class, 'hello'])
->host('{_locale}.mon-projet.local')
->requirements(['_locale' => 'en'])
;
};
Annotations
class WelcomeController extends AbstractController
{
/**
* @Route(
* "/hello/{name}",
* host="en.mon-projet.local",
* name="my_route"
* )
* @Route(
* "/bonjour/{name}",
* host="fr.mon-projet.local",
* name="ma_route"
* )
*/
public function hello($name)
{
return new Response('Hello '.$name.'!');
}
}
3. Explications
L’exemple précédent définit deux routes dont le sous-domaine et le path sont
différents. Ces routes correspondraient aux URL suivantes :
http://fr.mon-projet/bonjour/adam
http://en.mon-projet/hello/adam
Dans la définition de ces routes, un sous-domaine est utilisé pour la langue et la même
action peut être invoquée via une URL en français ou en anglais.
Avant de continuer, nous tenons à préciser que cet exemple existe uniquement à titre
pédagogique, pour illustrer le concept de routage par sous-domaine. Si vous souhaitez
utiliser des URL en plusieurs langues, reportez-vous à l’annexe Les traductions du
présent ouvrage.
Si un host est renseigné, ce n’est plus seulement le path de la requête qui est comparé
avec l’expression régulière de la route, mais aussi le nom de domaine. L’URL
http://en.mon-projet/bonjour/toto renverrait donc une page d’erreur 404, car le path
commençant par/bonjour est associé au sous-domaine fr et non en.
Vous aurez peut-être remarqué une limitation dans l’exemple précédent : nous
utilisons le nom de domaine mon-projet.local, la route ne serait donc pas trouvée sur
le site de production. En effet, ce dernier a forcément un nom de domaine différent de
celui du site de développement.
Pour régler ce problème, vous devez utiliser un paramètre dynamique mais nous ne
nous attarderons pas sur ce sujet, car il est relatif au composant d’injection de
dépendances (Dependency Injection), auquel un chapitre est déjà consacré.
Précédent
Configurer le path
Suivant
Le contrôleur
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
L’injection de dépendances
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Le contrôleur
Les contrôleurs sont des éléments centraux car ils contiennent la logique de votre
application. Ils disposent de plusieurs fonctionnalités, mises à leur disposition par
différents services.
Vous pouvez apercevoir ces fonctionnalités en parcourant la classe Symfony\Bundle\
FrameworkBundle\Controller\AbstractController, dont vos contrôleurs héritent.
Chaque méthode fait appel à un service du Service Container, ces méthodes
correspondant à des raccourcis.
Cette méthode est aussi bien capable de récupérer les services du framework que vos
services personnalisés. Pour plus d’informations sur les services, reportez-vous au
chapitre L’injection de dépendances.
3. Utiliser les paramètres de substitution
Jusqu’à présent, vos actions étaient des méthodes sans arguments. Sachez que
Symfony dispose d’une fonctionnalité de « paramètres magiques ».
Si un paramètre possède un nom ou un typage d’objet particulier, Symfony sera
capable d’injecter une certaine valeur (ou objet) lors de l’invocation de l’action. Ainsi,
le code de l’action se retrouve allégé.
a. Paramètres de substitution des routes
Nous avons déjà utilisé cette fonctionnalité précédemment : lorsqu’une route
possédait un paramètre de substitution, nous le placions en paramètre de l’action pour
le récupérer à la volée :
/**
* @Route("/hello/{name}")
*/
public function hello($name)
{
return new Response('Hello '.$name.'!');
}
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Annotation\Template;
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Annotation\Template;
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Annotation\Template;
Des paramètres libres (non présents dans la définition de la route) peuvent être
ajoutés. Ils seront donc ajoutés en fin d’URL (par exemple /hello/pierre?
param_2=valeur2).
Vous devrez toujours utiliser la génération d’URL plutôt qu’écrire vos URL
manuellement. Ainsi, lorsque vous modifiez le path d’une de vos routes, vous ne
prenez pas le risque de casser certaines URL en dur.
Par défaut, le code HTTP de la redirection est 302, mais ce code est modifiable via le
second paramètre de la méthode redirect().
public function index()
{
return $this->redirect($this->generateUrl('ma_route'), 301);
}
6. La délégation de requête
Il existe une alternative à la redirection HTTP : la délégation de requête. Il s’agit
d’exécuter une seconde action directement depuis le contrôleur courant.
La principale différence avec la redirection HTTP est que la redirection interne est
invisible pour le client (navigateur).
Tandis que la redirection HTTP nécessite le renvoi d’une réponse spécifique (de code
30X), qui génère à son tour une seconde requête, la redirection interne est gérée par
Symfony ; la réponse renvoyée au client correspond à celle de la seconde action.
public function index()
{
$response = $this->forward(
'App\Controller\WelcomeController::hello',
[
'name' => 'bobby',
]
);
return $response;
}
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Pour renvoyer une page d’erreur, il faut retourner l’exception correspondante. Ici, la
méthode createNotFoundException() n’est qu’un raccourci retournant une instance
de Symfony\Component\HttpKernel\Exception\NotFoundHttpException. Le
code suivant serait équivalent :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpKernel\Exception\HttpException;
if (!$article) {
throw new HttpException(
404,
"Cet article n'existe pas."
);
}
// ...
}
}
Ce raccourci de contrôleur existe car le code d’erreur 404 est très utilisé dans une
application web, mais pour les autres codes d’erreurs, vous devez instancier et lancer
une Symfony\Component\HttpKernel\Exception\HttpException vous-même. Le
premier argument est le code de l’erreur, le second est un message d’erreur.
b. La vue
Lorsque vous lancez des exceptions, vous remarquez une page avec un
fantôme. N’ayez crainte, cette dernière n’est pas destinée aux utilisateurs finaux mais
affichée seulement lors du développement, car elle comporte de nombreuses
informations sur l’erreur, dont une trace des méthodes invoquées avant l’incident.
Une page d’erreur lors du développement.
Il est néanmoins essentiel pour vous de pouvoir outrepasser cette page pour accéder à
la page d’erreur de production.
La technique la plus simple consiste à utiliser l’application en environnement de
production ; pour cela, il faut modifier la valeur de la variable APP_ENV pour
qu’elle soit égale à prod. En cas d’erreur au rafraîchissement de la page, pensez à
vider le cache avec la commande php bin/console cache:clear -e prod.
Une fois l’environnement de production activé, vous accédez à la page d’erreur par
défaut de Symfony :
Ce type de page n’est probablement pas adapté à votre application mais il est
heureusement très simple à personnaliser.
Si vous n’êtes pas familier avec Twig et les templates, nous vous conseillons de lire le
chapitre Les templates avec Twig avant d’entamer la personnalisation de vos pages
d’erreurs.
Pour personnaliser vos pages d’erreurs, il vous faut tout d’abord installer
le TwigBridge dans le projet, à l’aide de la commande suivante :
composer require symfony/twig-pack
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
L’injection de dépendances
Le Service Container
Créer un service et configurer ses injections
Le chargement automatique de services
Créer des services réutilisables et distribuables
Quiz
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Principes de base
2. Les différentes techniques d’injection de dépendances
a. L’injection de dépendances par le constructeur
b. L’injection de dépendances par setter (mutateur)
c. L’injection de dépendances par propriété
3. Les avantages
Le Service Container
Créer un service et configurer ses injections
Le chargement automatique de services
Créer des services réutilisables et distribuables
Quiz
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
L’injection de dépendances
1. Principes de base
L’idée qui sous-tend l’injection de dépendances est simple : au lieu de laisser une
classe aller chercher elle-même les dépendances dont elle a besoin, ce processus est
externalisé.
La classe X, allégée de cette tâche, pourra se concentrer sur sa fonction première.
Découvrons ci-après les différents types d’injection.
class X
{
private $db;
// ...
}
class X
{
private $db;
//...
}
}
// ...
}
Ce type d’injection est plutôt conseillé pour les dépendances optionnelles, ou lorsque
l’on souhaite se donner la possibilité de changer l’implémentation d’une dépendance
obligatoire.
c. L’injection de dépendances par propriété
Ce type d’injection est moins recommandé, car il n’existe aucun moyen de
vérifier que le type de la dépendance injectée est correct. Nous vous la présentons tout
de même :
<?php
class X
{
public $db;
//...
}
}
// ...
}
3. Les avantages
Ces techniques d’injection de dépendances ont un principal avantage : la flexibilité.
La classe, ayant délégué cette tâche au contexte, gagne en portabilité. Elle peut
maintenant fonctionner sous une multitude de contextes, dans la mesure où ceux-ci
respectent le « contrat » stipulé au travers de son TypeHint.
Cela permet par exemple d’utiliser la classe X dans une autre application, avec en
dépendance une autre classe que MyPDO (mais respectant toujours ce contrat
« instance de PDO »), ou d’utiliser un « mock object » (objet bouchon) lors de ses
tests unitaires, pour les focaliser sur la logique métier de cette classe X. Plus
largement, avec l’injection de dépendances, les classes contiennent uniquement des
contraintes ou possibilités d’injection. Les injections en tant que telles sont faites à
l’exécution du code.
Heureusement, Symfony nous simplifie cette tâche. Nous allons pouvoir définir des
injections pour des objets de nos classes grâce à des fichiers de configuration et, à
l’exécution, nous pourrons accéder à ces objets prêts à l’emploi.
Précédent
Le modèle de conception IoC : Inversion Of Control
Suivant
Le Service Container
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Les services
2. Explications au travers d’un service X
Le Service Container
Nous avons évoqué cette entité lors du chapitre sur l’architecture du framework. On
sait que le Service Container contient des « services ».
Son rôle est d’absorber la complexité que nous venons d’identifier : définir la manière
d’instancier des classes passives, ne contenant que des règles et
possibilités d’injection. Cette définition est ce qu’on appelle un service.
Concrètement, le Service Container est une classe dont chacune des méthodes
retourne un service.
1. Les services
Un service est un objet PHP prêt à être utilisé et ayant une tâche générique et unique.
Il peut servir à « persister » des données en base, envoyer des e-mails ou écrire des
logs.
Un service est accessible au travers du Service Container et le framework Symfony
possède par défaut un certain nombre de services.
class ServiceContainer
{
private $services = array();
public function getX()
{
if (isset($this->services['x'])) {
return $this->services['x'];
}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Tout déplier | Tout replier
Informations générales
Introduction
Mise en place d’un projet Symfony
Architecture du framework
Le modèle de conception MVC
Architecture de Symfony
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
Fonctionnement du routage dans Symfony
Définition des routes
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Le modèle de conception IoC : Inversion Of Control
L’injection de dépendances
Le Service Container
1. Les services
2. Explications au travers d’un service X
Le Service Container
Nous avons évoqué cette entité lors du chapitre sur l’architecture du framework. On
sait que le Service Container contient des « services ».
Son rôle est d’absorber la complexité que nous venons d’identifier : définir la manière
d’instancier des classes passives, ne contenant que des règles et
possibilités d’injection. Cette définition est ce qu’on appelle un service.
Concrètement, le Service Container est une classe dont chacune des méthodes
retourne un service.
1. Les services
Un service est un objet PHP prêt à être utilisé et ayant une tâche générique et unique.
Il peut servir à « persister » des données en base, envoyer des e-mails ou écrire des
logs.
Un service est accessible au travers du Service Container et le framework Symfony
possède par défaut un certain nombre de services.
class ServiceContainer
{
private $services = array();
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Créer un service
2. Les différents types d’injections dans un service Symfony
a. Injection par constructeur
b. Injection par méthode
c. Injection par propriété
3. Injection automatique avec l’autowiring
4. Les services « lazy »
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
Cette configuration crée un service my_pdo, qui est une instance de PDO (clé class).
Les arguments passés au constructeur sont définis sous la
clé arguments. Concrètement, avec cette configuration, le service my_pdo est créé
de cette manière :
<?php
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
x:
class: X
arguments: @my_pdo
$myPDO = $serviceContainer->get('my_pdo');
Ici, le Service Container récupère le service my_pdo, puis le passe en argument lors
de l’instanciation de X. Le Service Container injecte un service nommé my_pdo (et
non la chaîne de caractères @my_pdo) car @ est un caractère spécial signifiant que
ce qui suit est le nom d’un service.
b. Injection par méthode
On peut également demander au Service Container d’invoquer une méthode de l’objet
après qu’il ait été créé, pour, par exemple, injecter une dépendance (un autre service) :
# config/services.yaml
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
x:
class: X
calls:
- [setDb, [@my_pdo]]
$myPDO = $serviceContainer->get('my_pdo');
$service->setDb($myPDO);
Une fois la classe instanciée, la méthode setDb est invoquée avec, en argument, le
service my_pdo (cf. L’injection de dépendances par setter (mutateur), précédemment
dans ce chapitre).
En ajoutant un point d’interrogation entre l’arobase et le nom du service, on peut
également rendre l’injection optionnelle : la dépendance n’est pas injectée si le service
correspondant n’existe pas. Si le service my_pdo n’existait pas, avec @?my_pdo, la
méthode ne serait pas invoquée du tout, tandis qu’avec @my_pdo, une exception
serait lancée.
c. Injection par propriété
Comme nous l’avons vu (cf. L’injection de dépendances par propriété, précédemment
dans ce chapitre), il est également possible de faire une injection par propriété, même
s’il est préférable d’utiliser le constructeur ou un mutateur :
# config/services.yaml
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
x:
class: X
properties:
db: @my_pdo
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
x:
class: X
autowire: true
Ici, nous utilisons la directive autowire plutôt que de spécifier les arguments
manuellement. Lors de la création du Service Container, Symfony analyse la
signature du constructeur de la classe X. Étant donné que cette dernière requiert un
argument de type PDO, un service pour cette classe est recherché et injecté : c’est le
service my_pdo.
Autowiring par setter (mutateur)
Pour utiliser l’autowiring avec des setters, il faut les passer explicitement :
# config/services.yaml
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pass']
x:
class: X
autowire: [ 'setDb' ]
services:
x:
class: X
autowire: [ 'set*' ]
Grâce à ce dernier, toutes les méthodes commençant par set seront analysées par le
système d’autowiring.
services:
my_pdo:
class: PDO
arguments: ['mysql:host=localhost', 'bilal', 'pa$S']
lazy: true
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. La configuration
2. Exemple d’utilisation
1. La configuration
Le principe est assez simple : toutes les classes que vous créez (à quelques
exceptions près) sont enregistrées en tant que service, et donc injectables les unes
dans les autres. Vous pouvez aussi injecter des services Symfony dans celles-ci. Ce
comportement est rendu possible par la configuration par défaut fournie dans le
fichier config/services.yaml, dont voici le contenu avec ladite configuration mise en
gras :
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in
# your services.
autoconfigure: true # Automatically registers your services as
# commands, event subscribers, etc.
Nous voyons que toute classe déclarée dans le répertoire src/ est déclarée en tant que
service dont l’identifiant est le FQCN (Fully-Qualified Class Name), le nom de classe
pleinement qualifié. Cependant certains sous-dossiers de src/ sont exclus car ils
contiennent des classes qui ne doivent pas être gérées par le Service Container ; c’est
le cas des entités Doctrine, par exemple.
Les dernières lignes de configuration de ce fichier servent quant à elles à
permettre l’injection de services dans les contrôleurs :
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
Les services seront donc injectables en paramètres des méthodes d’action des
contrôleurs.
2. Exemple d’utilisation
Prenons un service nommé ArticleService qui reçoit par injection le service Doctrine
de Symfony et qui est injecté en paramètre des actions d’un contrôleur
nommé ArticleController. L’organisation des composants pourrait être schématisée
comme ceci :
use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
class ArticleService
{
// Attribut qui référence l'EntityManager de Doctrine
private $em;
use App\Entity\Article;
use App\Entity\Auteur;
use App\Form\ArticleType;
use App\Model\ArticleService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le concept de bundle
a. Créer un bundle
b. Arborescence du bundle
2. La définition de services dans un bundle
3. La configuration
a. Définir une arborescence
b. Les différentes étapes du traitement de la configuration
c. Récupérer la configuration validée
4. Les « Compiler Passes »
a. Concept
b. Les tags
c. Le Compiler Pass
Quiz
Les templates avec Twig
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
use Symfony\Component\HttpKernel\Bundle\Bundle;
b. Arborescence du bundle
Une fois le bundle créé, il est nécessaire d’ajouter des répertoires pour en organiser le
code. Voici la structure type recommandée (bien que certains répertoires puissent
s’avérer inutiles en fonction des fonctionnalités que vous développez).
Controller/ : contient les contrôleurs du bundle.
DependencyInjection/ : contient des classes d’injection de dépendances ; nous y
reviendrons justement dans la suite de cette section.
Resources/config/ : contient la configuration du bundle, par exemple un
fichier routing.yaml si le routage de ce bundle est déclaré au format YAML.
Resources/views/ : contient les templates Twig.
Tests/ : contient les classes de test du bundle.
services:
mon_service:
class: src/MonEntreprise/AdminBundle/MaClasse
3. La configuration
La spécificité d’une extension de container est sa portabilité, notamment grâce à un
système de configuration. La configuration permet de modifier dynamiquement les
services définis par un bundle, de façon beaucoup plus structurée et conventionnelle
qu’avec des paramètres de container seuls.
Reprenons le service my_pdo de notre exemple précédent pour mieux saisir l’intérêt
de la configuration :
# src/MonEntreprise/AdminBundle/Resources/config/services.yaml
services:
my_pdo:
class: PDO
arguments:
- '%dbhost%'
- '%dbuser%'
- '%dbpass%'
La classe PDO est instanciée avec trois arguments : le DSN (Data Source Name),
l’utilisateur et le mot de passe utilisés pour la connexion avec la base de données.
Tout d’abord, comme nous l’avons précédemment expliqué dans ce chapitre, il est
fortement déconseillé de placer des informations sensibles (comme un mot de passe)
au sein du fichier de configuration.
Les fichiers du répertoire config/packages ont été conçus dans cette optique. Voici, à
titre d’exemple, la structure du fichier exploité par le code précédent :
// config/packages/admin_bundle.yaml
admin_bundle:
dbhost: localhost
dbuser: dbusername
dbpass: dbpassword
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
$rootNode
->children()
->scalarNode('dbhost')->end()
->scalarNode('dbuser')->end()
->scalarNode('dbpass')->end()
->end()
;
return $treeBuilder;
}
}
Ici, nous définissons plusieurs nœuds : dbhost, dbuser, etc. Ces paramètres sont
récupérés depuis la configuration de l’application
dans config/packages/admin_bundle.yaml.
Les valeurs définies dans le fichier sont récupérables depuis la classe de
l’extension du bundle (MonEntrepriseAdminExtension dans notre cas). Cette classe
est dans le dossier DependencyInjection du bundle :
namespace MonEntreprise\AdminBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
$rootNode
->children()
->booleanNode('mon_booleen')->end()
->integerNode('mon_nombre')->end()
->arrayNode('mon_tableau')->end()
->variableNode('ma_variable')->end()
->end()
;
La différence entre les nœuds de type variable et scalar est que le nœud de
type variable accepte également les tableaux.
Valeurs requises et valeurs par défaut
Par défaut, aucun nœud n’est requis. Le fait de ne renseigner aucune configuration
pour un bundle n’entraînera pas d’erreur. Pour rendre un nœud obligatoire, il faut
invoquer la méthode isRequired() après sa déclaration :
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('admin_bundle');
$rootNode
->children()
->scalarNode('driver')->isRequired()->end()
->scalarNode('host')->defaultValue('localhost')->end()
->scalarNode('user')->end()
->scalarNode('pass')->end()
->scalarNode('path')->end()
->end()
;
Ici, le nœud driver est obligatoire, tandis que le nœud host vaut par défaut localhost.
Validation
La méthode isRequired() vue précédemment a pour conséquence de créer une règle
de validation : si aucune valeur n’est saisie, une exception sera lancée.
Cependant, si vous souhaitez utiliser des règles de validation plus complexes, vous
pouvez les définir manuellement. Une section de validation peut être ouverte avec la
méthode validate() :
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('admin_bundle');
$rootNode
->children()
->scalarNode('driver')
->isRequired()
->validate()
->ifNotInArray(array('mysql', 'sqlite'))
->thenInvalid('Pilote %s non supporté.')
->end()
->end()
->end()
;
namespace MonEntreprise\AdminBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
$rootNode
->children()
->scalarNode('driver')
->isRequired()
->validate()
->ifNotInArray(array('mysql', 'sqlite'))
->thenInvalid('Seuls les pilotes MySQL et SQLite
sont pris en charge.')
->end()
->end()
->scalarNode('host')->defaultValue('localhost')->end()
->scalarNode('user')->end()
->scalarNode('pass')->end()
->scalarNode('path')->end()
->end()
->validate()
->ifTrue(function($v) {
if ('mysql' == $v['driver'] && !isset($v['host'],
$v['user'], $v['pass'])) {
return true;
}
if ('sqlite' == $v['driver'] && empty($v['path'])) {
return true;
}
return false;
})
->thenInvalid('La configuration de la connexion avec
la base de données est invalide.')
->end()
;
return $treeBuilder;
}
}
Le tableau de configuration final peut être récupéré pour modifier les arguments du
service my_pdo :
<?php
namespace MonEntreprise\AdminBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
$pdoDefinition = $container->findDefinition('my_pdo');
if ('mysql' == $config['driver']) {
$dsn = 'mysql:host='.$config['host'];
$pdoDefinition->setArguments(array(
$dsn,
$config['user'],
$config['pass']
));
} elseif ('sqlite' == $config['driver']) {
$dsn = 'sqlite:'.$config['path'];
$pdoDefinition->addArgument($dsn);
}
}
}
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
mail(
$utilisateur->getEmail(),
'Confirmation d\'inscription',
'Votre inscription sur notre site a bien été validée.'
);
}
services:
my_event_subscriber:
class: Eni\DemoBundle\Listener\InscriptionListener
tags:
- { name: eni.event_subscriber }
La particularité de ce service est de comporter un tag. Un tag est un marqueur placé
sur un service. Ce marqueur peut ensuite être utilisé pour retrouver le service, lors de
la phase du Compiler Pass. Ici, le service est marqué comme
un eni.event_subscriber, c’est une valeur libre que nous avons choisie.
c. Le Compiler Pass
Par convention, il est conseillé de placer le Compiler Pass dans le dossier
DependencyInjection/Compiler :
// src/Eni/DemoBundle/DependencyInjection/Compiler/
RegisterListenersPass.php
namespace Eni\DemoBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
services:
my_event_subscriber:
class: Eni\DemoBundle\Listener\InscriptionListener
tags:
- { name: eni.event_subscriber, priority: 5 }
La dernière étape pour finaliser le Compiler Pass consiste à l’enregistrer. Cela se fait
depuis le fichier principal du bundle, à la racine de celui-ci :
<?php
namespace Eni\DemoBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Eni\DemoBundle\DependencyInjection\Compiler\RegisterListenersPass;
$container->addCompilerPass(new RegisterListenersPass());
}
}
Grâce à ce Compiler Pass, pour enregistrer un listener sur un événement donné, vous
n’aurez qu’à ajouter un tag lors de la définition du service.
Précédent
Le chargement automatique de services
Suivant
Quiz
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le concept de Templating
2. Templating et modèle MVC
Twig
Les gabarits de pages (layouts) et les blocks
Le langage Twig
Les filtres et les fonctions
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Quiz
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Présentation et concepts
1. Le concept de Templating
La notion de template (ou modèle) fait référence au principe d’élaboration de modèles
statiques de pages HTML dans lesquels viendront s’insérer des données dynamiques
résultant de l’exécution du code PHP.
Le templating est l’approche générale de découpage entre ces données statiques et
dynamiques, toujours dans l’optique de séparer les responsabilités : les traitements
applicatifs d’un côté, la présentation des données issues de ces traitements de l’autre.
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Présentation
2. Pourquoi un nouveau langage ?
3. Mise en pratique
4. Remarques sur l’utilisation
5. La notation des templates
6. Extension du système de templates
7. L’annotation @Template
Twig
1. Présentation
Twig est un moteur de templates, une librairie permettant de gérer la couche
« présentation », pour les applications utilisant le modèle de conception MVC.
Globalement, le principe est d’extraire tout ce qui est relatif à la vue dans des fichiers
dédiés, appelés « templates », qui représentent pour la plupart du HTML.
Nous insistons ici sur le terme « représenter ». En effet, les templates ne contiennent
pas forcément le code HTML tel qu’il sera retourné par l’application, ils contiennent
bien souvent du code permettant de générer ce code HTML. Ce code utilise un
langage spécifique au moteur de template et ce langage est communément appelé, par
extension, du « Twig ».
Twig est un projet qui voit le jour en 2008. Son auteur Armin Ronacher s’inspire très
largement du framework Jinja, un moteur de template pour Python. Aujourd’hui, le
projet Twig est maintenu par les équipes de développement de Symfony dont il est le
moteur de template par défaut. Le site officiel de Twig est accessible à
l’adresse https://twig.symfony.com.
3. Mise en pratique
Voyons comment créer concrètement cette couche Vue au sein de notre
application. Nous connaissons pour l’instant le processus entre une requête et une
réponse (cf. Architecture du framework et Routage et contrôleur). Nous savons que le
contrôleur est chargé de retourner une réponse HTTP, sous la forme d’un objet de
type Symfony\Component\HttpFoundation\Response :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Template
{# templates/default/hello.html.twig #}
Hello world!
7. L’annotation @Template
Le bundle SensioFrameworkExtraBundle intégré par défaut, offre un raccourci
intéressant pour l’affichage de vos templates. Plutôt que d’invoquer la
méthode render() du contrôleur comme nous venons de le voir, vous pouvez tout
simplement apposer une annotation sur votre action :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
Cette approche, bien que toujours utilisable, tend à être de moins en moins utilisée à
cause de l’aspect un peu trop « magique » de ce comportement. Nous vous
conseillons, de fait, de toujours utiliser la méthode render(), permettant de mieux
contrôler quel template sera rendu en fin d’action.
Précédent
Présentation et concepts
Suivant
Les gabarits de pages (layouts) et les blocks
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. La composition de pages
2. Définition des gabarits
3. Les blocks
Le langage Twig
Les filtres et les fonctions
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Quiz
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
On y reconnaît une page HTML5 basique mais certains mots-clés nous sont
inconnus : ils appartiennent au langage Twig.
Ces instructions de langage Twig sont des blocks. Ils sont définis via la syntaxe
suivante :
{% block nom_du_block %}
Contenu optionnel du bloc
{% endblock %}
nom_du_block est l’intitulé du block, nous vous conseillons vivement de choisir des
noms descriptifs. Dans notre layout par défaut, c’est déjà le cas.
Essayons d’interpréter le template templates/base.html.twig :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
Le code obtenu est en réalité légèrement différent, il contient la barre d’outils avec les
informations utiles lors du développement. Cette barre est générée automatiquement
par Symfony sur les pages HTML. Par souci de simplicité, nous omettons celle-ci du
code HTML.
Nous remarquons, en comparant le code HTML généré à celui du template, que les
blocks ont disparu.
3. Les blocks
Les blocks sont des zones, des marqueurs, placés sur les templates et dont le contenu
est optionnel (ils peuvent tout à fait être vides).
title, stylesheets, body et javascripts correspondent respectivement aux zones pour
le titre, les feuilles de style CSS, le contenu principal ainsi que les scripts JavaScript.
Les noms des blocks peuvent être choisis librement par le développeur : il n’y a pas
de comportement particulier en fonction de l’intitulé d’un block.
Les blocks sont particulièrement utiles lors de l’héritage de templates et nous pouvons
constater que le template base.html.twig est minimal.
Il peut servir de base pour d’autres templates :
Contrôleur
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Template
{# default/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
Hi there!
{% endblock %}
{% extends 'base.html.twig' %}
{% block title %}
{{ parent() }} Page d'index
{% endblock %}
{% block body %}
Hi there!
{% endblock %}
{% extends 'base.html.twig' %}
{% block javascripts %}
{{ parent() }}
<script src="http://..."></script>
{% endblock %}
...
Précédent
Twig
Suivant
Le langage Twig
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Le langage Twig
1. Les différents types d’instructions
Malgré les connaissances basiques que nous avons sur Twig pour l’instant, nous
pouvons déjà dégager des exemples précédents deux types de notations :
{% ... %} : cette instruction exécute ou définit quelque chose. Elle est liée à la
logique et c’est au travers de celle-ci que nous étendons des templates
({% extends … %}) ou définissons des blocks ({% block … %}).
{{ ... }} : cette instruction affiche quelque chose, le contenu du template parent par
exemple (via {{ parent() }}).
Sachez qu’il existe une troisième instruction, qui nous permet de placer des
commentaires au sein du template :
{# Ceci est un commentaire Twig #}
Les commentaires Twig, contrairement aux commentaires HTML, seront effacés lors
de la génération du code, donc non visibles dans le code source de la page.
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
{% extends 'base.html.twig' %}
{% block title %}
{{ titre }}
{% endblock %}
{% block body %}
{% for bonjour in bonjours % }
{{ bonjour }}<br />
{% endfor %}
{% endblock %}
Le code généré :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Bienvenue à tous</title>
</head>
<body>
Hello<br />
Bonjour<br />
Hallo<br />
</body>
</html>
Ici, lorsque variable est un objet, Twig renvoit la valeur, par ordre de priorité, de :
$variable[’valeur’], si l’objet implémente l’interface ArrayAccess.
la propriété valeur de l’objet : $variable->valeur.
la méthode $variable->valeur().
la méthode $variable->getValeur().
la méthode $variable->isValeur().
null le cas échéant.
Si variable est un tableau, Twig renvoie le contenu de la clé valeur, ou null (si la
clé valeur n’existe pas).
if ($variable['valeur'] == 'Bonjour') {
echo 'Bienvenue!';
} elseif ($variable['valeur'] == 'Hallo') {
echo 'Willkommen!';
} else {
echo 'Welcome!';
}
Ici, $variable[’valeur’] n’est pas forcément ce qui est généré car Twig utilise les
règles d’accès aux tableaux/objets que nous avons définies précédemment.
b. Les boucles
Les boucles permettent d’itérer sur des tableaux (ou objets implémentant
l’interface Traversable) :
{% for valeur in tableau %}
{{ valeur }}<br />
{% endfor %}
L’instruction for de Twig permet aussi de récupérer les clés, ce qui s’avère
particulièrement intéressant pour les tableaux associatifs :
{% for cle, valeur in tableau %}
{{ cle }} : {{ valeur }}<br />
{% endfor %}
Un raccourci très appréciable avec Twig est la possibilité d’afficher un texte alternatif
lors d’une tentative d’itération sur un tableau vide :
{% for valeur in tableau %}
{{ valeur }}<br />
{% else %}
Le tableau est vide.<br />
{% endfor %}
if ($tableau) {
foreach ($tableau as $valeur) {
echo $valeur . '<br />';
}
} else {
echo 'Le tableau est vide.<br />';
}
À l’intérieur de chaque boucle, une variable spéciale est accessible ; son nom est loop.
Elle contient de précieuses informations quant au déroulement des itérations :
« Quelle est la longueur totale du tableau ? » ou « À quelle occurrence sommes-
nous ? » sont autant de questions auxquelles elle pourra répondre.
Attributs de la variable loop :
Twig
loop.index Le numéro de l’itération courante. dispose
d’autres
loop.revindex Le nombre d’itérations à effectuer pour atteindre la fin de
la boucle.
En PHP :
<?php
Pour les affectations plus complexes, on utilise ce tag avec des balises ouvrantes et
fermantes :
{% set variable %}
{% for ... %}
{# ... #}
{% endfor %}
{# ... #}
{% endset %}
Cette deuxième technique s’avère relativement puissante quand elle est combinée à
d’autres tags, les valeurs générées pouvant ainsi devenir dynamiques (boucles,
conditions, etc.).
b. Twig et l’échappement
Par défaut, et par mesure de sécurité, Twig est plutôt agressif quant à sa
politique d’échappement : il considère toutes les variables comme potentiellement
dangereuses.
En conséquence, les variables contenant du code HTML (même maîtrisé par le
développeur) seront mal affichées, car les caractères spécifiques au HTML seront
échappés, empêchant le navigateur de les interpréter.
{% set titre = '<h1>Bac à sable</h1>' %}
{{ titre }}
Pour le template ci-dessus, le navigateur affichera les balises, car le code source serait
le suivant :
<h1>Bac à sable</h1>
{{ titre }}
{% endautoescape %}
<script>
var nom = "{{ nom }}";
// ...
</script>
Si ces morceaux de code JavaScript ou de CSS utilisent des variables dont le contenu
n’est pas maîtrisé par le développeur (provenant d’un utilisateur), il conviendra de les
échapper, de manière à éviter les injections de code.
Dans le cas ci-dessus, pour le code JavaScript, si la variable nom contient ";
alert("aaa"); var a = ", une alerte apparaîtra à l’écran, alors que le développeur
souhaite simplement affecter une valeur à une variable.
Voici comment modifier la stratégie d’échappement :
{% autoescape 'html' %}
Les variables de ce block seront échappées selon la stratégie
HTML.
C'est le comportement par défaut.
{% endautoescape %}
{% autoescape 'css' %}
Les variables de ce block seront échappées selon la stratégie
CSS.
{% endautoescape %}
{% autoescape 'js' %}
Les variables de ce block seront échappées selon la stratégie
JavaScript.
{% endautoescape %}
twig:
default_path: '%kernel.project_dir%/templates'
debug: %kernel.debug%
strict_variables: %kernel.debug%
autoescape: false
La clé en question est autoescape. Ici, via false, nous désactivons l’échappement.
Désactiver l’échappement par défaut implique une parfaite maîtrise des
variables utilisées dans les templates. Les variables non sûres devront être échappées
manuellement à l’affichage (au travers du tag autoescape ou du filtre escape). Cette
technique est déconseillée et nous l’évoquons à titre purement informatif.
Le template inclus aura accès aux variables définies dans le template incluant ; des
variables supplémentaires peuvent être passées lors de l’inclusion :
{{ include('publicite.html.twig', {'theme': 'cosmétiques'})
}}
Enfin, le template inclus peut se voir interdire l’accès aux variables du contexte
extérieur via le paramètre with_context :
{{ include('publicite.html.twig', with_context = false) }}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
{# affiche 'bonjour' #}
upper
Ce filtre est semblable à lower, mais renvoie des majuscules.
{{ 'Bonjour'|upper }}
{# affiche 'BONJOUR' #}
title
Ce filtre retourne une casse adaptée aux titres.
{{ 'le gilet bleu'|title }}
capitalize
Ce filtre met en majuscule le premier caractère de la chaîne.
{{ 'le gilet bleu'|title }}
b. Échappement
Nous avons déjà pris connaissance de la manière dont Twig gérait l’échappement
mais nous allons revenir ici sur le filtre escape précédemment évoqué, ainsi que sur
un nouveau filtre, raw.
escape
Le filtre escape a un alias : e. Les deux notations ci-dessous sont donc équivalentes :
{{ variable|escape('html') }}
{{ variable|e('html') }}
{# ou #}
{{ variable|e }}
Il est optionnel de spécifier la stratégie HTML car c’est la stratégie par défaut.
raw
Dans un contexte où l’échappement est activé, pour ne pas échapper une
variable spécifique, il faut utiliser le filtre raw :
{% autoescape %}
{{ variable|raw }} {# cette variable ne sera pas échappée #}
{% endautoescape %}
Si plusieurs filtres sont enchaînés, le filtre raw doit toujours être placé en dernier.
striptags
Une alternative à l’échappement est la suppression pure et simple des balises HTML.
{{ '<h1>Titre</h1>'|striptags }}
{# affiche 'Titre' #}
c. L’encodage
convert_encoding
De manière générale, il est recommandé d’utiliser l’encodage UTF-8 avec Symfony.
Il est configuré par défaut au niveau du Kernel ainsi que pour différents services :
Twig ou Doctrine (pour les bases de données) par exemple.
Si vous n’avez pas le contrôle de l’encodage sur une quelconque variable affichée
(par exemple dans le cas de données issues d’un flux RSS récupéré depuis une source
extérieure), il est possible d’appliquer un filtre qui se chargera de gérer l’encodage.
{{ caracteres_non_utf8|convert_encoding('UTF-8', 'ISO-8859-1') }}
En plus d’une utilisation traditionnelle en tant que format d’échange de données (API,
Ajax, etc.), ce filtre peut être utilisé pour générer des objets littéraux JavaScript.
On pourrait imaginer un plug-in jQuery dont les options seraient dynamiques (en
fonction des préférences de l’utilisateur) :
$('selecteur').plugin(
{{ options|json_encode|raw }}
);
{# le code généré pourrait être #}
$('selecteur').plugin(
{"color":"blue","title":"Messages de Pierre"}
);
Cette technique a toutefois une limite, elle supporte uniquement les configurations
simples (valeurs scalaires, tableaux, objet littéral imbriqué) autorisées par le format
JSON : les fonctions anonymes (ou Closures) ne sont pas transformables au travers de
ce filtre.
url_encode
Le filtre url_encode permet d’encoder des variables qui seront utilisées en tant que
parties d’URL.
Ce filtre accepte une chaîne de caractères ou un tableau.
{{ "mot-clé"|url_encode }}
{# génère ' mot-cl%C3%A9' #}
Vous n’aurez pas besoin de ce filtre pour générer les URL de votre application car
celui-ci est uniquement utile pour gérer les URL externes. Pour les URL
internes, reportez-vous à la section ci-après (Twig et le routage).
3. Les fonctions
Avec Twig, les fonctions sont semblables à ce que l’on entend par ce terme en PHP.
On pourrait définir une fonction par un morceau de code effectuant une certaine tâche
et caractérisé par un nom. C’est par ce nom que le développeur peut invoquer cette
fonction, de manière illimitée. Les fonctions acceptent souvent des paramètres,
correspondant à des cibles sur lesquelles le traitement doit être effectué, ou des
informations complémentaires quant au déroulement souhaité. Enfin, avec Twig, elles
retournent ou affichent toujours quelque chose.
a. Twig et le routage
Les routes sont très utilisées dans les templates et nous savons comment générer des
URL depuis le contrôleur (cf. Routage et contrôleur - Le contrôleur). Mais il est
fréquent de vouloir en générer depuis les vues, notamment pour les liens de ses pages
HTML.
Il existe deux fonctions relatives à la génération de routes : path et url.
path
Cette première fonction génère une URL relative. En général, il est préférable de
générer ce type d’URL.
<a href="{{ path('ma_route') }}">Visiter ma page.</a>
#}
Les paramètres de substitution doivent être placés en tant que second argument, dans
un tableau associatif :
<a href="{{ path('hello', { 'name': 'world' }) }}">
Visiter ma page.
</a>
{# Code généré :
<a href="/hello/world">
Visiter ma page.
</a>
#}
url
Cette fonction s’utilise de la même manière que path. La seule différence réside dans
la valeur retournée : c’est une URL absolue.
<a href="{{ url('ma_route') }}">Visiter ma page.</a>
{# Code généré :
<a href="http://localhost/sf/hello/world">
Visiter ma page.
</a>
#}
Un cas concret nécessitant des URL absolues est la génération du contenu d’un e-mail
avec Twig.
b. Débogage avec la fonction dump
En PHP, une fonction très pratique pour connaître le contenu d’une variable
est var_dump() car, à la différence de l’instruction echo, var_dump() peut afficher le
contenu de variables de types complexes tels que des tableaux ou des objets.
Avec Twig, nous utiliserons la fonction dump :
{{ dump(ma_variable) }}
{# équivalent de 'var_dump($ma_variable);' #}
Précédent
Le langage Twig
Suivant
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
Accéder aux bases de données avec Doctrine
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Ce processus peut s’avérer fastidieux car lorsque vous travaillez sur un fichier
JavaScript, par exemple, il vous faut lancer cette commande après chaque
modification de manière à pouvoir tester la page avec la dernière version du script.
Une alternative permet de contourner cela : l’utilisation de liens symboliques
(symlinks). Appliqué à un répertoire, un lien symbolique est un dossier « miroir »
pointant vers ce répertoire. En quelque sorte, on pourrait dire que c’est une copie de
dossier synchronisée automatiquement.
php bin/console assets:install public --symlink
<script
src="{{ asset('bundles/monbundle/js/mon_script.js') }}"></script>
{# référence public/bundles/monbundle/js/mon_script.js #}
Les pages de votre application devraient être décorées du style par défaut de
Bootstrap 4. Il vous suffit ensuite d’explorer les styles CSS présents dans Bootstrap
pour enjoliver vos éléments HTML.
Précédent
Les filtres et les fonctions
Suivant
Quiz
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Présentation et concepts
1. Les principes de l’ORM
Doctrine est une librairie permettant de gérer les interactions entre une application et
une (ou plusieurs) base de données. Bien que ce ne soit pas la seule manière de
communiquer avec une base de données (il existe d’autres librairies, comme Propel,
l’extension PHP PDO, etc.), Symfony a clairement fait de Doctrine son option
favorite. En effet, cette dernière est préconfigurée par défaut dans Symfony depuis la
version 2 du framework.
L’ORM (Object-To-Relational Mapping), ou mapping objet relationnel, est un
concept permettant d’associer des classes du modèle objet de PHP avec des tables de
bases de données. Une fois cette association réalisée (le « mapping »), la manipulation
des classes et des objets agit directement sur les données stockées en base, permettant
ainsi au programmeur de simplifier les opérations basiques de gestion de données.
2. Architecture de Doctrine
Avant d’entrer dans le vif du sujet, il convient de définir certains concepts
nécessaires à la compréhension du fonctionnement de Doctrine.
Doctrine se compose de plusieurs couches : DBAL, ORM et Entité.
a. DBAL
La couche DBAL (Database Abstraction Layer) est la couche de plus bas niveau. Elle
ne comporte aucune logique applicative et son rôle est d’envoyer des requêtes vers
une base de données et de récupérer les résultats. Elle est comparable à l’extension
PHP PDO et pourrait être définie comme étant une version plus avancée de cette
extension.
PHP PDO est d’ailleurs utilisé en interne par la couche DBAL.
b. Entité
Les entités sont les classes d’une application ayant été configurées de manière à ce
que Doctrine puisse établir une correspondance entre ces dernières et des tables en
base de données.
La principale fonctionnalité apportée par Doctrine (par rapport à une utilisation
directe de l’extension PHP PDO par exemple) en plus de pouvoir récupérer des objets
au lieu de tableaux en résultats de requêtes (car PDO est également capable de le
faire), est d’établir une véritable relation entre ces objets et les tables en base de
données.
L’entité est donc le reflet applicatif d’une table de la base de données, ses
propriétés étant en quelque sorte équivalentes à des colonnes.
Ainsi, en utilisant Doctrine, vous
pouvez récupérer, ajouter, modifier et supprimer des données en base sans avoir à
écrire une seule ligne de code SQL, toutes ces actions pouvant être effectuées au
travers des objets Entité.
c. ORM
La couche ORM (Object-relational mapping) est la librairie placée au cœur du
système ; elle est l’intermédiaire entre l’application et la couche DBAL. Son rôle est
de convertir les données tabulaires reçues depuis le DBAL en entités, mais aussi de
transformer les interactions avec les différents objets mis à disposition du développeur
(que nous évoquerons plus loin) en requêtes SQL à transmettre au DBAL.
En clair, la couche ORM établit une correspondance entre la base de données
relationnelle et la programmation orientée objet.
3. La notion d’entité
Dans Doctrine, et de manière générale dans les frameworks d’ORM, la notion d’entité
se rapporte aux classes qui représentent des données stockées dans une table de base
de données. À part de rares exceptions, chaque table sera identifiée une classe et
chaque colonne de la table aura une représentation sous la forme d’un attribut dans la
classe.
De ce fait, une classe d’entité est essentiellement une classe de structure de données
métier. Les informations y sont représentées par des attributs privés, pour respecter les
principes d’encapsulation du modèle objet, et des méthodes d’accès
(getXXX() et setXXX()) y sont définies pour permettre l’accès à ces attributs.
Cette structure est importante car Doctrine va automatiquement appeler ces méthodes
d’accès afin de valoriser et de lire les données sur chaque entité au moment des
interactions avec la base. Autre point important : le constructeur. Il doit pouvoir être
instancié sans paramètres.
Il est important de noter ici que les entités ne sont pas considérées comme des
services Symfony. En effet leur cycle de vie étant géré par Doctrine, elles ne peuvent
être prises en charge par le Service Container.
Précédent
Quiz
Suivant
Installer et configurer Doctrine
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
ou
symfony new mon_projet --version=4.4 --full
alors Doctrine est déjà présent par défaut dans votre application comme en atteste le
fichier composer.json situé à la racine du projet :
{
"type": "project",
"license": "proprietary",
"require": {
"php": ">=7.1.3",
...
"doctrine/doctrine-bundle": "^2.3",
...
},
...
}
Dans le cas contraire, il est nécessaire d’installer le pack Symfony ORM via la
commande :
composer require symfony/orm-pack
La configuration du pilote PDO de votre base de données doit être réalisée avant
d’aller plus loin dans la démarche.
3. Configuration
La configuration principale de Doctrine est définie
dans config/packages/doctrine.yaml :
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy:
doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
On y trouve deux sections, dbal et orm. La section orm permet de définir les
mécanismes de localisation des entités, comme le répertoire de base (src/Entity) et
l’espace de nom appliqué par défaut (App\Entity) alors que la section dbal fait
référence à la variable d’environnement DATABASE_URL.
La variable DATABASE_URL est définie dans les fichiers d’environnement de votre
projet Symfony, au minimum dans le fichier .env situé à la racine de votre projet.
Cette variable contient la chaîne de connexion PDO permettant à la couche DBAL de
Doctrine de se connecter à la base de données sous-jacente.
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doc
# trine-dbal/en/latest/reference/configuration.html#connecting-using-a-
# url
# IMPORTANT: You MUST configure your server version, either here or in
# config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/
db_name?serverVersion=5.7"
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/
db_name?serverVersion=13&charset=utf8"
DATABASE_URL="mysql://monjournal:monjournal@127.0.0.1:3306/
monjournal?serverVersion=5.7"
###< doctrine/doctrine-bundle ###
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
namespace App\Entity;
class Livre
{
private $id;
private $titre;
private $dateParution;
/**
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* @param string $titre
* @return Livre
*/
public function setTitre($titre)
{
$this->titre = $titre;
return $this;
}
/**
* @return string
*/
public function getTitre()
{
return $this->titre;
}
/**
* @param \DateTime $dateParution
* @return Livre
*/
public function setDateParution($dateParution)
{
$this->dateParution = $dateParution;
return $this;
}
/**
* @return \DateTime
*/
public function getDateParution()
{
return $this->dateParution;
}
}
Les propriétés sont privées, et des accesseurs et des mutateurs doivent donc être créés.
Les propriétés de vos entités ne doivent jamais être publiques, mais privées ou
protégées.
namespace App\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="livre")
*/
class Livre
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="titre", type="string", length=255)
*/
private $titre;
/**
* @ORM\Column(name="date_parution", type="date",
* nullable=true)
*/
private $dateParution;
/**
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* @param string $titre
* @return Livre
*/
public function setTitre($titre)
{
$this->titre = $titre;
return $this;
}
/**
* @return string
*/
public function getTitre()
{
return $this->titre;
}
/**
* @param \DateTime $dateParution
* @return Livre
*/
public function setDateParution($dateParution)
{
$this->dateParution = $dateParution;
return $this;
}
/**
* @return \DateTime
*/
public function getDateParution()
{
return $this->dateParution;
}
}
Nous détaillons les différentes annotations utilisées plus loin dans ce chapitre. La
dernière étape consiste à mettre à jour sa base de données en lançant la commande
suivante :
php bin/console doctrine:schema:update --force
Il est important de comprendre que les annotations ont un impact sur la structure de
votre base de données. Bien évidemment, cette dernière ne se mettra pas à jour
automatiquement à la sauvegarde du fichier contenant l’entité.
Veillez donc à exécuter la commande doctrine:schema:update après chaque
modification du mapping de vos entités, de manière à synchroniser votre base de
données.
Votre application est maintenant prête à interagir avec la table livre.
Mais nous n’allons pas nous y atteler tout de suite ; commençons par décrire les
principales directives disponibles pour le mapping de vos entités.
namespace App\Entity;
/**
* @ORM\Entity
*/
class MonEntite
{
// ...
}
namespace App\Entity;
/**
* @ORM\Entity(readOnly=true)
*/
class MonEntite
{
// ...
}
namespace App\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="livre")
*/
class Livre
{
/**
* @ORM\Column(name="titre", type="string", length=255)
*/
private $titre;
// ...
namespace App\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="livre")
*/
class Livre
{
/**
* @ORM\Column(type="string", length=255)
*/
private $titre;
// ...
type
Cette propriété définit le type de données de la colonne. Les valeurs acceptées ne sont
pas les types concrètement utilisés en base de données (VARCHAR, INT, etc.) car ce
concept de « type » n’englobe pas seulement le type de la colonne en base de
données, mais également le type de la valeur contenue dans la propriété de l’objet.
Doctrine effectue une conversion entre ces deux représentations. Dans l’exemple
précédent, ce n’est pas flagrant car les deux représentations du type string sont les
mêmes.
Examinons un autre type pour mettre en avant cette nuance :
<?php
namespace App\Entity;
/**
* @ORM\Entity
*/
class Article
{
/**
* @ORM\Column(type="date")
*/
private $date;
// ...
Ici, la colonne date sera de type DATE, tandis que la valeur attendue dans la
propriété $date sera un objet PHP de type DateTime. Lorsque vous récupérez une
entité, la propriété $date contient donc un objet DateTime. De la même manière,
lorsque vous travaillez avec cette propriété, en vue de créer ou de modifier une ligne
en base de données, il faut injecter des objets DateTime (et non des chaînes de
caractères comme ’2017-05-10’).
Les différents types disponibles
sont : string, integer, smallint, bigint, boolean, decimal, date, time, datetime, text
, object, array et float.
Leurs noms sont assez explicites mais n’hésitez pas à vous reporter à la
documentation officielle pour plus de
précisions : https://www.doctrine-project.org/projects/doctrine-orm/en/latest/
reference/basic-mapping.html#doctrine-mapping-types
length
Cette propriété spécifie la longueur de la colonne en base de données. Elle n’est
applicable que si le type est string et elle vaut par défaut 255.
/**
* @ORM\Column(type="string", length=50)
*/
private $titre;
unique
Cette propriété, mise à true, ajoute un index unique sur la colonne :
/**
* @ORM\Column(type="string", unique=true)
*/
private $titre;
Si vous souhaitez définir une clé unique sur plusieurs colonnes, reportez-vous à la
propriété uniqueConstraints de l’annotation @ORM\Table, qui est détaillée plus loin.
nullable
Indique si la valeur NULL est autorisée pour la colonne. Par défaut, elle ne l’est pas.
/**
* @ORM\Column(type="string", nullable=true)
*/
private $nom;
precision et scale
Ces deux propriétés s’appliquent aux colonnes de type float. precision définit le
nombre total de chiffres, que ce soit avant ou après la virgule, et scale définit le
nombre de chiffres après la virgule.
/**
* @ORM\Column(type="float", precision=6, scale=2)
*/
private $valeur;
/**
* @ORM\Entity
* @ORM\Table(name="membre",
* uniqueConstraints={
* @UniqueConstraint(name="pseudo_uniq", columns={"pseudo"})
* },
* indexes={
* @Index(name="email_idx", columns={"email"}),
* @Index(name="nom_idx", columns={"nom"})
* }
* )
*/
class Utilisateur
{
// ...
}
L’exemple ci-dessus est une entité dont la table en base de données est
nommée membre ; les colonnes email et nom sont des index et la
colonne pseudo est un index unique.
Les index sont placés sous forme de tableaux d’annotations au sein de la
propriété indexes. L’annotation utilisée pour chaque index est @Index (détaillée plus
loin).
Quant aux index uniques, ils sont placés dans la propriété uniqueConstraints et
utilisent l’annotation @UniqueConstraint (détaillée plus loin également).
Changer le moteur de table
Avec MySQL, Doctrine utilise par défaut InnoDB comme moteur pour les tables. Si
vous souhaitez en utiliser un autre pour une table donnée, vous pouvez le spécifier au
travers d’un attribut options, sous la clé engine :
namespace App\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="membre", options={"engine": "MyISAM"})
*/
class Utilisateur
{
// ...
}
namespace App\Entity;
/**
* @ORM\Entity
*/
class Livre
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
Ici, une technique de génération par identité est utilisée (IDENTITY) ; pour MySQL
cela signifie l’usage de la propriété AUTO_INCREMENT. Pour les bases de données
Oracle et PostgreSQL, il faut utiliser la valeur SEQUENCE.
Si l’annotation @ORM\GeneratedValue est omise, vous devrez gérer la clé primaire
au niveau applicatif, en vous assurant qu’elle est unique pour chaque nouvelle entité
créée.
e. Configurer les index
@ORM\Index
Cette annotation est utilisée dans la propriété indexes de l’annotation @ORM\Table.
Elle possède deux propriétés : name et columns. La première spécifie le nom de
l’index, tandis que columns contient la ou les colonnes pour cet index.
L’exemple ci-dessous configure un index multicolonne (sur les
colonnes col_1 et col_2 de la table) :
namespace App\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="ma_table",
* indexes={
* @Index(name="multi_idx", columns={"col_1", "col_2"})
* }
* )
*/
class MonEntite
{
// ...
}
@ORM\UniqueConstraint
Tout comme @ORM\Index, @ORM\UniqueConstraint comporte les
propriétés name et column et est utilisée au sein de l’annotation @ORM\Table.
namespace Eni\DemoBundle\Entity;
/**
* @ORM\Entity
* @ORM\Table(name="membre",
* uniqueConstraints={
* @UniqueConstraint(name="uniq", columns={"nom", "prenom"})
* }
* )
*/
class Utilisateur
{
// ...
}
Ici, une clé unique multicolonne est configurée sur nom et prenom.
namespace App\Entity;
/**
* @ORM\Entity
*/
class Biographie
{
// ...
/**
* @ORM\OneToOne(
* targetEntity="App\Entity\Auteur"
* )
*/
private $auteur;
// ...
}
Ici, l’entité Biographie a une relation avec une entité Auteur et cette relation est de
type 1-1. Une clé étrangère est donc placée sur la table de l’entité Biographie, sur une
nouvelle colonne auteur_id, qui référence la clé primaire id de la table correspondant
à l’entité Auteur.
Personnaliser les colonnes de la relation
Pour l’exemple précédent, les deux colonnes auteur_id et id sont les colonnes par
défaut attendues par Doctrine dans le cadre de relations. Si vous souhaitez
personnaliser ces noms, vous devez utiliser une annotation spéciale : @ORM\
JoinColumn.
Cette dernière permet de configurer les colonnes utilisées pour la relation. Elle
possède les propriétés suivantes :
name : nom à utiliser pour la colonne contenant l’identifiant de clé étrangère.
referencedColumnName : nom de la clé primaire de la table référencée.
unique : si cette propriété est placée à true, un index unique est créé pour la
colonne contenant l’identifiant de clé étrangère.
nullable : par défaut, la relation est optionnelle, mais si cette propriété est mise
à false, elle devient obligatoire (la colonne contenant la clé étrangère n’accepte
pas la valeur NULL).
<?php
namespace App\Entity;
/**
* Biographie
*
* @ORM\Entity
*/
class Biographie
{
// ...
/**
* @ORM\OneToOne(
* targetEntity="App\Entity\Auteur"
* )
* @ORM\JoinColumn(
* name="mon_auteur",
* referencedColumnName="sa_cle_primaire",
* unique=true
* )
*/
private $auteur;
// ...
b. @ORM\ManyToOne
Cette annotation est utilisée pour configurer les relations de type n-1.
<?php
namespace App\Entity;
/**
* @ORM\Entity
*/
class Critique
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Livre")
*/
private $livre;
Dans l’exemple ci-dessus, l’entité Critique est en relation avec l’entité Livre.
Plusieurs critiques sont reliées à un livre. De manière plus naturelle, on peut dire
qu’un livre possède plusieurs critiques.
Notons au passage que le nom de classe dans targetEntity n’est pas obligatoirement
un FQCN (Fully Qualified Class Name), si les deux entités appartiennent au même
espace de noms.
c. @ORM\ManyToMany
L’annotation @ORM\ManyToMany est capable de configurer les relations de
type n-n.
Ce type de relation implique une particularité, car, lors d’une relation entre deux
tables A et B, avoir une clé étrangère sur la table A limiterait le nombre d’occurrences
de B à 1. Il faut donc passer par une table de jointure.
Imaginons qu’un Livre puisse avoir plusieurs Thèmes. De la même manière,
un Thème pourrait regrouper plusieurs Livres. Nous serions ici dans le cas d’une
relation de type n-n, qui serait configurable comme suit :
<?php
namespace App\Entity;
/**
* @ORM\Entity
*/
class Livre
{
// ...
/**
* @ORM\ManyToMany(targetEntity="Theme")
*/
private $themes;
namespace App\Entity;
/**
* @ORM\Entity
*/
class Livre
{
// ...
/**
* @ORM\ManyToMany(targetEntity="Theme")
* @ORM\JoinTable(name="themes_des_livres",
* joinColumns={
* @ORM\JoinColumn(
* name="id_du_livre",
* referencedColumnName="id"
* )
* },
* inverseJoinColumns={
* @ORM\JoinColumn(
* name="theme_id",
* referencedColumnName="id"
* )
* }
* )
*/
private $themes;
namespace App\Entity;
/**
* @ORM\ManyToOne(
* targetEntity="App\Entity\Auteur",
* inversedBy="livres"
* )
*/
private $auteur;
// ...
/**
* Get auteur
*
* @return App\Entity\Auteur
*/
public function getAuteur()
{
return $this->auteur;
}
// ...
}
<?php
// src/Entity\Auteur.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Auteur
*
* @ORM\Table()
* @ORM\Entity
*/
class Auteur
{
// ...
/**
* @ORM\OneToMany(
* targetEntity="App\Entity\Livre",
* mappedBy="auteur"
* )
*/
private $livres;
// ...
/**
* Get livres
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getLivres()
{
return $this->livres;
}
// ...
}
Ici, selon les règles que nous avons définies précédemment, le côté propriétaire doit
être Livre : c’est le côté n de la relation n-1 (ou ManyToOne). Dans
l’association @ORM\ManyToOne qu’il contient, la propriété utilisée pour cette
relation dans l’entité du côté inverse (Auteur) doit être spécifiée au sein du
paramètre inversedBy. Ici, nous utilisons livres. Il faut donc créer une
propriété $livres dans l’entité Auteur.
L’annotation d’association utilisée dans Auteur est @ORM\OneToMany (1-n). Elle
est la relation inverse de @ORM\ManyToOne (n-1), et possède quant à elle le
paramètre mappedBy, qui référence la propriété de relation de l’entité du côté
propriétaire (Livre), à savoir auteur.
Ainsi configurée, cette relation est bidirectionnelle ; l’Auteur est récupérable depuis
le Livre, et vice versa.
Pour les relations @ORM\OneToOne (1-1) et @ORM\ManyToMany (n-
n), l’inversion de la relation n’affecte pas sa cardinalité, l’annotation du côté inverse
reste donc la même.
Relation bidirectionnelle avec l’annotation @ORM\OneToOne
Voici un second exemple de relation bidirectionnelle. Les entités y participant
sont Biographie et Auteur, la relation est de type 1-1 et le côté propriétaire
est Biographie.
<?php
namespace App\Entity;
/**
* @ORM\Entity
*/
class Biographie
{
// ...
/**
* @ORM\OneToOne(
* targetEntity="App\Entity\Auteur",
* inversedBy="biographie"
* )
*/
private $auteur;
// ...
}
<?php
namespace App\Entity;
/**
* @ORM\Entity
*/
class Auteur
{
// ...
/**
* @ORM\OneToOne(
* targetEntity="App\Entity\Biographie",
* mappedBy="auteur"
* )
*/
private $biographie;
// ...
}
L’annotation du côté inverse n’a strictement aucun impact sur la structure de votre
base de données, elle intervient seulement au niveau applicatif.
Cette commande effectue deux vérifications. Dans un premier temps, votre mapping
est passé au peigne fin. Ici, il est correctement défini. Si tel n’est pas le cas, repérez
l’entité mentionnée dans le message d’erreur et vérifiez son mapping. Ensuite, la
structure de votre base de données est analysée. Si elle ne correspond pas à celle
attendue en fonction du mapping de vos entités, une erreur sera affichée. Pour régler
le problème, vous devrez tout simplement lancer la
commande doctrine:schema:update.
b. Générer le schéma des données à partir des entités
Précédemment dans ce chapitre, nous avons vu qu’il était possible de créer le schéma
de base de données (les tables) à partir du mapping des entités ; pour cela la
commande doctrine:schema:update est utilisée.
php bin/console doctrine:schema:update --dump-sql
Cette commande permet de voir les requêtes SQL qui seront générées pour mettre à
jour la structure de données.
php bin/console doctrine:schema:update --force
Cette commande permet quant à elle de mettre à jour effectivement les tables, ce qui
peut engendrer des pertes de données.
c. Générer les entités à partir du schéma des données
Une autre option de génération consiste à produire les entités à partir d’un schéma de
données existant. Cette approche est courante dans la mesure où de nombreuses
applications s’appuient sur des structures de données existantes. Avec un schéma très
complexe constitué de plusieurs dizaines de tables, le gain de temps est considérable
car toutes les entités seront automatiquement générées.
Pour générer les entités à partir d’un schéma de données, il faut procéder en deux
étapes :
Générer le mapping des entités ;
Générer les classes d’entités.
Générer le mapping
On utilise la commande doctrine:mapping:import en précisant :
l’espace de noms des entités ;
le format de configuration ;
le répertoire dans lequel le code doit être généré.
À titre d’exemple :
php bin/console doctrine:mapping:import App\Entity annotation --path=src/
Dans le cas où le mapping a été généré via des annotations, cette commande se
contente simplement d’ajouter les getters et les setters à la classe d’entités.
Précédent
Installer et configurer Doctrine
Suivant
Manipulation des entités avec l’EntityManager
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le rôle de l’EntityManager
2. Insertion de données
3. Modification de données
4. Suppression de données
5. Autres opérations de l’EntityManager
a. refresh()
b. detach()
6. Les opérations en cascade
2. Insertion de données
Pour insérer une ligne dans une table donnée, il faut tout d’abord instancier l’entité
correspondant à cette table :
use App\Entity\Livre;
Ensuite, les données à insérer doivent être injectées au travers des mutateurs :
use App\Entity\Livre;
Enfin, il ne reste plus qu’à demander à Doctrine d’envoyer la requête d’insertion vers
la base de données :
use App\Entity\Livre;
$em = $container->get('doctrine')->getManager();
$em->persist($livre);
$em->flush();
Revenons sur cette dernière étape plus en détail. Dans un premier temps, nous
récupérons l’EntityManager de Doctrine. Ce dernier peut être récupéré en
invoquant la méthode getManager() du service doctrine.
Dans la majorité des situations, le code de récupération de l’EntityManager se trouve
au sein d’un service de manipulation de données créé dans votre application.
L’avantage de cette approche est que l’EntityManager Doctrine pourra y être injecté.
Voici un exemple d’un tel service :
namespace App\Model;
use App\Entity\Livre;
use Doctrine\ORM\EntityManagerInterface;
class ArticleService
{
private $em;
3. Modification de données
La modification de données est quasiment identique à l’insertion. La seule
différence est que l’entité cible n’est pas instanciée manuellement, mais récupérée au
travers de Doctrine. Elle correspond donc déjà à une ligne en base de données.
Dans un second temps, les modifications sont appliquées sur l’entité, avant
l’invocation des méthodes persist() et flush() de l’EntityManager :
public function modifierTitreLivre($idLivre)
{
$repository = $this->em->getRepository('App:Livre');
$livre = $repository->find($idLivre);
$livre->setTitre('Nouveau titre');
$this->em->persist($livre);
$this->em->flush();
}
4. Suppression de données
La suppression de données implique tout d’abord, comme c’est le cas pour une
modification, de récupérer l’entité. Sur cette entité, la méthode remove() de
l’EntityManager doit être appliquée :
public function supprimerLivre($idLivre)
{
$repository = $this->em->getRepository(App:Livre');
$livre = $repository->find($idLivre);
$this->em->remove($livre);
$this->em->flush();
}
$this->em->persist($livre);
// détache l'entité
$this->em->detach($livre);
$this->em->flush();
/**
* @ORM\Entity
*/
class Livre
{
// ...
/**
* @ORM\ManyToOne(
* targetEntity="Auteur",
* onDelete="CASCADE",
* onUpdate="CASCADE",
* )
*/
private $auteur;
$this->em->persist($auteur);
$this->em->flush();
namespace App\Entity;
/**
* Auteur
*
* @ORM\Table()
* @ORM\Entity
*/
class Auteur
{
// ...
/**
* @ORM\OneToMany(
* targetEntity="Eni\DemoBundle\Entity\Livre",
* mappedBy="auteur",
* cascade={"persist", "remove"}
* )
*/
private $livres;
// ...
}
Avec Doctrine, les opérations en cascade sont définies sur la propriété du côté inverse
de la relation.
Désormais, le code suivant est fonctionnel :
$auteur = new Auteur();
$this->em->persist($auteur);
$this->em->flush();
Ici, l’intérêt n’est pas flagrant, cependant, pour les opérations de suppression, la
longueur du code se voit fortement réduite :
$auteur = $repository->find($id);
$this->em->remove($auteur);
$this->em->flush();
Au lieu de :
$auteur = $repository->find($id);
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le repository
a. Un rôle de centralisateur
b. Les méthodes de base du repository
c. Les méthodes personnalisées du repository
2. Le DQL
a. SELECT
b. FROM
c. JOIN et LEFT JOIN
d. WHERE
e. ORDER BY
f. Les limites
g. Les limites et la pagination
3. Le QueryBuilder
Fonctionnalités avancées
Quiz
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
findOneBy()
Cette méthode retourne une entité en fonction de critères de filtrage donnés. Les
critères sont passés en argument sous la forme d’un tableau :
$entite = $repository->findOneBy(array(
'nom' => 'Larry',
'pays' => 'USA',
));
Le code ci-dessus récupère l’entité dont le nom vaut Larry et le pays vaut USA.
Si vous n’avez qu’un seul critère de filtrage, vous pouvez utiliser le raccourci
suivant :
$entite = $repository->findOneByNom('Larry');
findBy()
La méthode findBy() est semblable à findOneBy(), à la différence qu’elle
récupère plusieurs entités tandis que findOneBy() n’en retourne qu’une seule.
findBy() accepte également plus de paramètres que findOneBy(), à savoir un ordre
de tri et une limite :
$entites = $repository->findByNom(
'Larry',
array('age' => 'DESC'),
10,
0
);
use Doctrine\ORM\EntityRepository;
class LivreRepository extends EntityRepository
{
public function maMethode()
{
// ...
}
// ...
}
/**
* Livre
*
* @ORM\Entity(
* repositoryClass="App\Entity\LivreRepository"
* )
*/
class Livre
{
// ...
}
2. Le DQL
Les questions qui se posent maintenant sont : « Que doivent contenir les
méthodes personnalisées d’un repository ? Comment effectuer des requêtes
complexes, non supportées par les méthodes de base du repository ? »
Comme nous l’avons rapidement évoqué, il faut utiliser un langage spécifique à
Doctrine : le DQL.
Le DQL est le langage utilisé pour créer des requêtes avec Doctrine ; il remplace le
traditionnel SQL. Bien évidemment, le SQL est toujours utilisé en interne, car votre
base de données ne peut communiquer qu’avec ce langage, mais vous ne l’écrivez pas
vous-même : les requêtes DQL sont transformées en requêtes SQL par Doctrine.
L’intérêt d’écrire ses requêtes en DQL plutôt qu’en SQL est multiple.
L’abstraction : avec le DQL, nous n’utilisons pas des noms de tables et de
colonnes, mais des noms d’entités et de propriétés. Cela permet de se détacher de
la structure concrète de la base de données. Le mapping des entités est utilisé de
pair avec le langage DQL.
La simplicité d’utilisation : cette abstraction permet au langage DQL d’être plus
concis que le SQL. À titre d’exemple, pour effectuer une jointure, il n’y a pas
besoin de spécifier les colonnes y participant car Doctrine les connaît déjà (grâce
au mapping des entités).
L’exemple suivant est une méthode personnalisée du repository retournant toutes les
entités Livre :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
Comme vous pouvez le constater, ce langage est semblable au SQL. Il sera donc,
malgré ses quelques différences, assez facile à prendre en main.
a. SELECT
La commande SELECT permet de spécifier les entités à récupérer. Les entités à
sélectionner sont séparées par des virgules et référencées par leur alias, qui est défini
juste après la classe de l’entité.
Pour la requête DQL précédente, l’entité App:Livre possède l’alias l.
En SQL, il est possible de sélectionner des colonnes particulières. Avec
Doctrine, l’équivalent serait de récupérer des entités dont toutes les propriétés ne sont
pas renseignées. Cela est possible, mais vivement déconseillé, car
potentiellement source de problèmes. Cependant, cette technique a du sens si elle est
alliée à l’hydratation, qui consiste à récupérer des tableaux, par exemple, et non des
entités. Nous évoquerons l’hydratation plus loin dans ce chapitre.
b. FROM
La clause FROM est semblable à celle du langage SQL, à la différence que la classe
de l’entité est utilisée à la place du nom de la table, et que l’alias est obligatoire.
Le nom de l’entité spécifiée est primordial : si App:Livre est utilisé, le résultat sera
un tableau d’entités Livre. Peu importe la requête ou les jointures que vous
effectuerez, le résultat sera toujours des entités Livre. C’est l’entité principale de la
requête.
La classe de l’entité peut utiliser le nom court ou un FQCN. Les requêtes DQL
suivantes sont donc strictement équivalentes :
SELECT l FROM App:Livre l
SELECT l FROM App\Entity\Livre l
use Doctrine\ORM\EntityRepository;
return $query->getResult();
}
}
c. JOIN et LEFT JOIN
En DQL, les jointures s’effectuent avec les clauses JOIN et LEFTJOIN.
Contrairement au SQL, la clause RIGHT JOIN n’existe pas, pour une raison
intrinsèquement liée aux résultats attendus pour une requête. Comme nous venons de
l’expliquer avec la clause FROM, l’entité y étant spécifiée est l’entité principale de la
requête ; les entités en relation avec Livre sont accessibles au travers de cette entité
uniquement (Livre::getCritique(), Livre::getAuteur(), etc.) Une clause RIGHT
JOIN impliquerait le renvoi potentiel d’entités sans relation avec Livre, ce qui
n’aurait aucun sens. D’ailleurs, dorénavant, nous cesserons de systématiquement
comparer le DQL avec le SQL car il est important que vous compreniez que le DQL
est utilisé pour interroger la couche modèle de votre application, et non la base de
données directement.
Pour découvrir les jointures, créons une méthode dans un LivreRepository retournant
les Livres et leur Auteur :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
return $query->getResult();
}
}
Cette méthode serait invoquée comme suit depuis un service dans lequel
l’EntityManager serait injecté :
public function ListeDesLivres()
{
$livres = $this->em
->getRepository('App:Livre')
->getLivresEtAuteur()
;
return $livres;
}
Au niveau d’une vue Twig à laquelle nous transmettrions ce tableau via un contrôleur,
nous pourrions imaginer un affichage de chaque livre et de son auteur dans une liste :
<ul>
{% for livre in livres %}
<li>
"{{ livre.titre }}"
par {{ livre.auteur.prenom }}
{{ livre.auteur.nom }}
</li>
{% endfor %}
</ul>
Le résultat est un tableau d’entités Livre. Nous itérons dessus et ensuite, pour chaque
entrée, livre.auteur permet de récupérer l’auteur d’un article (Twig invoque la
méthode getAuteur(), cf. Les Templates avec Twig - Les gabarits de pages (layout) et
les blocks). La propriété $auteur de la classe Livre contient quant à elle
l’entité Auteur en relation avec Livre, car elle est mappée de cette façon :
/**
* @ORM\ManyToOne(
* targetEntity="App\Entity\Auteur",
* inversedBy="livres"
* )
*/
private $auteur;
Les jointures sont donc importantes si vous souhaitez travailler avec les entités en
relation avec l’entité principale de la requête. Cependant, Doctrine est capable de
retrouver ces entités même si la jointure n’est pas faite explicitement lors de la
requête. Bien que cette fonctionnalité soit intéressante, elle pourrait réduire fortement
les performances d’une application si on ne sait pas la maîtriser.
Pour illustrer ce problème, reprenons notre dernière requête DQL avec une jointure :
SELECT l , a
FROM App:Livre l
JOIN l.auteur a
Les clauses LEFT JOIN sont utilisées pour les jointures optionnelles : si l’entité en
relation n’est pas présente pour une entité, la propriété de la relation vaudra NULL,
tandis qu’avec une clause JOIN, les entités principales n’ayant pas la relation ne
seront pas retournées.
d. WHERE
La clause WHERE filtre les entités à récupérer grâce à des opérateurs de
comparaison.
Voici comment récupérer un Livre par son titre :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
return $query->getOneOrNullResult();
}
}
use Doctrine\ORM\EntityRepository;
$query->setParameter('titre', $titre);
return $query->getOneOrNullResult();
}
}
use Doctrine\ORM\EntityRepository;
$query = $this->getEntityManager()->createQuery(
'SELECT l
FROM App:Livre l
WHERE l.titre LIKE ?1 AND l.dateParution < ?2'
);
$query->setParameters(array(
1 => '%mot-clé%',
2 => new \DateTime('2005-05-10'),
));
return $query->getResult();
}
}
$entites = $query->getResult();
récupère les entités App:Entite dont la propriété valeur vaut entre 15 et 20.
$query = $this->getEntityManager()->createQuery(
'SELECT e FROM App:Entite e
WHERE e.valeur IN (5, 6, ?1)'
);
$query->setParameter(1, 10);
$entites = $query->getResult();
use Doctrine\ORM\EntityRepository;
return $query->getResult();
}
}
$utilisateurs = $query->getResult();
$utilisateurs = $query->getResult();
e. ORDER BY
La clause ORDER BY trie les entités retournées selon un ou plusieurs critères, son
utilisation est la même qu’en SQL :
SELECT e
FROM App:Entite e
ORDER BY e.propriete_1 DESC, e.propriete_2 ASC
$query->setFirstResult(5);
$query->setMaxResults(20);
return $query->getResult();
}
}
use Doctrine\ORM\EntityRepository;
$query->setFirstResult(($page - 1) * $nbParPage);
$query->setMaxResults($nbParPage);
return $query->getResult();
}
//...
}
Ici, la méthode getPage() retourne les entités à afficher pour une page donnée ; le
numéro de la page doit être passé en tant que premier argument ($page), le second est
le nombre d’entités souhaitées pour chaque page ($nbParPage).
Cependant, pour les calculer, il nous manque une information cruciale : le nombre
total d’entités. En effet, Le nombre de pages disponibles correspond au nombre total
d’entités divisé par le nombre d’entités à afficher par page, et si le résultat est un
nombre décimal, il doit être arrondi à l’entier supérieur.
Il nous faut donc tout d’abord être capables de calculer le nombre total d’entités. Pour
ce faire, nous créons une méthode au sein du repository :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
class LivreRepository extends EntityRepository
{
public function getNombredeLivres()
{
$query = $this->getEntityManager()->createQuery(
'SELECT Count(l.id) FROM App:Livre l'
);
return $query->getSingleScalarResult();
}
// ...
}
Cette requête DQL compte le nombre d’entités persistées en base de données. Vous
remarquerez que la méthode invoquée pour récupérer le résultat
est getSingleScalarResult() et non getResult(). Cette méthode transforme le résultat
(on parle d’hydratation). Ici, il est hydraté en valeur unique et scalaire : un nombre
entier.
Pagination avancée
La pagination que nous venons de créer est fonctionnelle et nous a permis de mieux
comprendre le fonctionnement des limites. Cependant, vous remarquerez rapidement
que ses fonctionnalités sont assez réduites, notamment au niveau de l’affichage. En
pratique, les systèmes de pagination ont pour la plupart besoin d’intégrer des liens
« suivant » et « dernier », ou de ne pas afficher tous les numéros de page disponibles,
mais seulement ceux des pages proches.
3. Le QueryBuilder
Une difficulté fréquemment rencontrée avec l’extension PHP PDO est la gestion des
requêtes dynamiques.
Le contenu d’une requête peut parfois varier lors de l’exécution de l’application.
Généralement, ce sont les valeurs des paramètres qui changent. PDO disposant de
requêtes préparées et de marqueurs de paramètres, il est aisé d’utiliser des paramètres
de différentes valeurs sans avoir à modifier le contenu de la requête.
Mais qu’en est-il de la structure même de la requête ? Dans certains cas, vous aurez
besoin de rajouter dynamiquement une clause WHERE ou une jointure.
Prenons l’exemple d’une méthode de repository chargée de récupérer des utilisateurs.
Optionnellement, cette dernière pourrait accepter une partie de pseudonyme à
rechercher :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
if ($pseudo) {
$dql .= ' WHERE u.pseudo LIKE :pseudo';
}
$query = $this->getEntityManager()->createQuery($dql);
if ($pseudo) {
$query->setParameter('pseudo', '%'.$pseudo.'%');
}
return $query->getResult();
}
}
L’exemple ci-dessus est fonctionnel, mais très peu lisible : la chaîne de caractères
correspondant au DQL doit être modifiée manuellement, en veillant à ne pas oublier
les espaces.
Pour ces situations, il est plutôt conseillé d’utiliser un QueryBuilder. Le
QueryBuilder est capable de générer des objets Query au travers d’une interface
orientée objet :
namespace App\Entity;
use Doctrine\ORM\EntityRepository;
return $qb->getQuery()->getResult();
}
}
Nous remarquons que le code est beaucoup plus concis et lisible. La chaîne de
caractères de la requête n’ayant pas à être générée manuellement, les erreurs de
syntaxe sont évitées.
Pour plus d’informations sur le QueryBuilder, veuillez-vous reporter à la
documentation officielle
: https://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-
builder.html
Précédent
Manipulation des entités avec l’EntityManager
Suivant
Fonctionnalités avancées
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
La gestion des événements applicatifs
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Fonctionnalités avancées
1. Les extensions Doctrine
Les extensions de Doctrine sont une fonctionnalité incontournable. Très
pratiques, elles permettent d’ajouter certains comportements à vos entités, comme par
exemple :
un slug : une propriété générée automatiquement et qui contient un identifiant
lisible pour votre entité, parfaitement adapté aux URL (nous illustrerons cette
extension par un exemple),
les traductions : le contenu de vos entités peut être traduit en plusieurs langues,
l’ajout de propriétés created et updated : des dates mises à jour
automatiquement lors de la création et de la dernière modification de l’entité.
Cette liste est non exhaustive, il existe bien d’autres extensions.
a. Installation
Ces extensions ne sont pas installées par défaut. Le
bundle DoctrineExtensionsBundle les met à votre disposition.
Pour l’installer, il suffit d’utiliser la recette Flex associée et donc d’exécuter la
commande :
composer require antishov/doctrine-extensions-bundle
stof_doctrine_extensions:
orm:
default:
sluggable: true
namespace Eni\DatabaseBundle\Entity;
/**
* Livre
*
* @ORM\Entity
*/
class Livre
{
// ...
/**
* @var string
*
* @ORM\Column(name="titre", type="string", length=250)
*/
private $titre;
// ...
/**
* @Gedmo\Slug(fields={"titre"})
* @ORM\Column(type="string", length=255, unique=true)
*/
private $slug;
$this->em->flush();
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Httpfoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/blog")
*/
class DefaultController extends AbstractController
{
/**
* @Route("/publier")
*/
public function publier(Request $request)
{
if ('POST' === $request->getMethod()) {
$article = $request->request->get('article');
// Enregistrement en base
$this->get('pdo')
->prepare('
INSERT INTO article (auteur, titre, contenu)
VALUES(:auteur, :titre, :contenu)
')->execute(array(
'auteur' => $article['auteur'],
'titre' => $article['titre'],
'contenu' => $article['contenu'],
));
// Envoi de l'e-mail
$message = \Swift_Message::newInstance()
->setSubject('Validation de ton post')
->setFrom('webmaster@example.com')
->setTo($article['auteur'])
->setBody('Merci pour le post !');
$this->get('mailer')->send($message);
Nous remarquons que le code du contrôleur est assez long ; cela ne devrait pas être le
cas, c’est ce qu’on appelle un « fat controller ». Une bonne pratique est de garder ses
contrôleurs légers, comme nous l’avons vu précédemment (cf. chapitre Routage et
contrôleur).
Nous pourrions créer un service métier permettant de gérer les articles, et y ajouter
une méthode dont le contenu serait l’exécution des trois actions (persistance en base
de données, envoi de l’e-mail, rédaction du tweet), mais nous allons plutôt réaliser
cela grâce au répartiteur d’événements :
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Httpfoundation\Request;
$this->get('event_dispatcher')->dispatch(
'new_post', new GenericEvent($article)
);
}
return $this->render('default/blog.html.twig');
}
}
Le code de notre action est maintenant beaucoup plus léger, mais il n’est pas encore
fonctionnel. L’action ne fait que « dispatcher » un événement new_post représenté
par un objet de type GenericEvent.
L’objet GenericEvent est la représentation orientée objet de l’événement ; il contient
des informations sur l’événement qui a été déclenché (ici un article).
database_service:
class: App\Service\DatabaseService
arguments:
- '@pdo'
tags:
- { name: kernel.event_listener, event: new_post, method: onNewPost }
mailer_service:
class: App\Service\MailerService
arguments:
- '@mailer'
tags:
- { name: kernel.event_listener, event: new_post, priority: -5 }
twitter_service:
class: App\Service\TwitterService
arguments:
- '@twitter'
tags:
- { name: kernel.event_subscriber }
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
// Création du tweet
$tweet = 'Nouvel article sur le blog : '
. $article['titre']
;
$this->api->post(
'statuses/update',
array('status' => $tweet)
);
}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
2. Applications
Grâce à l’événement kernel.terminate, vous pouvez par exemple retarder l’exécution
de tâches lourdes après le renvoi de la réponse, et non pendant, de manière à ne pas
pénaliser les performances de votre site. Dans ce cas-là, les tâches lourdes ne peuvent
pas avoir d’impact sur le contenu de la réponse HTTP renvoyée au client, car, au
moment où elles sont exécutées, la réponse est déjà envoyée.
Par défaut, Symfony (plus précisément SwiftMailerBundle) utilise cet événement
pour, entre autres, envoyer des e-mails. Lorsque vous en envoyez un depuis un
contrôleur, Symfony garde le message dans une file d’attente, pour effectuer l’envoi
concret seulement lors de l’événement kernel.terminate. Reportez-vous à l’annexe
Envoyer des e-mails grâce à SwiftMailer pour plus d’informations.
Voici un contrôleur dont l’exécution serait assez longue :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends AbstractController
{
/**
* @Route("/")
*/
public function tacheLourde()
{
sleep(5); // tâche lourde
Ici, nous utilisons la fonction PHP sleep() pour simuler une action lourde. Lorsque
cette action sera invoquée, l’utilisateur attendra au minimum cinq secondes devant
son navigateur, ce qui pourrait être évité en utilisant une tâche asynchrone.
Pour cela, il faut tout d’abord extraire le code lourd au sein d’un listener :
namespace App\Listener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
Dorénavant, la réponse sera envoyée très rapidement à l’utilisateur, tandis que la tâche
lourde sera effectuée après coup.
Précédent
Concepts et écoute d’événement applicatifs
Suivant
Les événements de la console
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Prérequis
2. Les événements
Quiz
Les formulaires
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
$commande = $event->getCommand();
$output->writeln(sprintf(
'La commande "%s" va être exécutée.',
$commande->getName()
));
}
$event->getOutput()->writeln(sprintf(
'Une exception a été lancée : "%s".',
$exception->getMessage()
));
}
Le listener peut être enregistré de la même manière que pour les événements du
Kernel :
# Fichier config/services.yaml
services:
my_event_subscriber:
class: Eni\DemoBundle\Listener\ConsoleListener
tags:
- { name: kernel.event_subscriber }
Dorénavant, pour toute commande que vous exécuterez, un message s’affichera lors
de chaque événement de la Console.
Précédent
Les événements du Kernel
Suivant
Quiz
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le modèle
2. Le contrôleur
3. La vue
Fonctionnement du composant
Les types de champs de formulaire
Créer des formulaires réutilisables
Validation des données
Personnaliser le rendu - thèmes de formulaires
Quiz
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Un composant MVC
Les formulaires sont des éléments indispensables aux sites web : c’est le principal
moyen par lequel les utilisateurs interagissent avec l’application.
Ils sont affichés dans des pages (couche Vue) et, une fois soumis, ils sont
généralement utilisés pour modifier des données (couche Modèle), tout ceci étant
orchestré par le contrôleur.
On retrouve donc les protagonistes de notre fameux modèle de conception MVC (cf.
Architecture du framework - Le modèle de conception MVC).
Cette particularité fait du composant « Form » de Symfony non pas le plus difficile à
prendre en main, mais l’un des plus complets. Aussi, pour mieux appréhender ce
chapitre, il convient de maîtriser le Contrôleur et Twig.
1. Le modèle
Dans leur utilisation la plus fréquente, les formulaires permettent d’interagir avec la
couche Modèle (bien que le composant puisse fonctionner avec des tableaux). Les
« cibles » des formulaires sont donc des objets.
Imaginons une classe représentant un client :
namespace App\Model;
class Client
{
private $nom;
private $dateDeNaissance;
return $this;
}
return $this;
}
C’est une classe PHP classique, mais elle pourrait très bien correspondre à une entité
Doctrine si jamais son mappage était défini.
Un des buts du composant « Form » sera de créer ou de modifier des instances de
cette classe, en fonction de données transmises par l’utilisateur, via un formulaire.
2. Le contrôleur
Le contrôleur, en tant que point de relais, est chargé de :
créer ou récupérer l’objet de la couche Modèle,
créer le formulaire et lui faire connaître cet objet de la couche Modèle, dont il
permettra la modification,
passer le formulaire à la vue pour que celle-ci s’occupe de l’affichage des
différents champs,
valider, une fois le formulaire soumis par le client, les données, de manière à
pouvoir afficher des messages d’erreur si besoin est (par exemple « Ce champ est
obligatoire », « Ce n’est pas une adresse e-mail valide », etc.).
Voici le code au sein d’une action (il n’est pas nécessaire que vous le compreniez en
détail pour le moment, nous reviendrons bien évidemment sur le sujet plus loin dans
ce chapitre) :
/**
* @Route("/")
*/
public function index(Request $request)
{
$client = new Client();
$form = $this->createFormBuilder($client)
->add('nom', TextType::class)
->add('date_de_naissance', BirthdayType::class)
->add('valider', SubmitType::class)
->getForm()
;
$form->handleRequest($request);
return $this->render('default/index.html.twig',
[ 'form' => $form->createView() ]
);
}
3. La vue
À la fin de notre action, nous envoyons notre formulaire à la vue, au sein d’une
variable form. Le template a pour objectif de mettre en forme ce formulaire, en
affichant ses différents champs.
On notera que l’objet formulaire n’est pas envoyé tel quel, mais qu’il faut invoquer la
méthode $form->createView(), cette méthode transformant le formulaire en un objet
spécialement adapté aux templates.
Au niveau du template, l’affichage du formulaire est d’une simplicité déconcertante :
<html>
<body>
{{ form(form) }}
</body>
</html>
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Tout déplier | Tout replier
Informations générales
Introduction
Mise en place d’un projet Symfony
Architecture du framework
Le modèle de conception MVC
Architecture de Symfony
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
Fonctionnement du routage dans Symfony
Définition des routes
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Le modèle de conception IoC : Inversion Of Control
L’injection de dépendances
Le Service Container
Créer un service et configurer ses injections
Le chargement automatique de services
Créer des services réutilisables et distribuables
Quiz
Les templates avec Twig
Présentation et concepts
Twig
Les gabarits de pages (layouts) et les blocks
Le langage Twig
Les filtres et les fonctions
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Quiz
Accéder aux bases de données avec Doctrine
Présentation et concepts
Installer et configurer Doctrine
Définition des entités et de leur mapping
Manipulation des entités avec l’EntityManager
Récupérer des entités
Fonctionnalités avancées
Quiz
La gestion des événements applicatifs
Concepts et écoute d’événement applicatifs
Les événements du Kernel
Les événements de la console
Quiz
Les formulaires
Un composant MVC
Fonctionnement du composant
1. L’objet « Form »
a. Soumission
b. Validation
c. Vue
2. Les types
3. Les options
4. Les objets « Form » et « FormBuilder »
a. Le FormBuilder
b. Structure de l’objet Form
5. Association avec l’objet de la couche Modèle
6. Formulaires sans objet
7. La représentation des valeurs
a. Transformation des données
b. Illustration avec le type date
Fonctionnement du composant
1. L’objet « Form »
L’objet Form est le principal élément utilisé au niveau du contrôleur. Dans l’exemple
ci-dessus, il est contenu dans la variable $form. Il représente l’ensemble des champs
du formulaire, sous forme hiérarchique.
C’est le point central autour duquel tout s’articule. L’objet représentant la
requête HTTP ou l’objet de la couche Modèle y est « injecté ». Les tâches de
traitement classiques (telles que la gestion de la soumission, la validation ou la
création de la vue du formulaire) se font également au travers de cet objet Form.
a. Soumission
L’objet Form comporte une méthode handleRequest(), prenant en paramètre
l’objet Request (la requête HTTP courante). Cette méthode est capable de
l’introspecter.
De cette introspection naîtra une conclusion, à savoir, une réponse à la
question suivante : est-ce que l’utilisateur, lors de la requête HTTP courante, est en
train de soumettre le formulaire, ou est-il seulement en train de l’afficher ?
S’il est en train de le soumettre (généralement cette action est reconnaissable par le
fait que la méthode de la requête HTTP courante est de type POST), les données
postées sont rattachées au formulaire et l’objet de la couche Modèle est également mis
à jour (dans la mesure où les données saisies par l’utilisateur sont valides, nous y
reviendrons plus tard).
Par défaut, invoquer la méthode handleRequest() serait équivalent à :
if ($request->isMethod('POST')) {
$form->submit($request);
}
2. Les types
Les classes telles TextType ou SubmitType que nous avons utilisées précédemment
correspondent aux types des champs de notre formulaire. Nous les référençons avec
leur FQCN grâce au mot-clé ::class (disponible depuis PHP 5.5).
3. Les options
Lors de la configuration des différents champs du formulaire, un troisième
argument de la méthode add() peut être utilisé pour passer des options, au travers
d’un tableau.
Ces options sont utilisées pour personnaliser le champ : changer son label, sa valeur
par défaut ou son rendu sont autant de possibilités offertes par les options.
Illustrons ceci avec un exemple concret. Imaginons que le
champ date_de_naissance soit restreint aux jeunes de 7 à 77 ans :
$form = $this->createFormBuilder($client)
->add('nom', TextType::class)
->add('date_de_naissance', BirthdayType::class, array(
'years' => range(date('Y') - 77, date('Y') - 7)
))
->add('valider', SubmitType::class)
->getForm()
;
Nous utilisons pour ceci l’option years, qui accepte un tableau de valeurs
correspondant aux années disponibles pour le champ.
Il y a des options communes à tous les types, tandis que d’autres sont spécifiques à un
type de champ donné. Ici, par exemple, l’option years est spécifique aux champs
relatifs à la gestion des dates (elle n’aurait pas de sens pour un champ de
type textarea). La liste complète des options disponibles pour chaque type est
détaillée plus loin.
En répétant cette technique sur l’objet Form enfant, nous pouvons accéder à
n’importe quel nœud de l’arborescence :
$noeudJour = $form->get('date_de_naissance')->get('day');
$form->handleRequest($request);
if ($form->isSubmitted()) {
$donnees = $form->getData();
// Traitement du tableau contenant les données soumises...
}
$form->handleRequest($request);
if ($form->isSubmitted()) {
$donnees = $form->get('ma_date')->getData();
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. L’héritage
2. FormType
a. label
b. label attr
c. data
d. required
e. disabled
f. mapped
g. property_path
h. attr
i. trim
j. error_bubbling
3. TextType
4. PasswordType
5. RepeatedType
a. type
b. first_options et second_options
c. options
d. first_name
e. second_name
f. invalid_message
6. ChoiceType
a. choices
b. expanded et multiple
c. placeholder
d. preferred_choices
e. Types similaires
7. EntityType
a. class
b. choice_label
c. query_builder
d. group_by
e. em
8. DateType
a. widget
b. format
c. model_timezone
d. view_timezone
e. years
f. months
g. days
h. placeholder
i. Types similaires
9. FileType
a. multiple
b. Récupérer les fichiers
c. Traiter les fichiers
10. CheckboxType
11. SubmitType, ResetType et ButtonType
1. L’héritage
Avant d’aborder les différents types ainsi que leurs options, il est nécessaire de
comprendre l’héritage de ceux-ci.
Chaque type peut hériter d’un parent. En pratique, la quasi-totalité des types utilisés
avec Symfony héritent du même type : FormType. Nous avons déjà utilisé ce type
indirectement : le « nœud » principal de la hiérarchie du formulaire est par défaut de
type FormType.
Au cours de cette section dédiée aux types, le parent de chacun de ceux-ci sera
spécifié. Cela vous permettra de retrouver la totalité de leurs options. Si un type B a
comme parent A, toutes les options de A sont alors disponibles lors de l’utilisation
de B.
2. FormType
C’est un type plutôt abstrait, contenant des options de configuration assez générales.
La plupart du temps, nous ne l’utilisons pas directement, mais au travers de ses
nombreux sous-types.
a. label
Cette option indique le label à utiliser pour le champ. Si cette option n’est pas
renseignée, le framework fait de son mieux pour « deviner » le label ; par exemple, un
champ nommé date_de_naissance aura le label Date de naissance.
b. label attr
En passant un tableau associatif à cette option, vous pouvez définir les attributs
HTML pour la balise <label>.
c. data
Cette option permet de modifier la valeur par défaut du champ.
Par exemple, pour préremplir un champ de type text (il hérite de form) avec une
valeur donnée, il suffit de configurer le type de cette manière :
$formBuilder->add('champ', TextType::class, array(
'data' => 'Valeur par défaut'
));
d. required
Si cette option vaut true, l’attribut required est ajouté au champ.
Cet attribut HTML5 spécifie que le champ en question doit obligatoirement être
renseigné et tant qu’il ne l’est pas, l’utilisateur ne peut soumettre le formulaire et voit
un message d’erreur. Ce comportement est géré nativement par le navigateur.
La validation HTML5 est un « complément » à la validation côté serveur,
évoquée plus loin dans ce chapitre. Elle ne doit pas être utilisée en tant que solution
de remplacement d’une validation serveur.
e. disabled
Lorsque cette option est à true, le framework ajoute un attribut disabled au champ.
Symfony ignore alors complètement le champ, c’est comme s’il n’existait pas : il
n’est pas validé mais uniquement affiché dans la vue.
f. mapped
Cette option vaut true par défaut. Si elle vaut false, le framework n’associera pas le
champ à l’objet de la couche Modèle.
Cette option est particulièrement utile, par exemple pour les champs « CAPTCHA »
ou la case à cocher pour accepter des conditions générales, qui contiennent des
informations qui ne sont pas destinées à être persistées de manière durable dans la
couche Modèle.
g. property_path
Cette option définit la manière dont les données du champ sont lues/injectées dans
l’objet de la couche Modèle.
Par défaut, cette option prend la valeur du nom du champ. Si cette option (ou le nom
du champ, donc) vaut date_de_naissance, par exemple, le framework s’attend à
interagir avec l’objet de la couche Modèle avec l’une de ces techniques (par ordre de
priorité) :
Au travers d’une propriété publique date_de_naissance.
Via les méthodes
publiques getDateDeNaissance(), isDateDeNaissance() ou hasDateDeNaissanc
e() pour la lecture, et pour l’écriture via la méthode setDateDeNaissance().
Enfin, les méthodes magiques PHP __call(), __get()et __set().
h. attr
Cette option est utilisée pour ajouter des attributs HTML au champ. Elle se présente
sous forme de tableau dont les clés correspondent aux noms des attributs et les valeurs
aux valeurs des attributs.
Une classe (au sens HTML) peut donc facilement être ajoutée à un champ :
$formBuilder->add('champ', TextType::class, array(
'attr' => array('class' => 'ma_classe')
));
i. trim
La fonction trim() de PHP permet d’effacer les espaces en début et fin de chaîne de
caractères. Quand cette option vaut true (ce qui est le cas par défaut), cette fonction
est appliquée aux données provenant du champ en question.
j. error_bubbling
Cette option est liée à la validation des données, évoquée plus loin dans ce chapitre.
Définie à true, cette option a pour effet de rattacher les éventuelles erreurs du nœud à
son parent. Elle est généralement utilisée pour afficher les erreurs en haut du
formulaire et non près du champ comportant l’erreur.
3. TextType
Ce type hérite de FormType. Il est utilisé pour représenter un input de type text.
L’exemple ci-dessous crée un champ text acceptant uniquement des
caractères alphanumériques.
$formBuilder->add('champ', TextType::class, array(
'pattern' => '[a-zA-Z0-9]*'
));
4. PasswordType
Le type password hérite de FormType. Il permet d’afficher des champs relatifs à la
saisie de mots de passe.
La particularité de ce type est qu’il cache la valeur entrée avec des puces noires.
Son autre spécificité est de ne jamais préremplir le champ avec la valeur soumise par
l’utilisateur. Dans le cas d’un formulaire d’inscription, par exemple, si l’utilisateur
soumet le formulaire avec un champ invalide, la page est réaffichée avec un message
d’erreur près du champ en question, mais les autres champs auront gardé les valeurs
soumises, à l’exception des champs de type password.
Ce comportement peut être modifié grâce à l’option always_empty, en la mettant
à false.
5. RepeatedType
Ce type duplique un champ et attend de l’utilisateur qu’il saisisse la même valeur dans
les deux.
Le principal cas d’utilisation est pour les mots de passe ou adresses e-mail : lors de
l’inscription d’un utilisateur, il lui est souvent demandé de confirmer la valeur saisie,
de manière à éviter les erreurs dues aux fautes de frappe.
a. type
Cette option configure le type des deux champs. L’exemple suivant affiche deux
champs de type password :
$builder->add('mot_de_passe', RepeatedType::class, array(
'type' => PasswordType::class,
));
b. first_options et second_options
Ces options permettent de passer respectivement les options au premier et
second champ. Grâce à celles-ci, nous pouvons configurer le label de chaque champ :
$builder->add('mot_de_passe', RepeatedType::class, array(
'type' => PasswordType::class,
'first_options' => array('label' => 'Mot de passe'),
'second_options' => array('label' => 'Confirmer'),
));
c. options
S’il existe des options communes aux deux champs, il convient de les passer par
l’option options, plutôt que de les dupliquer.
d. first_name
Cette option permet de définir le nom du premier champ.
e. second_name
Cette option permet de définir le nom du second champ.
f. invalid_message
Lorsque les valeurs des deux champs sont différentes, le message d’erreur affiché
peut être personnalisé grâce à cette option.
6. ChoiceType
Le type choice hérite de FormType. Il est utilisé pour générer des
champs select, radio ou checkbox.
Un champ choice contient donc une liste de données. L’utilisateur peut en choisir une,
ou éventuellement plusieurs.
a. choices
Cette option doit contenir les valeurs présentes dans la liste : les choix.
Ces choix sont sous la forme d’un tableau associatif. La clé correspond à la
valeur envoyée dans la requête HTTP, tandis que la valeur correspond au label affiché
à l’utilisateur.
$formBuilder->add('categorie_produit', ChoiceType::class, array(
'choices' => array(
'tv_em' => 'Télévisions et électroménager',
'jv' => 'Jeux vidéo',
'livres' => 'Livres et BD',
)
));
L’exemple ci-dessus affiche un champ de type select, dont les options ont pour
valeurs tv_em, jvet livres, et comme labels, respectivement, Télévisions et
électroménager, Jeux vidéo et Livres et BD.
b. expanded et multiple
Le type de champ affiché dépend des options expanded et multiple :
multiple : cette option permet de définir si l’utilisateur peut sélectionner plusieurs
valeurs. Mise à true, cette option générera soit un select multiple, soit une liste de
checkboxes (en fonction de l’option expanded).
expanded : si cette option vaut false (valeur par défaut), le champ généré est de
type select, selon la valeur de l’option multiple, c’est soit une liste de cases à
cocher, soit une liste de boutons radio.
Voici un tableau récapitulatif :
c.
multiple expanded champ
placeholder
Cette option spécifie si, dans le cas d’un champ select, une option spéciale doit
apparaître en début de liste, avec un label de type « Choisissez une valeur », par
exemple.
$formBuilder->add('categorie_produit', ChoiceType::class, array(
'choices' => array(
'tv_em' => 'Télévisions et électroménager',
'jv' => 'Jeux vidéo',
'livres' => 'Livres et BD',
),
'empty_value' => 'Choisir une catégorie',
));
d. preferred_choices
Toujours dans le cas d’un champ select, cette option, contenant un tableau de choix
prioritaires, affiche ceux-ci en début de liste, les rendant plus visibles pour
l’utilisateur.
Un cas d’utilisation courant pourrait être les listes de pays. Il est ainsi possible de
placer le pays retrouvé grâce à l’adresse IP de l’utilisateur en début de liste, pour
faciliter le remplissage du formulaire.
$formBuilder->add('categorie_produit', ChoiceType::class, array(
'choices' => array(
'US' => 'Etats-Unis',
'FR' => 'France',
'IN' => 'Inde',
'VE' => 'Venezuela',
// ...
),
'preferred_choices' =>
array(geoip_country_code_by_name($userIp)),
));
e. Types similaires
Quelques types définis au sein du framework héritent
de choice : country, language, locale, timezone et currency.
La seule différence avec le type choice est que ces derniers renseignent
l’option choices, avec, comme leur nom l’indique, une liste de pays, ou de langues,
etc.
7. EntityType
entity hérite de choice, c’est un type spécifique à Doctrine.
Contrairement à choice, où c’est une valeur scalaire (ou un tableau de valeurs
scalaires) qui est récupérée, le type entity récupère des entités.
a. class
Cette option permet de spécifier la classe de l’entité de la liste. Les raccourcis de
classes sont autorisés (par exemple, App:MonEntite).
b. choice_label
Cette option indique le property_path (cf. options du type form) qui doit être affiché
en tant que label pour chaque entrée de la liste.
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
Si cette option n’est pas renseignée, le framework pourra « deviner » le label à utiliser
automatiquement, à condition que la méthode magique __toString() soit définie au
sein de la classe.
c. query_builder
Cette option prend comme argument une fonction anonyme, qui aura à sa disposition
le repository de l’entité quand elle sera invoquée par le framework.
Son rôle est de retourner la liste des entités disponibles pour remplir la liste
déroulante. Par défaut, toutes les entités existantes en base de données sont présentes
dans la liste déroulante.
use Doctrine\ORM\EntityRepository;
d. group_by
Dans le cas d’un champ select, cette option permet de regrouper les choix par
sections. Des balises HTML optgroup seront alors générées.
À l’instar de l’option property, la valeur de l’option group_by doit être
un property_path.
e. em
Cette option permet de spécifier l’EntityManager à utiliser pour récupérer les entités,
en passant son nom explicitement.
Cette option est destinée aux utilisateurs avancés. Dans l’immense majorité des cas, il
n’y a nul besoin de la renseigner.
8. DateType
Ce champ permet la saisie de dates.
a. widget
Au niveau du rendu, un champ date se traduit au choix par trois éléments select (jour,
mois et année), trois champs texte (également pour jour, mois et année) ou un seul
champ texte, où la date devra être saisie manuellement sous un format donné.
L’option widget définit ce rendu ; les valeurs possibles sont :
choice (défaut) : affiche trois éléments select (listes déroulantes),
text : affiche trois champs texte,
single_text : affiche un seul champ texte.
b. format
Cette option gère le format d’affichage du widget.
Pour un widget de type choice ou text, il s’agit principalement de configurer l’ordre
d’affichage des éléments. La valeur dMy affiche par exemple le champ pour le jour,
puis le mois, et enfin l’année. L’affichage peut être ainsi rapidement personnalisé : la
valeur d - M - y, par exemple, sépare les différents champs avec un tiret et des
espaces.
Dans le cas d’un widget de type single_text, cette option spécifie la mise en forme
des données attendue dans le champ texte. Si l’option est placée à d-M-y HH:mm:ss,
par exemple, l’utilisateur devra saisir une valeur de type 18-06-2014 14:58:05.
Pour des exemples détaillés sur l’utilisation des formats, veuillez vous référer à la
documentation suivante : https://unicode-org.github.io/icu/userguide/icu/
c. model_timezone
Cette option permet de spécifier le fuseau horaire utilisé pour la représentation Model
data.
Les fuseaux horaires admis sont les mêmes que ceux de PHP, référencés à l’adresse
suivante : http://php.net/manual/fr/timezones.php
d. view_timezone
Cette option permet de spécifier le fuseau horaire utilisé pour la représentation View
data. En d’autres termes, c’est le fuseau horaire dans lequel l’utilisateur saisit les
données.
Cette option est utile lorsque le fuseau horaire de l’utilisateur est différent de celui du
serveur web, par exemple.
e. years
Dans le cas de listes déroulantes, cette option spécifie les années à afficher.
Par défaut, une période de dix années est disponible, avec l’année courante à égale
distance de la première et de la dernière.
Pour générer facilement les années à afficher, vous pouvez utiliser la fonction
range : range(2010, 2020) par exemple.
f. months
Dans le cas de listes déroulantes, cette option spécifie les mois à afficher.
g. days
Dans le cas de listes déroulantes, cette option spécifie les jours à afficher.
h. placeholder
Pour le widget choice (listes déroulantes), cette option ajoute une option sans valeur
en début de liste.
Elle peut être contrôlée individuellement pour chaque élément :
$builder->add('ma_date', DateType::class, array(
'placeholder' => array(
'day' => 'Jour',
'month' => 'Mois',
'year' => 'Année',
),
));
i. Types similaires
Quelques autres types permettant de gérer les dates sont semblables au
type date : BirthdayType, DatetimeType et TimeType.
9. FileType
Cette option permet de générer un champ capable de sélectionner et de charger des
fichiers.
Ce champ est assez simple au niveau du rendu et il est différent des autres au niveau
de l’utilisation des données soumises. En effet, tandis que pour la plupart des champs,
la valeur soumise est injectée dans l’objet de la couche Modèle, ici, il n’y a pas de
valeur concrète en tant que tel, mais un envoi de fichier.
a. multiple
Cette option accepte une valeur de type boolean. Lorsqu’elle est placée à true,
l’utilisateur a la possibilité de charger plusieurs fichiers via le champ. Cela se fait
grâce à l’attribut HTML5 multiple.
b. Récupérer les fichiers
La valeur utilisée par le framework est un objet spécial. Cet objet regroupe les
différentes informations sur le ou les fichiers chargés par l’utilisateur.
Il est de type Symfony\Component\HttpFoundation\File\UploadedFile.
Vous pouvez le récupérer en invoquant la
méthode getData() (ou getNormData(), car, ici, les
représentations Norm et Model sont identiques) du nœud représentant le champ
fichier :
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
$form->handleRequest($request);
if ($form->isSubmitted()) {
$fichier = $form->get('fichier')->getNormData();
// traitement du fichier...
}
return $this->render('default/index.html.twig',
[ 'form' => $form->createView() ]
);
}
Nous verrons plus loin, au cours de la section sur la validation, comment valider
automatiquement un fichier.
Enregistrer un fichier
Lors du chargement de fichiers, PHP les traite en tant que fichiers « temporaires ». Ils
sont placés dans un répertoire volatile et il revient à l’application de les enregistrer
définitivement au sein d’un répertoire donné :
if ($fichier->isValid()) {
$fichier->move('/chemin/vers/repertoire', 'nom_du_fichier');
}
10. CheckboxType
Ce champ correspond à une seule case à cocher. La valeur récupérée dans la couche
Modèle est de type boolean.
$builder->add('email_partenaires', CheckboxType::class, array(
'label' => 'Acceptez-vous de recevoir des e-mails de nos
partenaires ?',
'required' => false,
));
Dans certains cas, le champ n’a pas de correspondance au sein de l’objet de la couche
Modèle. Pour les conditions d’utilisation d’un site, par exemple, les utilisateurs étant
obligés de cocher cette case pour pouvoir s’inscrire, il convient de placer
l’option mapped à false.
À première vue, cette technique paraît inutile. En effet, nous savons que la
méthode isSubmitted() de l’objet Form principal permet de savoir s’il a été soumis
ou non.
Cependant, dans certains cas, les formulaires peuvent avoir plusieurs boutons submit.
Un exemple concret est la rédaction de messages : l’utilisateur, au lieu de l’envoyer
directement, pourrait l’enregistrer en brouillon pour continuer plus tard. Il y aurait
donc deux boutons distincts (« Envoyer » et « Enregistrer en brouillon ») :
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
$form->handleRequest($request);
if ($form->isSubmitted()) {
$message = $form->getData();
if ($form->get('envoyer')->isClicked()) {
// envoi du message...
} else {
// enregistrement du brouillon
}
}
return $this->render('default/index.html.twig',
[ 'form' => $form->createView() ]
);
}
Précédent
Fonctionnement du composant
Suivant
Créer des formulaires réutilisables
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
getParent
Non utilisée dans l’exemple précédent, cette méthode retourne le nom du type dont
doit hériter le type courant. Par défaut, elle retourne form.
La classe ci-dessous contient un type ma_date héritant de date :
// src/Form/Type/MaDateType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
Ici, le type ma_date hérite de date mais n’apporte aucune modification à ce dernier.
Les deux types sont identiques.
L’héritage d’un type doit toujours se faire avec la méthode getParent() et non en PHP
via l’héritage classique (par exemple MaDateType extends DateType).
configureOptions
La méthode configureOptions() est utilisée pour gérer les différentes options du
formulaire.
Le framework l’invoque avec en argument un objet implémentant Symfony\
Component\OptionsResolver\OptionsResolverInterface.
Le composant OptionsResolver est capable de gérer les options. Il offre une
multitude de possibilités (normalisation, valeurs autorisées, etc.), et le
composant Form l’intègre pour la configuration des options des types.
En pratique, vous l’utiliserez principalement pour :
changer les valeurs par défaut de certaines options,
créer vos propres options.
On pourrait qualifier le composant OptionsResolver comme étant une version
« allégée » de la configuration avec l’injection de dépendances (cf. L’injection de
dépendances - Les extensions de bundle) ; ils ont sensiblement les mêmes objectifs.
L’exemple ci-dessous modifie la valeur par défaut de l’option years et ajoute une
option use_jquery pour le type ma_date :
// src/Form/Type/MaDateType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
$resolver->setNormalizers(array(
'widget' => function (Options $options, $value) {
if ($options['use_jquery']) {
return 'single_text';
}
return $value;
}
));
}
Ici, les années sélectionnables seront restreintes aux années courante, précédente et
suivante.
L’option use_jquery est également disponible et on pourrait imaginer que, mise
à true, elle permette d’activer un plug-in jQuery pour la sélection d’une date sur un
champ texte (par exemple, jQuery UI datepicker).
C’est ici qu’intervient la normalisation. Son rôle est de transformer la valeur de
certaines options si cela est nécessaire. Dans l’exemple ci-dessus, l’option widget du
type est normalisée en fonction de l’option use_jquery. Si cette dernière vaut true,
l’option widget est placée à single_text car le plug-in jQuery doit s’appliquer sur un
champ de type texte.
buildForm
C’est la méthode incontournable d’une classe représentant un type. Elle permet de
configurer les champs (ou nœuds) du formulaire.
Le premier argument passé par le framework est un FormBuilder, le second est un
tableau contenant les options du formulaire.
// src/Form/Type/ClientType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
Ici, pour un widget single_text (champ unique), si l’option use_jquery est égale
à true, nous plaçons la variable type à text.
$form->handleRequest($request);
return $this->render('default/index.html.twig',
['form' => $form->createView() ]
);
}
Le service doit comporter le tag form.type. Il peut ensuite être utilisé comme ceci :
/**
* @Route("/")
*/
public function index(Request $request)
{
$client = new Client;
$form = $this->createFormBuilder($client)
->add('nom', TextType::class)
->add('date_de_naissance', 'ma_date', array(
'use_jquery' => true,
))
->add('valider', 'submit')
->getForm()
;
$form->handleRequest($request);
return $this->render('default/index.html.twig',
['form' => $form->createView() ]
);
}
Avec cette technique, votre type de formulaire est utilisé de la même manière que les
types de Symfony : en le référençant par son alias (ma_date dans l’exemple ci-
dessus).
Précédent
Les types de champs de formulaire
Suivant
Validation des données
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Objectifs
2. La définition des contraintes de validation
a. Ajout des contraintes lors de la configuration d’un formulaire
b. Ajout des contraintes sur l’objet associé au formulaire
c. Les différents formats de configuration
d. Les options
3. Les contraintes et leurs options
a. NotBlank et NotNull
b. IsNull et Blank
c. IsTrue, IsFalse
d. Type
e. Email, Url et Ip
f. Regex
g. Length, Count
h. Range
i. Comparaisons
j. Dates
k. File
l. Image
m. Choice
n. UniqueEntity
o. Données financières
p. Callback
q. All
r. Valid
4. Groupes de validation
5. Validation d’un objet hors du contexte d’un formulaire
...
return $this->render('default/index.html.twig',
['form' => $form->createView() ]
);
}
/**
* Message
*
* @ORM\Table()
* @ORM\Entity
*/
class Message
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @NotBlank()
* @Email()
* @ORM\Column(name="destinataire", type="string", length=255)
*/
private $destinataire;
/**
* @var string
*
* @ORM\Column(name="titre", type="string", length=255)
*/
private $titre;
/**
* @var string
*
* @NotBlank()
* @ORM\Column(name="message", type="text")
*/
private $message;
/**
*
* @File()
* @var UploadedFile
*/
private $piece_jointe;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set destinataire
*
* @param string $destinataire
* @return Message
*/
public function setDestinataire($destinataire)
{
$this->destinataire = $destinataire;
return $this;
}
/**
* Get destinataire
*
* @return string
*/
public function getDestinataire()
{
return $this->destinataire
}
/**
* Set titre
*
* @param string $titre
* @return Message
*/
public function setTitre($titre)
{
$this->titre = $titre;
return $this;
}
/**
* Get titre
*
* @return string
*/
public function getTitre()
{
return $this->titre;
}
/**
* Set message
*
* @param string $message
* @return Message
*/
public function setMessage($message)
{
$this->message = $message;
return $this;
}
/**
* Get message
*
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* Set pieceJointe
*
* @param UploadedFile $piece_jointe
* @return Message
*/
public function setPieceJointe($piece_jointe)
{
$this->piece_jointe = $piece_jointe;
return $this;
}
/**
* Get pieceJointe
*
* @return UploadedFile|null
*/
public function getPieceJointe()
{
return $this->piece_jointe;
}
}
Ici, les contraintes sont utilisées en tant qu’annotations au-dessus des propriétés.
Cette technique est semblable au mapping de Doctrine2. La seule différence est que
ces annotations n’ont pas le même but : les unes gèrent la structure d’une table en
base de données alors que les autres sont liées à la validation.
Ensuite, la création du formulaire se retrouve fortement allégée :
use Symfony\Component\HttpFoundation\Request;
use App\Entity\Message;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
...
$form->handleRequest($request);
return $this->render('default/index.html.twig',
['form' => $form->createView() ]
);
}
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
class Message
{
/**
* @Email()
* @NotBlank()
*/
private $destinataire;
// ....
}
class Message
{
/**
* @Assert\Email()
* @Assert\NotBlank()
*/
private $destinataire;
// ....
}
App\Entity\Message:
properties:
destinataire:
- NotBlank: ~
- Email: ~
XML
Pour le format XML, le principe est le même que pour le format YAML ; le
fichier doit être créé au même endroit et être nommé validation.xml.
<!-- config/validator/validation.xml -->
<class name="App\Entity\Message">
<property name="destinataire">
<constraint name="NotBlank" />
<constraint name="Email" />
</property>
</class>
</constraint-mapping>
PHP
Pour définir les contraintes en PHP, il faut créer la
méthode loadValidatorMetadata() au sein de la classe contenant les données à
valider.
Cette méthode est invoquée par le framework avec un objet spécial en paramètre. Cet
objet permet, entre autres, d’ajouter des contraintes grâce à sa
méthode addPropertyConstraint(). Le premier argument est le nom de la propriété,
le second est la contrainte.
namespace App\Entity;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
class Message
{
public $destinataire;
d. Les options
Les contraintes de validation ne sont pas des règles figées car des options
permettent de modifier leur comportement.
L’option commune à toutes les contraintes est le message d’erreur utilisé en cas de
valeur incorrecte. Le framework possédant un inventaire de messages d’erreur, vous
n’aurez heureusement pas à spécifier ce message à chaque fois, mais seulement si
vous souhaitez modifier ou apporter plus de précision au message par défaut.
Il existe ensuite des options spécifiques. Dans le cas d’une contrainte sur la longueur
d’un pseudo par exemple, il n’y a pas de règle absolue, universelle ou incontestée
pour la règle à utiliser, certains sites web limitont la longueur à 3 à 20 caractères, et
d’autres, plus stricts, utilisont une fourchette de 5 à 15 caractères. La contrainte a
donc une option pour modifier cette limite.
Voyons comment configurer les options pour une contrainte avec les différents
formats. Ici, nous configurons la contrainte Length avec les options min et max.
Annotations
namespace App\Entity;
class Message
{
/**
* @Assert\Length(min="3", max="100")
*/
private $titre;
// ....
}
YAML
# config/validator/validation.yml
App\Entity\Message:
properties:
titre:
- Length:
min: 3
max: 100
XML
<!-- config/validator/validation.xml -->
<class name="App\Entity\Message">
<property name="titre">
<constraint name="Length">
<option name="min">5</option>
<option name="max">100</option>
</constraint>
</property>
</class>
</constraint-mapping>
PHP
namespace App\Entity;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
class Message
{
public $destinataire;
a. NotBlank et NotNull
Ces deux contraintes vérifient que la valeur n’est pas vide (NotBlank) ou nulle
(NotNull).
La contrainte NotNull est plus restrictive que NotBlank,
car NotBlank implique NotNull mais l’inverse n’est pas vrai.
En pratique, en utilisant les formulaires, vous n’aurez souvent pas à faire cette subtile
distinction entre une valeur vide et une valeur nulle. Avec un champ de type text, par
exemple, si l’utilisateur soumet le formulaire sans aucune valeur pour ce champ, le
framework utilisera null en Model data.
Utilisation (annotation)
namespace App\Entity;
class Message
{
/**
* @Assert\NotBlank(message="Le destinataire doit être
* renseigné".)
*/
protected $destinataire;
}
b. IsNull et Blank
Ces contraintes s’assurent que la valeur est vide (Blank) ou nulle (IsNull).
c. IsTrue, IsFalse
IsTrue et IsFalse valident les valeurs booléennes true et false.
IsTrue peut par exemple être utilisé pour vérifier que l’utilisateur a accepté les
conditions générales d’utilisation du site, en cochant une case à cocher.
d. Type
Cette contrainte peut vérifier qu’une valeur est d’un type donné.
Ici, par type, nous entendons les types PHP tels que boolean, int, string, etc. Cela n’a
aucun rapport avec les types de formulaires.
type
L’option type définit le type requis. En interne, le framework utilise les
fonctions PHP is_*() (is_int(), is_string(), etc.)
Cette option accepte également une classe. La valeur est valide si elle correspond à
une instance de cette classe donnée.
Contrainte de type string sur une propriété (annotation)
Avec l’exemple suivant, nous vérifions que la valeur de la propriété $messageest bien
une chaîne de caractères.
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Message
{
/**
* @Assert\Type(type="string")
*/
protected $message;
}
e. Email, Url et Ip
Ces contraintes permettent la validation de chaînes de caractères par rapport à un
format précis (email, url et ip). Elles sont l’équivalent des filtres de validation
PHPFILTER_VALIDATE_EMAIL, FILTER_VALIDATE_IP, etc.
Email
Cette contrainte vérifie la validité d’une adresse e-mail.
L’option checkMX peut effectuer une validation plus approfondie de l’e-mail.
Lorsqu’elle est mise à true, le framework vérifiera que le domaine est a priori capable
de traiter les e-mails. Pour cela, il utilise la fonction PHP checkdnsrr() sur le nom de
domaine de l’e-mail, en cherchant un enregistrement de type MX.
L’option checkHost est moins restrictive car elle considère l’e-mail valide si son
domaine possède au moins un enregistrement de type MX, A ou AAAA.
Ces deux options ont pour objectif de filtrer certaines adresses e-mail frauduleuses,
qui seraient correctes au niveau de la forme, mais dont le nom de domaine serait
inexistant.
Si vous souhaitez effectuer une validation du nom de domaine de l’e-mail, nous vous
conseillons d’utiliser checkHost. En effet, selon les spécifications du protocole
SMTP, s’il n’y a pas d’enregistrement MX, l’enregistrement A doit être utilisé.
Url
Valide une valeur comme étant une URL. C’est une validation du format.
Contrairement à la contrainte Email, ici, il ne peut y avoir aucun test au niveau du
réseau.
L’option protocols accepte un tableau de protocoles valables pour l’URL à
valider. Par défaut, cette option contient http et https.
Ip
Cette contrainte peut valider les adresses IP et, comme avec la contrainte Url, c’est
uniquement une validation du format.
L’option version permet de spécifier s’il y a une contrainte de type (V4 ou V6) ou de
plages (privées et/ou réservées). Les valeurs possibles
sont 4, 6, all, 4_no_priv, 6_no_priv, all_no_priv, 4_no_res, 6_no_res, all_no_res, 4
_public, 6_public et all_public. Par défaut, seules les IPv4 sont acceptées.
f. Regex
Cette contrainte valide une chaîne de caractères par rapport à une expression régulière
(Regex).
pattern
Cette option contient l’expression régulière. C’est l’option par défaut ; elle peut donc
être utilisée sans être nommée.
L’exemple suivant valide un numéro de téléphone portable français :
use Symfony\Component\Validator\Constraints as Assert;
// ...
/**
* @var string
*
* @Assert\Regex("/^0[6|7][0-9]{8}$/")
*/
private $portable;
match
L’option match vaut true par défaut. Si elle est configurée à false, la valeur ne devra
pas correspondre au pattern pour être considérée comme valide.
g. Length, Count
Ces contraintes contrôlent la longueur d’une chaîne de caractères, la valeur ou la taille
d’un tableau.
min et max
Ces options définissent les longueurs minimale et/ou maximale de la valeur. Il s’agit
du nombre de caractères pour la contrainte Length, ou, pour la contrainte Count, du
nombre d’éléments.
Renseigner les deux options n’est pas obligatoire. Si l’une d’entre elles est omise, il
n’y a pas de limite minimale ou maximale.
namespace App\Entity;
class Message
{
/**
* @Assert\Length(min="3")
*/
private $message;
// ....
}
Ici, la propriété $message doit contenir au minimum trois caractères pour être valide.
Attention, comme c’est le cas avec la plupart des contraintes, la valeur null ou une
chaîne vide seront considérées comme valides. Si ce n’est pas le comportement
souhaité, les contraintes NotBlank ou NotNull peuvent être utilisées en complément.
minMessage
Cette option définit le message d’erreur qui est affiché si la valeur est inférieure
à min.
maxMessage
Cette option définit le message d’erreur qui est affiché si la valeur est supérieure
à max.
exactMessage
Enfin, les contraintes Length et Count peuvent être configurées de manière à valider
une longueur fixe et non une fourchette de valeurs. Pour cela, il faut placer les mêmes
valeurs dans les options min et max.
Dans ce cas-là, c’est le message d’erreur contenu dans l’option exactMessage qui est
affiché.
Les contraintes Length et Count, contrairement aux autres, n’ont donc pas
d’option message, mais une option spécifique pour chaque message en fonction de
l’erreur.
h. Range
Cette contrainte est semblable à Length et Count, mais s’applique à des
valeurs numériques. Elle permet de vérifier qu’un nombre est compris entre deux
valeurs.
Ses options sont les mêmes que celles de Length et Count.
i. Comparaisons
Il existe plusieurs contraintes de comparaisons ; elles ont toutes (en plus de message)
une seule option : value.
Il existe une contrainte pour chacun des opérateurs suivants : ==, !=, ===, !
==, >, >=, < et <=.
EqualTo : compare en utilisant l’opérateur ==.
NotEqualTo : compare en utilisant l’opérateur !=.
IdenticalTo : compare en utilisant l’opérateur ===.
NotIdenticalTo : compare en utilisant l’opérateur !==.
LessThan : compare en utilisant l’opérateur <.
LessThanOrEqual : compare en utilisant l’opérateur <=.
GreaterThan : compare en utilisant l’opérateur >.
GreaterThanOrEqual : compare en utilisant l’opérateur >=.
L’exemple suivant est une classe Devis, dont le montant ne doit pas être fixé en
dessous de 20 euros.
namespace App\Entity;
class Devis
{
/**
* @Assert\GreaterThanOrEqual(20)
*/
protected $montant;
// ...
j. Dates
Certaines contraintes sont capables de valider des dates : Date, DateTime et Time.
Ces trois contraintes considèrent un objet PHP de type DateTime comme
valide. Elles acceptent toutefois des chaînes de caractères respectant un certain format
:
Date : YYYY-MM-DD (par exemple 2014-05-15)
DateTime : YYYY-MM-DD HH:MM:SS (par exemple 2014-05-15 15:10:39)
Time : HH:MM:SS (par exemple 15:10:39)
k. File
File valide que la valeur est un fichier chargé par formulaire (une instance
de Symfony\Component\HttpFoundation\File\UploadedFile) ou un fichier
classique (une instance de Symfony\Component\HttpFoundation\File\File ou une
chaîne de caractères contentant le chemin vers le fichier).
maxSize
Cette option spécifie la taille maximale du fichier. Au-dessus de celle-ci, il est
considéré comme invalide.
Il existe différents formats de valeurs pour cette option :
un entier : correspond à un nombre d’octets (par exemple, 1024).
un entier suivi de k ou M : correspond à un nombre de kilo-octet ou méga-octet
(par exemple 1M ou 50k).
Cette option est une configuration applicative. Si sa valeur dépasse la
directive PHP post_max_size, l’option n’aura aucun effet.
mimeTypes
Cette option contient un tableau de chaînes ou une chaîne représentant un type
MIME ; le sous-type peut être remplacé par un astérisque :
# config/validator/validation.yaml
App\Entity\Chanson:
properties:
fichier:
- File:
maxSize: 10M
mimeTypes: audio/*
l. Image
Cette contrainte hérite de File, mais elle valide seulement les fichiers représentant des
images.
Les options minWidth, maxWidth, minHeight et maxHeight spécifient (en pixels)
les largeurs et hauteurs (minimales et maximales) pour l’image.
m. Choice
Choice restreint les valeurs valides en fonction d’une liste donnée.
Il n’est pas nécessaire d’utiliser cette contrainte pour valider les données en
provenance d’un champ de type choice car le composant Form le fait déjà.
choices
Cette option contient les valeurs valides, sous la forme d’un tableau :
# config/validator/validation.yml
App\Entity\Commande:
properties:
statut:
- Choice:
choices: [créée, validée, annulée, envoyée]
Ici, la propriété statut d’un objet de type App\Entity\Commande doit valoir une des
valeurs spécifiées.
callback
Au lieu de définir la liste de valeurs manuellement avec l’option choices, il est
possible d’utiliser le retour d’une méthode statique. Dans ce cas là, le nom de cette
méthode doit être renseigné via le paramètre callback.
multiple
Cette option, mise à true, affectera la contrainte de validation de manière à ce qu’elle
s’attende à un tableau de valeurs et non à une seule valeur scalaire.
min et max
Si multiple vaut true, les options min et max peuvent limiter le nombre d’entrées du
tableau.
n. UniqueEntity
Cette contrainte est l’équivalent applicatif d’une clé unique avec MySQL. Elle peut
être appliquée à une ou plusieurs propriétés mappées par Doctrine (donc des colonnes,
cf. Accéder aux bases de données avec Doctrine - Définition des entités et de leur
mapping).
UniqueEntity doit être configuré sur la classe et non sur une propriété :
namespace App\Entity;
/**
* @ORM\Entity
* @UniqueEntity("email")
*/
class Utilisateur
{
/**
* @var string $email
*
* @ORM\Column(type="string", unique=true)
*/
protected $email;
// ...
}
use Symfony\Component\Validator\ExecutionContextInterface;
use Symfony\Component\Validator\Constraints as Assert;
class Vehicule
{
private neuf;
private kilometrage;
// ...
/**
* @Assert\Callback()
*/
public function isValid(ExecutionContextInterface $context)
{
if (!$this->neuf && !$this->kilometrage) {
$context->addViolationAt(
'kilometrage',
'Le kilométrage du véhicule doit être renseigné.'
);
}
4. Groupes de validation
Parfois, un objet est modifiable depuis plusieurs sources différentes, chacune ayant
ses spécificités. Par exemple, un objet Utilisateur peut être créé à la suite du
remplissage d’un formulaire d’inscription et des informations peuvent être
ajoutées/modifiées depuis un autre formulaire, dans une rubrique Mon compte ou via
une API.
À chaque fois, les contraintes de validation à utiliser sont potentiellement
différentes (en fonction de la source). C’est alors qu’interviennent les groupes de
validation. Leur rôle est de répartir les contraintes de validation en plusieurs sections.
// src/Entity/Utilisateur.php
namespace App\Entity;
class Utilisateur
{
/**
* @Assert\Email()
*/
private $email;
/**
* @Assert\NotBlank()
* @Assert\Length(min=5)
*/
private $mot_de_passe;
/**
* @Assert\EqualTo(
* "Kj8Zep12kado",
* message="Le code coupon est invalide.",
* groups={"Privileges"}
* )
*/
private $coupon_privileges;
}
Cependant, le cas le plus courant est sans aucun doute une utilisation des groupes au
travers des formulaires.
Pour l’exemple précédent, on pourrait imaginer une page spéciale où l’utilisateur
aurait la possibilité de remplir un champ avec le code d’un coupon lui donnant accès à
des offres spéciales. Le formulaire serait défini comme ceci :
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use App\Entity\Client;
...
public function index(Request $request)
{
$client = new Client();
$form->handleRequest($request);
return $this->render('default/index.html.twig',
['form' => $form->createView() ]
);
}
$erreurs = $this->get('validator')->validate($message);
if (0 === count($erreurs)) {
// Les données sont valides.
} else {
// Il y a au moins une erreur, les données sont invalides.
}
}
Précédent
Créer des formulaires réutilisables
Suivant
Personnaliser le rendu - thèmes de formulaires
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Cette simple fonction permet l’affichage du formulaire entier. Elle est donc très
pratique mais est aussi très restrictive. En effet, elle n’offre aucune possibilité de
contrôle sur le rendu final.
Le morceau de code suivant est équivalent à la fonction form() :
{{ form_start(form) }}
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Ici, le formulaire est configuré de manière à envoyer les données via une requête de
type GET et vers l’URL correspondant à la route recherche.
Enfin, il est possible de configurer ces attributs au niveau du template, lors de
l’invocation de la fonction form() ou de celle de form_start() :
{{ form(form, {'action': path('recherche'), 'method': 'GET'}) }}
{# ou #}
c. form_widget()
C’est la principale fonction liée au rendu d’un formulaire. Elle est chargée
d’afficher l’élément correspondant au formulaire. Pour un nœud de type text par
exemple, elle affiche un input de type text.
Cependant, elle se comporte différemment selon le type de nœud qui lui est passé en
paramètre :
Si c’est un nœud de type compound (composé), comme c’est le cas avec un
formulaire principal, elle itère sur tous ses enfants et les affiche.
Si le nœud n’est pas compound (nœud de type text, par exemple), elle
affiche directement l’élément correspondant.
Dans l’exemple précédent, nous avons passé le formulaire principal en paramètre, la
fonction form_widget a donc itéré et affiché chacun des champs composant le
formulaire.
Mais si nous lui passons un nœud non composé, le champ correspondant est affiché
directement :
{{ form_start(form) }}
{{ form_widget(form.prenom) }}
f. form_row()
form_row() est un raccourci pour
invoquer form_label(), form_errors() et form_widget() en une séquence.
g. form_rest()
Cette fonction affiche tous les nœuds qui ne l’ont pas encore été.
h. Arborescence des parties de formulaires
Les différentes fonctions que nous venons d’évoquer sont complémentaires, elles
forment les maillons d’une même chaîne. Chaque fonction génère une partie du
formulaire. Ces parties sont ensuite imbriquées les unes dans les autres et forment une
arborescence.
En réalité, même si vous n’invoquez pas toutes les fonctions directement, elles seront
invoquées par le framework comme défini sur le schéma suivant :
L’invocation d’une des fonctions ci-dessus entraîne d’autres appels en cascade. Ainsi,
invoquer form() revient à invoquer form_start(), form_row() pour chaque nœud,
puis form_end(), tandis que form_row() invoque à son
tour form_label(), form_errors() et form_widget(). Enfin, form_end() est chargé de
faire appel à form_rest(), pour, si nécessaire, afficher les champs manquants avant de
refermer le formulaire.
Contrairement aux autres parties du formulaire, form_rows n’est pas disponible en
fonction Twig. C’est simplement un bloc contenant une boucle itérant sur les
formulaires composés, pour ensuite invoquer form_row() sur chacun des nœuds.
return $this->render('default/index.html.twig'
[ 'form' => $form->createView() ]
);
}
...
{# ... #}
{{ form(form) }}
{% block form_row %}
<div style="margin: 15px 0;">
<div style="float: left; width: 100px;">
{{ form_label(form) }}
</div>
<div>
{{ form_widget(form) }}
</div>
<div style="color: red;">
{{ form_errors(form) }}
</div>
</div>
{% endblock %}
{# ... #}
{{ form(form) }}
Le tag form_theme est chargé d’effectuer l’association manuelle. Pour cela, il attend
deux arguments :
Le premier est l’objet FormView sur lequel doit être appliqué le thème de
formulaires (ici form).
Le second est le template contenant le thème de formulaires
(ici form.html.twig).
Optionnellement, form_theme peut accepter des troisième, quatrième où énième
arguments. Ces arguments correspondent à des thèmes supplémentaires à associer
au formulaire.
Une fois notre tag configuré, le rendu du formulaire change :
{% extends 'base.html.twig' %}
{% block body %}
{# ... #}
{{ form(form) }}
{% endblock %}
{% block form_row %}
<div style="margin: 15px 0;">
<div style="float: left; width: 100px;">
{{ form_label(form) }}
</div>
<div>
{{ form_widget(form) }}
</div>
<div style="color: red;">
{{ form_errors(form) }}
</div>
</div>
{% endblock %}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
La sécurité dans une application Symfony
Tester son application Symfony
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Cette simple fonction permet l’affichage du formulaire entier. Elle est donc très
pratique mais est aussi très restrictive. En effet, elle n’offre aucune possibilité de
contrôle sur le rendu final.
Le morceau de code suivant est équivalent à la fonction form() :
{{ form_start(form) }}
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Il introduit de nouvelles fonctions Twig liées à l’affichage de
formulaires : form_start(), form_label(), form_errors(), form_widget() et form_en
d().
a. form_start()
Cette fonction ouvre le formulaire en affichant la balise d’ouverture <form>.
Les principaux attributs contenus dans cette balise sont action et method. Le premier
désigne la page vers laquelle le formulaire doit être soumis, tandis que le second
spécifie la méthode HTTP à utiliser pour la requête.
Vous pouvez contrôler ces attributs de différentes manières. Il existe tout d’abord les
options method et action, qui peuvent être passées au formulaire principal :
$form = $this->createForm(new RechercheType(), null, array(
'action' => $this->generateUrl('recherche'),
'method' => 'GET',
));
Ici, le formulaire est configuré de manière à envoyer les données via une requête de
type GET et vers l’URL correspondant à la route recherche.
Enfin, il est possible de configurer ces attributs au niveau du template, lors de
l’invocation de la fonction form() ou de celle de form_start() :
{{ form(form, {'action': path('recherche'), 'method': 'GET'}) }}
{# ou #}
c. form_widget()
C’est la principale fonction liée au rendu d’un formulaire. Elle est chargée
d’afficher l’élément correspondant au formulaire. Pour un nœud de type text par
exemple, elle affiche un input de type text.
Cependant, elle se comporte différemment selon le type de nœud qui lui est passé en
paramètre :
Si c’est un nœud de type compound (composé), comme c’est le cas avec un
formulaire principal, elle itère sur tous ses enfants et les affiche.
Si le nœud n’est pas compound (nœud de type text, par exemple), elle
affiche directement l’élément correspondant.
Dans l’exemple précédent, nous avons passé le formulaire principal en paramètre, la
fonction form_widget a donc itéré et affiché chacun des champs composant le
formulaire.
Mais si nous lui passons un nœud non composé, le champ correspondant est affiché
directement :
{{ form_start(form) }}
{{ form_widget(form.prenom) }}
f. form_row()
form_row() est un raccourci pour
invoquer form_label(), form_errors() et form_widget() en une séquence.
g. form_rest()
Cette fonction affiche tous les nœuds qui ne l’ont pas encore été.
h. Arborescence des parties de formulaires
Les différentes fonctions que nous venons d’évoquer sont complémentaires, elles
forment les maillons d’une même chaîne. Chaque fonction génère une partie du
formulaire. Ces parties sont ensuite imbriquées les unes dans les autres et forment une
arborescence.
En réalité, même si vous n’invoquez pas toutes les fonctions directement, elles seront
invoquées par le framework comme défini sur le schéma suivant :
L’invocation d’une des fonctions ci-dessus entraîne d’autres appels en cascade. Ainsi,
invoquer form() revient à invoquer form_start(), form_row() pour chaque nœud,
puis form_end(), tandis que form_row() invoque à son
tour form_label(), form_errors() et form_widget(). Enfin, form_end() est chargé de
faire appel à form_rest(), pour, si nécessaire, afficher les champs manquants avant de
refermer le formulaire.
Contrairement aux autres parties du formulaire, form_rows n’est pas disponible en
fonction Twig. C’est simplement un bloc contenant une boucle itérant sur les
formulaires composés, pour ensuite invoquer form_row() sur chacun des nœuds.
return $this->render('default/index.html.twig'
[ 'form' => $form->createView() ]
);
}
...
{# ... #}
{{ form(form) }}
b. Créer et associer un thème de formulaires
Ce rendu par défaut est peu attractif mais il est heureusement facilement
modifiable. Pour cela, il suffit de créer un fichier qui contient nos thèmes pour ce
formulaire, localisé à l’emplacement suivant : templates/form.html.twig.
L’emplacement et le nom du fichier ci-dessus sont complètement arbitraires. Vous
pouvez utiliser l’emplacement qui vous convient, dans la mesure où il correspond à un
emplacement valide pour un template Twig.
Créons un thème pour la partie form_row (celle-ci correspond à une ligne de
formulaire) de manière à rendre le formulaire plus ordonné :
{# templates/form.html.twig #}
{% block form_row %}
<div style="margin: 15px 0;">
<div style="float: left; width: 100px;">
{{ form_label(form) }}
</div>
<div>
{{ form_widget(form) }}
</div>
<div style="color: red;">
{{ form_errors(form) }}
</div>
</div>
{% endblock %}
{# ... #}
{{ form(form) }}
Le tag form_theme est chargé d’effectuer l’association manuelle. Pour cela, il attend
deux arguments :
Le premier est l’objet FormView sur lequel doit être appliqué le thème de
formulaires (ici form).
Le second est le template contenant le thème de formulaires
(ici form.html.twig).
Optionnellement, form_theme peut accepter des troisième, quatrième où énième
arguments. Ces arguments correspondent à des thèmes supplémentaires à associer
au formulaire.
Une fois notre tag configuré, le rendu du formulaire change :
{% extends 'base.html.twig' %}
{% block body %}
{# ... #}
{{ form(form) }}
{% endblock %}
{% block form_row %}
<div style="margin: 15px 0;">
<div style="float: left; width: 100px;">
{{ form_label(form) }}
</div>
<div>
{{ form_widget(form) }}
</div>
<div style="color: red;">
{{ form_errors(form) }}
</div>
</div>
{% endblock %}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Pare-feu
a. Pare-feu pour ressources statiques/développement
2. Authentification HTTP
3. Authentification par formulaire de connexion
4. Connexion automatique des utilisateurs
5. Déconnexion des utilisateurs
Utilisateurs et rôles
Autorisations
Quiz
Tester son application Symfony
Introduction au test logiciel
Les tests unitaires avec PHPUnit
Les tests fonctionnels
Quiz
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Authentification
1. Pare-feu
Le rôle du pare-feu est de définir les zones de l’application sujettes au système de
sécurité.
Lorsque la requête d’un utilisateur correspond aux règles définies par un pare-feu, la
sécurité est activée. La requête doit donc passer au travers de ce pare-feu et satisfaire
les contraintes d’autorisations qui y sont spécifiées.
Une requête est associée à un pare-feu en fonction de son chemin (path). Reprenons la
section firewalls de notre configuration de base :
# config/packages/security.yaml
security:
# ...
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
# ...
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
2. Authentification HTTP
La méthode d’authentification la plus simple est sans doute l’authentification HTTP
Basic :
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
providers:
mes_utilisateurs:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
firewalls:
mon_pare_feu:
pattern: ^/
http_basic:
realm: "Mon pare-feu demande une authentification"
providers:
mes_utilisateurs:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
form_login: ~
Une authentification par formulaire requiert la création de deux routes pour les
chemins (paths) /login et /login_check. La première correspondra à la page avec le
formulaire de connexion, la seconde est automatiquement interceptée par le pare-feu.
Ajoutons d’abord la route pour le chemin /login_check dans le
fichier config/routes.yaml, fichier principal de configuration des routes :
# config/routes.yaml
login_check:
path: /login_check
Cette dernière ne comporte pas d’action et nous la définissons dans le simple but de
pouvoir générer la valeur de l’attribut target du formulaire de connexion (grâce aux
fonctions Twig path() ou url()).
Voici l’action de la page du formulaire d’authentification :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
return $this->render('login/index.html.twig', [
'erreur' => $erreur
]);
}
}
{% block body %}
{% if erreur %}
<div>{{ erreur.message }}</div>
{% endif %}
<label>
Mot de passe :
</label>
<input type="password" name="_password" />
<br />
<button type="submit">Connexion</button>
</form>
{% endblock %}
# ..
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
form_login: ~
remember_me:
key: "%secret%"
lifetime: 7776000 # 90 jours (en secondes)
path: /
domain: ~
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
form_login: ~
logout: ~
Par défaut, le chemin utilisé est /logout. Vous devez obligatoirement créer une route
pour ce chemin dans le fichier config/routes.yaml :
# config/routes.yaml
Précédent
Présentation et concepts de sécurité
Suivant
Utilisateurs et rôles
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. L’utilisateur
a. Récupérer l’utilisateur courant
2. Les fournisseurs d’utilisateurs
a. En mémoire
b. Fournisseur d’utilisateurs de bases de données
c. Fournisseur d’utilisateurs personnalisé
d. Notes additionnelles
3. Cryptage des mots de passe
a. Encodeurs
b. Le salage
c. Crypter un mot de passe
4. Les rôles
Autorisations
Quiz
Tester son application Symfony
Introduction au test logiciel
Les tests unitaires avec PHPUnit
Les tests fonctionnels
Quiz
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Utilisateurs et rôles
1. L’utilisateur
Avec Symfony, l’objet représentant un utilisateur implémente obligatoirement
l’interface Symfony\Component\Security\Core\User\UserInterface. Elle garantit
les méthodes suivantes :
getUsername(), qui retourne le nom d’utilisateur.
getPassword(), qui retourne le mot de passe de l’utilisateur.
getSalt(), qui retourne la valeur du salage (salt) du mot de passe ; nous
reviendrons plus loin dans ce chapitre sur ce concept, avec les encodeurs.
getRoles(), qui retourne les rôles de l’utilisateur. Ces rôles ont un impact sur le
niveau d’autorisation de chaque membre. En pratique, ils permettent, par
exemple, de dire si le membre est un administrateur, un modérateur ou un simple
utilisateur.
eraseCredentials(), qui est utilisée en interne par le framework. Cette
méthode efface les informations sensibles (s’il en existe) contenues dans l’objet
utilisateur. Elle est utile si, par exemple, à un moment donné, le mot de passe est
stocké en clair dans l’objet.
Selon le fournisseur d’utilisateurs que vous choisissez ou créez, les objets représentant
les utilisateurs peuvent implémenter une classe plus complète que
UserInterface : Symfony\Component\Security\Core\User\AdvancedUserInterface.
Le but de cette dernière est de prendre en charge les fonctionnalités de suspension et
d’expiration de comptes utilisateur.
a. Récupérer l’utilisateur courant
Pour récupérer l’utilisateur actuellement authentifié, vous disposez du
raccourci getUser() dans vos contrôleurs :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
security:
# ...
providers:
mes_utilisateurs:
memory:
users:
bob: { password: pa$S, roles: [ 'ROLE_USER' ] }
sarah: { password: 4Dm1nP4$s , roles: [ 'ROLE_USER',
'ROLE_ADMIN' ] }
# ...
Avec cette configuration, nous définissons deux utilisateurs (bob et sarah) ayant
chacun un mot de passe ainsi qu’un ou plusieurs rôles.
Le fournisseur d’utilisateurs memory consiste donc à définir des comptes
utilisateur directement dans le fichier security.yaml. Il s’avère pratique uniquement si
votre pare-feu est à destination d’un groupe restreint et figé d’utilisateurs. Il convient
par exemple parfaitement à une zone d’administration à laquelle vous seul avez accès.
b. Fournisseur d’utilisateurs de bases de données
Le fournisseur d’utilisateurs que nous allons décrire ici persiste les utilisateurs dans
une base de données. Il répond à la demande de la majorité des sites web, où les
utilisateurs s’enregistrent d’eux-mêmes au travers d’un formulaire d’inscription, leurs
données étant enregistrées en base.
Avant de configurer ce fournisseur d’utilisateurs, il convient de créer une
entité Utilisateur :
namespace App\Entity;
/**
* @ORM\Entity()
*/
class Utilisateur implements UserInterface
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $username;
/**
* @ORM\Column(type="string")
*/
private $password;
// mutateurs...
}
Comme nous l’avons expliqué précédemment, les objets utilisateurs utilisés par le
système de sécurité de Symfony implémentent l’interface Symfony\Component\
Security\Core\User\UserInterface. C’est donc le cas ici pour votre classe d’entité
Utilisateur car elle est destinée à représenter un utilisateur.
Par souci de simplicité, cette entité est volontairement minimaliste. Elle stocke le mot
de passe en clair, et elle ne doit en aucun cas être utilisée en production. Elle a
uniquement valeur d’exemple.
En exécutant la commande php bin/console doctrine:schema:update --force, une
table Utilisateur sera créée. Vous pouvez d’ores et déjà y insérer quelques utilisateurs
avec leur mot de passe (en clair).
Pour créer le fournisseur d’utilisateurs approprié, il faut utiliser la valeur entity :
security
encoders:
App\Entity\Utilisateur: plaintext
providers:
ma_bdd:
entity:
class: App\Entity\Utilisateur
property: username
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
form_login: ~
logout: ~
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 UtilisateurProvider implements UserProviderInterface
{
private $pdo;
$stmt->execute(array($username));
if ($password = $stmt->fetchColumn()) {
return new Utilisateur($username, $password);
}
return $this->loadUserByUsername($user->getUsername());
}
use Symfony\Component\Security\Core\User\UserInterface;
Notre fournisseur d’utilisateurs doit être configuré en tant que service dans le
fichier config/services.yaml :
# config/services.yaml
# ...
services:
mon_pdo:
class: PDO
arguments:
- mysql:host=localhost;dbname=monjournal
- monjournaluser
- monjournalpass
mon_fournisseur_pdo:
class: App\Security\UtilisateurProvider
arguments: [@mon_pdo]
security:
encoders:
App\Security\Utilisateur: plaintext
providers:
ma_bdd:
id: mon_fournisseur_pdo
firewalls:
mon_pare_feu:
pattern: ^/
anonymous: ~
form_login: ~
logout: ~
d. Notes additionnelles
FOSUserBundle
Pour la gestion de vos utilisateurs en base de données (inscription, confirmation par e-
mail, récupération du mot de passe, etc.), nous vous conseillons l’incontournable
FOSUserBundle. C’est le plus populaire des bundles Symfony.
Une annexe est consacrée à l’installation et à la configuration de ce bundle, en fin
d’ouvrage.
OAuth, annuaire LDAP, etc.
Avant de vous lancer dans la création d’un fournisseur d’utilisateurs personnalisé,
pensez à vérifier qu’un bundle tiers ne réponde pas déjà à votre besoin. Ainsi, le
bundle HWIOAuthBundle peut être utilisé pour une authentification OAuth (pour
Facebook et Twitter par exemple) tandis que le bundle IMAGLdapBundle est capable
d’authentifier les utilisateurs d’un annuaire LDAP.
HTTP et les formulaires de connexion
Le protocole HTTP étant défini comme stateless (sans état), chaque requête y est
traitée indépendamment, et non dans une continuité avec les précédentes requêtes de
l’utilisateur.
Avec une authentification HTTP Basic, les informations d’identification sont
soumises à chaque requête par le navigateur. Ce n’est pas le cas avec la traditionnelle
connexion par formulaire de connexion. Pour cette dernière, le framework utilise
les cookies, qui servent à conserver l’authentification pendant toute la navigation de
l’utilisateur.
L’utilisation de cookies crée certaines contraintes pour vos classes utilisateurs. Vous
devez ainsi veiller à ce qu’elles soient sérialisables.
Avec Doctrine, une entité désérialisée est particulière car elle n’est plus suivie par
l’EntityManager. C’est notamment pour cette raison que vous remarquerez qu’à
chaque changement de page, Doctrine effectue une requête pour rafraîchir l’utilisateur
courant.
# ...
/**
* @ORM\Entity()
*/
class Utilisateur implements UserInterface
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $username;
/**
* @ORM\Column(type="string")
*/
private $salt;
/**
* @ORM\Column(type="string")
*/
private $password;
// ...
}
Ici, à la création d’une nouvelle entité Utilisateur, nous générons une valeur de salage
aléatoire avec md5(uniqid(rand(), true)).
c. Crypter un mot de passe
Voici comment crypter un mot de passe depuis un contrôleur :
$factory = $this->get('security.encoder_factory');
$encodeur = $factory->getEncoder($user);
$password = $encodeur->encodePassword(
'p4s$',
$utilisateur->getSalt()
);
$utilisateur->setPassword($password);
4. Les rôles
Les rôles définissent le niveau d’autorisation de chaque membre. Des rôles
couramment utilisés sont par exemple administrateur, modérateur, éditeur ou
simple utilisateur. L’administrateur a tous les droits, le modérateur peut supprimer
des messages injurieux et bannir les membres les propageant, l’éditeur peut publier
des articles sur le site, et enfin, l’utilisateur est le membre « normal » de votre site.
La méthode getRoles() de vos objets utilisateurs doit retourner un tableau de rôles,
chaque rôle étant une chaîne de caractères qui commence par ROLE_. La seconde
partie de cette chaîne de caractères est libre.
Même si la méthode getRoles() retourne un tableau, ce dernier peut tout à fait ne
contenir qu’un seul rôle.
Voici un exemple d’entité Utilisateur gérant deux rôles ; le rôle de base (nous
choisissons ROLE_USER) et le rôle administrateur (que nous
appelons ROLE_ADMIN) :
namespace App\Entity;
/**
* @ORM\Entity()
*/
class Utilisateur implements UserInterface
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", length=32)
*/
private $salt;
/**
* @ORM\Column(type="string", length=40)
*/
private $password;
/**
* @ORM\Column(name="is_admin", type="boolean")
*/
private $isAdmin;
if ($this->isAdmin) {
$roles[] = 'ROLE_ADMIN';
}
return $roles;
}
// mutateurs
// ...
}
Pour cette entité, nous avons une propriété isAdmin mappée par Doctrine. Elle
accepte une valeur booléenne indiquant si l’utilisateur est un administrateur ou non ;
cette valeur est persistée en base de données. La méthode getRoles() ajoute le rôle
d’administrateur en fonction de cette propriété. En clair, si vous utilisez une base de
données MySQL, il vous suffit de passer la valeur de cette colonne à 1 pour qu’un
utilisateur obtienne le rôle d’administrateur.
Hiérarchie de rôles
Les rôles correspondent rarement à des droits isolés les uns des autres : il existe la
plupart du temps une hiérarchie. L’administrateur possède, en plus de ses propres
autorisations, les mêmes autorisations que le modérateur ou l’éditeur, ces derniers
étant quant à eux aussi des utilisateurs de base, avec certes quelques autorisations
supplémentaires.
Plutôt que de devoir retourner l’ensemble des rôles pour chaque utilisateur, vous
pouvez facilement définir une hiérarchie de rôles :
security:
role_hierarchy:
ROLE_MODERATEUR: ROLE_USER
ROLE_EDITEUR: ROLE_USER
ROLE_ADMIN: [ ROLE_MODERATEUR, ROLE_EDITEUR ]
# ...
Ainsi, le simple fait de posséder le rôle ROLE_ADMIN implique tous les autres
rôles. Cela rend la gestion des rôles plus simple et vous pouvez dorénavant retourner
un seul rôle depuis votre objet utilisateur. Symfony y associera automatiquement les
rôles inférieurs dans la hiérarchie.
Précédent
Authentification
Suivant
Autorisations
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
Tester son application Symfony
Introduction au test logiciel
Les tests unitaires avec PHPUnit
Les tests fonctionnels
Quiz
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
Autorisations
Jusqu’ici, nous avons appris à authentifier des utilisateurs, au travers de différentes
méthodes et de différents fournisseurs d’utilisateurs. Nous connaissons la
représentation d’un utilisateur et nous savons qu’il possède des rôles. Nous allons
maintenant voir comment sécuriser des ressources.
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
if ($this->isGranted('ROLE_USER')) {
return new Response('Bienvenue cher utilisateur.');
}
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class DefaultController extends AbstractController
{
/**
* @Route("/page-admin")
*/
public function pageAdmin()
{
if (!$this->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
L’exception Symfony\Component\Security\Core\Exception\
AccessDeniedException diffère de Symfony\Component\HttpKernel\Exception\
AccessDeniedHttpException par le fait que l’exception du
composant HttpKernel est définitive : la réponse renvoyée sera obligatoirement de
statut 403.
Avec l’exception du composant Security, une réponse de statut 403 n’est pas
obligatoirement renvoyée. Si l’utilisateur n’est pas authentifié (anonyme), son
authentification sera demandée et il sera par exemple redirigé vers un formulaire de
connexion.
# ...
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
# ...
access_control:
- { host: ^admin\.example\.com$, roles: ROLE_ADMIN }
# ...
access_control:
- { path: ^/paiement, roles: IS_AUTHENTICATED_ANONYMOUSLY,
requires_channel: https }
# ...
access_control:
- { path: ^/api, ip: 11.22.33.44, methods: [POST], roles:
IS_AUTHENTICATED_ANONYMOUSLY }
Ici, la première règle de contrôle d’accès est valide pour votre partenaire, elle autorise
les utilisateurs anonymes. Cependant, si toute autre personne tente de requêter votre
API, comme elle ne répond pas aux critères de la règle de contrôle d’accès, elle ne
verra pas son accès être refusé : il y a une règle qui autorise l’accès à votre partenaire,
mais il n’y a pas de règle qui refuse l’accès aux autres.
Pour éviter cela, vous pouvez utiliser la technique suivante :
security:
# ...
access_control:
- { path: ^/api, ip: 11.22.33.44, methods: [POST], roles:
IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: ROLE_FORBIDDEN }
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
3. Analogie
Un test est donc la vérification que la réaction entraînée par une action
donnée correspond à nos attentes. La différence entre un test unitaire ou
d’intégration et un test fonctionnel est que les premiers s’appliquent à une partie
précise et isolée de l’application, tandis que le second s’applique à un plus large
ensemble, généralement une fonctionnalité.
L’automobile
Si ces concepts de tests unitaires et fonctionnels vous semblent flous, rassurez-vous,
nous les utilisons presque tous les jours, à notre insu.
Imaginez qu’un matin d’hiver, en montant à bord de votre véhicule, vous vous rendiez
compte que vos phares éclairent moins bien que d’habitude. Très vite, en sortant de
votre voiture et en vous approchant de l’avant, l’explication vous saute aux yeux : un
phare ne s’allume plus.
Vous emmenez votre automobile à un garage, et le mécanicien retire l’ampoule du
bloc optique, mesure sa résistance électrique grâce à un ohmmètre, puis vous informe
qu’elle est défaillante : elle doit être changée.
Le matin, vous avez remarqué une anomalie sur votre véhicule, vous avez donc
effectué un test fonctionnel : en actionnant vos feux de croisement depuis l’intérieur
de la voiture, les deux phares devaient s’allumer, mais cela n’a pas été le cas, le test a
donc échoué.
Le mécanicien a quant à lui réalisé un test unitaire. Ce dernier était isolé sur une pièce
précise, l’ampoule, et a également échoué. Nous avons pu en déduire la correction à
appliquer sur le véhicule pour régler le problème : changer l’ampoule.
Ici, le problème était léger et une panne plus grave aurait pu, par exemple,
empêcher le démarrage de l’automobile. C’est pour prévenir ces grosses pannes
désagréables que votre véhicule doit régulièrement subir un contrôle technique. Le
contrôle technique contient une multitude de tests fonctionnels et unitaires sur les
pièces de votre voiture. Ainsi, les problèmes sont détectés dans un garage et non en
pleine nuit au milieu d’une autoroute.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Une fois l’installation terminée, vous devriez pouvoir invoquer PHPUnit avec la
commande suivante passée depuis votre répertoire de projet :
php ./vendor/bin/phpunit
Par défaut, cette commande exécute tous les tests présents dans le répertoire tests/ ;
ici, aucun test n’est exécuté, comme l’indique la sortie de la commande.
class Utilisateur
{
private $prenom;
private $nom;
Cette classe Utilisateur est assez simple. Lors de son instanciation, elle prend en
argument un prénom et un nom. Sa méthode getNomComplet() retourne le nom
complet de l’utilisateur.
Pour tester cette méthode getNomComplet(), nous allons créer un fichier de tests
pour la classe Utilisateur dans le dossier tests/ de l’application. Ce dossier tests/ doit
être le reflet de la structure du dossier src/ et chacun des noms de ses fichiers de tests
doit être suffixé de Test.
Ainsi, les tests de la classe App\Model\Utilisateur, déclarée dans le
fichier src/Model/Utilisateur.php, doivent être dans le
fichier tests/Model/UtilisateurTest.php, dont voici quel pourrait être le contenu :
namespace App\Tests\Model;
use App\Model\Utilisateur;
$nomComplet = $utilisateur->getNomComplet();
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
Journalisation et surveillance avec Symfony
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Controller\DefaultController;
$reponse = $controleur->bonjourAction('Amadou');
$this->assertEquals(
'Bonjour Amadou !',
$reponse->getContent()
);
}
}
À première vue, ce test est correct, mais même si PHPUnit le marquait comme valide,
il est en réalité inapproprié.
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
$client->request('GET', '/bonjour/Amadou');
$this->assertEquals(
'Bonjour Amadou !',
$client->getResponse()->getContent()
);
}
}
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
$client->request('GET', '/bonjour/Amadou');
$this->assertEquals(
'Bonjour Amadou !',
$client->getResponse()->getContent()
);
$client->request('POST', '/bonjour/Amadou');
$this->assertTrue(
$client->getResponse()->isClientError()
);
$client->getRequest()
L’objet Crawler
Tester le contenu des réponses de l’application est très utile, mais offre son lot de
limitations. Lors des tests, il est parfois nécessaire d’effectuer des opérations plus
complexes sur les pages HTML, comme la soumission d’un formulaire. Le
framework dispose d’un objet facilitant l’extraction d’informations et la navigation
dans vos réponses HTML : le Crawler.
Un objet Crawler est retourné à chaque fois que vous effectuez une requête avec le
Client :
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
La méthode filter() du Crawler accepte un sélecteur CSS et, ici, nous récupérons le
premier élément input du formulaire ayant comme identifiant mon_formulaire.
Un exemple d’utilisation concret du Crawler pourrait être le test suivant, qui vérifie
que le premier champ du formulaire est de type text :
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
$input = $crawler
->filter('form#mon_formulaire input')
->first()
;
$this->assertEquals('text', $input->attr('type'));
}
}
Pour découvrir toutes les possibilités du Crawler, sa page de documentation est d’une
précieuse aide : https://symfony.com/doc/4.4/components/dom_crawler.html
4. Soumettre un formulaire
En combinant les objets Client et Crawler, des fonctionnalités intéressantes
apparaissent. La plus puissante est sans doute la soumission de formulaires :
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
$form = $crawler
->filter('form#mon_formulaire')
->selectButton('Valider')
->form(array(
'mon_champ_1' => 'White',
'mon_champ_2' => 'Pinkman',
))
;
$client->submit($form);
// $client->getResponse(); contient la nouvelle réponse
// $client->getCrawler(); contient le nouveau Crawler
}
}
Cet exemple prépare la soumission d’un formulaire de la page avec des valeurs pour
les champs mon_champ_1 et mon_champ_2. Le Client est ensuite chargé de le
soumettre, et vous avez finalement accès à la réponse de cette nouvelle requête.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. La journalisation
2. La librairie Monolog
3. Le service logger
4. Le fichier journal
a. Identifier la cause d’un bogue
b. Le problème
5. Les gestionnaires (handlers)
a. Définir plusieurs gestionnaires
b. Envoyer des logs par e-mail
c. Utiliser un tampon (buffer)
d. Ajouter des informations complémentaires
6. Les canaux (channels)
a. Ajouter ses propres canaux
b. Envoyer un enregistrement sur un canal donné
c. Configurer les gestionnaires par canaux
d. Gestion des erreurs 404
2. La librairie Monolog
Monolog est une librairie capable non seulement d’enregistrer les logs dans un fichier
de journalisation, mais également de les envoyer par e-mail, avec le protocole syslog,
ou même de les stocker en base de données.
Monolog dans Symfony
Monolog est installable dans Symfony à partir d’une recette Flex. Voici la commande
à utiliser pour l’installer dans votre application :
composer require symfony/monolog-bundle
3. Le service logger
Le service de journalisation par défaut s’appelle logger. Monolog supporte le standard
PSR-3 et implémente l’interface Psr\Log\LoggerInterface.
Voici comment déclencher un log depuis un contrôleur :
$this->get('logger')->warning('Houston, on a eu un problème.');
...
4. Le fichier journal
Jusqu’à présent, l’utilisation de la journalisation s’avère assez simple. Il suffit de
récupérer le service logger, puis d’invoquer une méthode (warning(), error(), etc.)
avec un message. Les logs sont ensuite enregistrés dans un fichier au sein du
répertoire var/log.
a. Identifier la cause d’un bogue
Le fichier de logs contient des informations intéressantes sur votre application. Il peut
être utilisé si vous souhaitez effectuer des statistiques ou, plus couramment, pour
identifier la cause d’un dysfonctionnement.
Si un utilisateur porte à votre connaissance le fait qu’il a rencontré un bogue sur votre
application, le fichier de logs peut vous aider à détecter la cause de ce bogue.
Les logs étant listés chronologiquement, il vous suffit de naviguer jusqu’au créneau
horaire durant lequel le bogue a eu lieu et de rechercher un log dont le niveau est au
minimum ERROR. Le message de ce log vous donnera sûrement de précieuses
indications quant à la cause de ce dysfonctionnement.
Imaginons que vous trouviez un log de niveau ALERT dont le message explique que
la connexion à la base de données a échoué : vous contactez la personne en charge de
la base de données, qui corrige le problème.
b. Le problème
Même si, grâce au fichier de logs, vous avez réussi à identifier (et par la suite
corriger) un bogue, vous avez fait preuve d’un manque de réactivité sur une situation
urgente.
Si un log survient dans votre application, l’enregistrer dans un fichier pour une
analyse a posteriori n’est donc parfois pas suffisant. Cela dépend surtout du niveau de
gravité du log.
Nous allons voir comment les gestionnaires (ou handlers) peuvent nous aider à traiter
cette problématique. Il est possible d’affecter différents gestionnaires pour vos logs,
en fonction notamment de leur niveau de gravité.
monolog:
handlers:
main:
type: stream
path: %kernel.logs_dir%/%kernel.environment%.log
level: debug
Ici, nous en définissons deux : important et secondaire. Avec cette configuration, les
logs seront répartis en deux fichiers : important.log contiendra les enregistrements de
niveaux ERROR et supérieurs, tandis que secondaire.log contiendra les autres
enregistrements (niveaux inférieurs à ERROR).
L’option bubble mise à false sur le premier handler indique qu’une fois un
enregistrement traité, il ne doit pas être envoyé aux handlers suivants ; l’ordre dans
lequel les handlers sont définis est donc important.
Si vous retirez l’option bubble, les enregistrements de niveaux ERROR et supérieurs
seront écrits dans les deux fichiers.
b. Envoyer des logs par e-mail
Le handler swift_mailer est capable d’envoyer des e-mails avec la
librairie SwiftMailer, intégrée par défaut à Symfony.
Ce handler est tout à fait adapté aux enregistrements de niveaux CRITICAL et
supérieurs. Voici comment le configurer :
monolog:
handlers:
swift:
type: swift_mailer
from_email: rapport@mon-projet.local
to_email: developpeurs@mon-projet.local
subject: Rapport d'erreur
level: critical
Ici, les logs de niveaux CRITICAL et supérieurs seront envoyés dans des
messages, à l’adresse developpeurs@mon-projet.local.
c. Utiliser un tampon (buffer)
Le handler swift_mailer que nous venons de configurer n’est pas optimal. Si
plusieurs erreurs critiques surviennent au cours d’une même requête, vous
recevrez autant d’e-mails.
Il est possible de regrouper les enregistrements d’une requête au sein du même
message grâce à un tampon (buffer en anglais) :
monolog:
handlers:
buffer:
type: buffer
handler: swift
level: critical
swift:
type: swift_mailer
from_email: rapport@mon-projet.local
to_email: developpeurs@mon-projet.local
subject: Rapport d'erreur
Avec cette configuration, le handler buffer englobe le handler swift, qui n’existe plus
de lui-même, il n’a donc pas besoin de niveau. Le handler buffer récupérera les
enregistrements de niveaux CRITICAL et supérieurs, mais ne les transférera au
handler swift qu’en fin de script. Ainsi, si plusieurs enregistrements sont récupérés, ils
seront regroupés en un seul e-mail.
d. Ajouter des informations complémentaires
Pour récupérer des informations complémentaires, il existe un handler très utile
appelé fingers_crossed :
monolog:
handlers:
fingers_crossed:
type: fingers_crossed
level: info
action_level: error
handler: nested
nested:
type: stream
path: %kernel.logs_dir%/enregistrements.log
Tout comme buffer, ce handler en englobe un autre (ici nested, qui n’existe donc plus
de lui-même).
Le handler fingers_crossed comporte deux principales
directives : level et action_level. L’option level indique le niveau à partir duquel les
enregistrements seront interceptés, alors qu’action_level définit le niveau à partir
duquel les enregistrements interceptés doivent être transmis au handler englobé.
Ainsi, avec l’exemple ci-dessus, si, au cours d’une requête, seuls des enregistrements
de niveaux DEBUG et WARNING sont déclenchés, rien ne sera écrit dans le
fichier enregistrements.log. Cependant, si, au cours d’une autre requête, au moins un
log de niveaux ERROR ou supérieur est déclenché, tous les enregistrements
interceptés par le handler fingers_crossed seront ajoutés au
fichier enregistrements.log.
Grâce à cette technique, vous pouvez récupérer des informations complémentaires sur
le déroulement de la requête et non les messages d’erreur isolés.
Ce handler peut facilement être intégré à swift_mailer :
monolog:
handlers:
mail:
type: fingers_crossed
level: info
action_level: critical
handler: buffered
buffered:
type: buffer
handler: swift
swift:
type: swift_mailer
from_email: rapport@mon-projet.local
to_email: developpeurs@mon-projet.local
subject: Rapport d'erreur
Ici, nous ajoutons deux canaux : mon_api et mon_autre_canal. Ils seront par défaut
traités par tous vos handlers.
b. Envoyer un enregistrement sur un canal donné
Pour envoyer un enregistrement sur un canal donné, vous devez utiliser le
service correspondant. En clair, chaque canal possède son propre service de
journalisation :
// Canal « app »
$this->get('logger')->error('Une erreur est survenue.');
// Canal « mon_api »
$this
->get('monolog.logger.mon_api')
->critical('Mon API ne répond pas.')
;
// Canal « doctrine »
$this
->get('monolog.logger.doctrine')
->debug('Ma requête n\'a retourné aucun résultat.')
;
Comme vous l’aurez compris, pour envoyer les enregistrements sur un canal X, il
suffit d’utiliser le service monolog.logger.X.
Notons tout de même que, à titre d’exception, le service logger utilise le canal app :
c’est le service de journalisation principal.
c. Configurer les gestionnaires par canaux
Il n’y a pas d’intérêt à utiliser ses propres canaux, si ce n’est pour les attacher à
différents gestionnaires.
La directive channels est chargée d’effectuer cette tâche. Elle fonctionne soit par
inclusion, soit par exclusion :
monolog:
handlers:
api:
type: stream
path: %kernel.logs_dir%/mon_api.log
channels: [mon_api]
principal:
type: stream
path: /%kernel.logs_dir%/enregistrements.log
channels: [!mon_api, !doctrine]
Avec cette dernière, Monolog exclura du fichier de logs les erreurs 404 dont le path
de l’URL commence par /phpmyadmin ou /wp-admin. Ce sont typiquement des
erreurs provoquées par des robots ou des pirates informatiques qui tentent d’accéder
respectivement aux pages d’administration de PHPMyAdmin et WordPress, en se
basant sur les paths par défaut de ces projets.
Précédent
Quiz
Suivant
Le monitoring avec Prometheus et Grafana
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
Amélioration des performances
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
# ...
Prometheus\Storage\APC: ~
Prometheus\CollectorRegistry:
public: true
arguments: ['@Prometheus\Storage\APC']
use Prometheus\RenderTextFormat;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use HRTime\StopWatch;
use Prometheus\CollectorRegistry;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
$this->timer->stop();
$histogram = $this->registry->getOrRegisterHistogram(
'',
'http_request_duration_seconds',
'request duration',
['route'],
[0.05, 0.1, 0.2, 0.5, 1, 2, 5]
);
$histogram->observe(
$this->timer->getElapsedTime(),
[$event->getRequest()->attributes->get('_route')]
);
}
}
Ici, nous mesurons la durée de chaque requête puis l’enregistrons dans un
histogramme. Nous utilisons l’extension hrtime plutôt que la
fonction microtime(), car cette dernière n’est pas monotonique (elle peut avancer ou
reculer arbitrairement).
Ici, vous devez remplacer les valeurs selon votre installation : job_name avec le nom
de votre application, par exemple, scrape_interval pour l’intervalle entre chaque
requête de Prometheus, et enfin targets pour l’hôte et le port de votre application.
Prometheus est maintenant capable de récupérer les mesures. Il ne reste plus qu’à les
intégrer à Grafana pour obtenir des graphiques personnalisables.
Dans l’interface Grafana, ouvrez le menu Data Source - Add a data source, donnez
un nom à votre source et sélectionnez le type « Prometheus ». Saisissez l’URL de
votre application Symfony (sans le path /metrics, par exemple http://localhost:9000).
Créez maintenant un dashboard ; vous pouvez y ajouter des graphiques. Voici un
exemple de dashboard avec deux graphiques utilisant les mesures précédemment
définies.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<?php echo date('y-m-d H:i:s') ?><br />
<a href="">Rafraîchir</a>
</body>
</html>
Si vous ouvrez cette page avec un navigateur web, puis cliquez sur le
bouton Rafraîchir, vous vous rendrez compte que la date affichée se met à jour
seulement au bout de 10 secondes.
Lors des autres chargements, le navigateur utilise la page présente dans son cache,
car, lorsqu’il récupère la réponse la première fois, l’en-tête Cache-
Control indique que la page peut être gardée en cache pendant 10 secondes, et qu’au-
delà de ce délai, une nouvelle version devra être demandée au serveur web.
Grâce à cette technique, la page peut être renvoyée très rapidement à l’utilisateur,
aucune requête HTTP n’est effectuée, et la charge du serveur web s’en trouve allégée.
Le serveur web doit générer la page pour chaque client, et si ces derniers
effectuent une nouvelle requête pour la même page, et que les en-têtes indiquent que
la copie est encore valable (Cache-Control), ils utilisent la copie dans le cache du
navigateur.
Il existe un mécanisme capable de fournir un cache commun pour tous les clients : le
proxy inverse (ou reverse proxy).
Ici, les clients ne communiquent plus directement avec le serveur web, mais passent
par un proxy inverse.
Le proxy transmet ensuite la requête à l’application, et si la réponse renvoyée
comporte des en-têtes liés à la mise en cache, il enregistre une copie de la page en
cache avant de l’envoyer au client.
Lors des prochaines requêtes pour cette même page, quel que soit le client, si la copie
enregistrée par le proxy inverse est toujours valable, il la retourne au client sans
interroger l’application.
La particularité du proxy inverse est d’être extrêmement rapide. Il a été
spécialement conçu pour effectuer certaines tâches, comme la gestion du cache ou la
compression des réponses.
a. HttpCache
HttpCache est un proxy inverse développé par Symfony. Ce composant ne nécessite
aucune installation système mais doit être mis en place au niveau du projet.
Il commence par créer le cache. Pour cela, créez par exemple le
fichier CacheKernel.php dans le dossier src/ et mettez-y le code suivant :
// src/CacheKernel.php
namespace App;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class CacheKernel extends HttpCache {
}
require dirname(__DIR__).'/config/bootstrap.php';
if ($_SERVER['APP_DEBUG']) {
umask(0000);
Debug::enable();
}
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
La classe CacheKernel englobe le Kernel ; c’est une simulation d’un proxy inverse au
niveau applicatif.
b. Nginx
Nginx peut être utilisé en tant que proxy inverse de mise en cache. En voici la
configuration minimale :
http {
# ...
proxy_redirect off;
proxy_cache_path /var/cache/http_cache levels=1:2
keys_zone=http_cache:128m;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
server {
listen 80;
server_name mon_projet.local;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_cache http_cache;
}
}
server {
listen 8080;
root /var/www/mon_projet/web;
location / {
try_files $uri @rewriteapp;
}
location @rewriteapp {
# rewrite all to app.php
rewrite ^(.*)$ /app.php/$1 last;
}
location ~ ^/(app|app_dev|config)\.php(/|$) {
fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
}
error_log /var/log/nginx/project_error.log;
access_log /var/log/nginx/project_access.log;
}
}
# sub vcl_pipe {
# # Note that only the first request to the backend will have
# # X-Forwarded-For set. If you use X-Forwarded-For and want
# # to have it set for all requests, make sure to have:
# # set bereq.http.connection = "close";
# # here. It is not set by default as it might break some
# # broken web applications, like IIS with NTLM authentication.
# return (pipe);
# }
#
# sub vcl_pass {
# return (pass);
# }
#
# sub vcl_hash {
# hash_data(req.url);
# if (req.http.host) {
# hash_data(req.http.host);
# } else {
# hash_data(server.ip);
# }
# return (hash);
# }
#
#sub vcl_hit {
# return (deliver);
#}
#
# sub vcl_miss {
# return (fetch);
# }
#
sub vcl_fetch {
if (beresp.ttl <= 0s ||
beresp.http.Vary == "*") {
/*
* Mark as "Hit-For-Pass" for the next 2 minutes
*/
set beresp.ttl = 120 s;
return (hit_for_pass);
}
remove beresp.http.Set-Cookie;
return (deliver);
}
#sub vcl_deliver {
# return (deliver);
#}
#
# sub vcl_error {
# set obj.http.Content-Type = "text/html; charset=utf-8";
# set obj.http.Retry-After = "5";
# synthetic {"
# <?xml version="1.0" encoding="utf-8"?>
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
# "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
# <html>
# <head>
# <title>"} + obj.status + " " + obj.response + {"</title>
# </head>
# <body>
# <h1>Error "} + obj.status + " " + obj.response + {"</h1>
# <p>"} + obj.response + {"</p>
# <h3>Guru Meditation:</h3>
# <p>XID: "} + req.xid + {"</p>
# <hr>
# <p>Varnish cache server</p>
# </body>
# </html>
# "};
# return (deliver);
# }
#
# sub vcl_init {
# return (ok);
# }
#
# sub vcl_fini {
# return (ok);
# }
Chaque section (sub vcl_fetch, sub vcl_recv, etc.) correspond à ce qu’on appelle une
« subroutine ». Les subroutines définissent le comportement de Varnish tout au long
du traitement d’une requête. Elles utilisent un langage spécial : le VCL (Varnish
Configuration Language). Par défaut, toutes les subroutines sont commentées avec le
comportement standard de Varnish
Pour modifier une subroutine, il suffit de la décommenter et d’y placer sa
logique personnalisée. Ici, nous avons modifié les
subroutines vcl_recv et vcl_fetch pour autoriser la mise en cache de pages ayant un
cookie.
Pour plus d’informations sur les subroutines de Varnish : https://www.varnish-
cache.org/docs/3.0/reference/vcl.html#subroutines
Lorsque vous autorisez la mise en cache de pages avec cookie, vous devez faire
particulièrement attention à ne pas mettre en cache des pages protégées. Si le cookie
d’un utilisateur contient une authentification avec un certain niveau d’autorisation, lui
permettant d’accéder à une ressource donnée, et qu’elle est mise en cache, n’importe
quel utilisateur pourrait potentiellement accéder à cette ressource.
Durée de vie (ou Time To Live)
Par défaut, Varnish met en cache toutes les réponses qui ne présentent pas de directive
d’expiration (max-age, s-max-age ou Expires) pendant deux minutes.
Pour désactiver cette option, vous devez spécifier un Time To Live (durée de vie) à
zéro. Le moyen le plus simple consiste à passer une option lors du lancement du
démon Varnish, l’option -t. Vous pouvez l’ajouter dans le
fichier /etc/default/varnish.
3. Les en-têtes
Une fois votre proxy inverse configuré, vous êtes prêt à mettre vos pages en cache.
Comme nous l’avons expliqué au début de ce chapitre, au niveau applicatif, vous
n’aurez pas à stocker ou récupérer quoi que ce soit dans un système de cache car
toutes les techniques de mise en cache utilisent les en-têtes de la requête et/ou de la
réponse HTTP. Toutes ces règles sont définies dans les spécifications du protocole
HTTP et Symfony ne fait que les reprendre.
5. L’expiration
Le modèle d’expiration permet de définir le délai ou la date à laquelle la copie en
cache ne sera plus valable.
a. L’en-tête Expires
L’en-tête Expires spécifie la date d’expiration de la page :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
$response->setExpires($date);
return $response;
}
}
La page est mise en cache par le proxy inverse ainsi que par le client.
b. Les directives max-age et s-max-age
L’en-tête HTTP Cache-Control contient, entre autres, les directives max-age et s-
max-age. Pour configurer ces deux directives, l’objet Response de Symfony fournit
les méthodes setMaxAge() et setSharedMaxAge() :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
$response->setMaxAge(30);
return $response;
}
}
c. L’annotation @Cache
Ajouter les en-têtes de cache à une réponse est parfois fastidieux, comme lorsque la
réponse utilise Twig :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
$response->setMaxAge(30);
Ci-dessus, l’annotation @Template ne peut être utilisée, car elle créerait une réponse
sans les en-têtes, mais grâce à l’annotation @Cache, le code est simplifiable :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
if ($response->isNotModified($this->getRequest())) {
return $response;
}
return $this->render(
'article/voir.html.twig',
array('article' => $article),
$response
);
}
}
La réponse est publique ; elle peut donc être mise en cache par le proxy inverse.
Lors des requêtes suivantes, le proxy inverse ajoute l’en-tête HTTP If-Modified-
Since à la requête du client avant de la transférer à l’application. Le contenu de cet
en-tête est la valeur de Last-Modified pour la réponse en cache dans le proxy inverse.
L’application n’a pas à générer la réponse complète car elle compare la date de
dernière modification de l’article avec la date fournie par le proxy inverse.
Si les dates sont les mêmes ($response->isNotModified($request) renvoie true), la
réponse peut être retournée immédiatement. Symfony renvoie une réponse de statut
304, au contenu vide, et le proxy inverse transmet sa copie en cache au client.
En clair, une nouvelle réponse est générée seulement après modification de l’article.
b. Par empreinte avec l’en-tête ETag
Voici un exemple semblable au précédent, à la différence près que la date de dernière
modification est remplacée par un Etag, une empreinte (ou hash) basée sur le titre et
le contenu de l’article :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
if ($response->isNotModified($this->getRequest())) {
return $response;
}
return $this->render(
'article/voir.html.twig',
array('article' => $article),
$response
);
}
}
7. Les ESI
Souvent, les pages de sites web sont complexes et il est nécessaire d’avoir
différentes stratégies de cache pour les différentes sections de la page.
Par exemple, la page d’un article peut utiliser la stratégie de validation (sur la date de
dernière modification), alors qu’une section avec les derniers tweets du compte du site
devrait se mettre à jour toutes les heures.
Les Edge Side Includes (ou ESI) sont un langage de balisage pour assembler des
pages dynamiquement. Le contenu provient potentiellement de plusieurs sources.
Concrètement, l’application insère des balises ESI dans la page. Ces balises
contiennent une URL, l’emplacement où peut être récupérée la section de page
pertinente. Le proxy inverse, en rencontrant ces balises, est capable d’aller chercher le
contenu et de le substituer à la balise.
Varnish et HttpCache de Symfony prennent en charge les spécifications ESI, ce qui
n’est pas le cas de Nginx.
a. Activation
Le paramètre esi de la configuration du FrameworkBundle doit être activé :
# config/packages/framework.yaml
framework:
# ...
esi: { enabled: true }
Varnish
Avec Varnish, il faut effectuer quelques modifications supplémentaires :
Ajouter l’instruction set req.http.Surrogate-Capability = "abc=ESI/1.0"; dans
la subroutine vcl_recv de votre fichier de configuration Varnish.
Sur ce même fichier, dans la subroutine vcl_fetch, ajouter les instructions
suivantes :
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
set beresp.do_esi = true;
remove beresp.http.Surrogate-Control;
}
{% block body %}
Le contenu suivant a été généré grâce à un ESI :<br />
{{ render_esi(controller(
'App\\Controller\\DefaultController::esi',
{ mon_parametre: 'ma_valeur' }
)) }}
{% endblock %}
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
{% block body %}
Le contenu suivant a été généré grâce à un ESI :<br />
{% endblock %}
Précédent
Quiz
Suivant
L’autochargement des classes
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Générer un classmap
Avec l’option -o de la commande dump-autoload, Composer regénère un chargeur
de classes plus performant : un tableau contenant chaque classe du projet, associée à
son emplacement physique, est créé.
Ce dernier est utilisé par le chargeur de classes pour trouver très rapidement le fichier
à inclure pour charger une classe donnée.
Voici la commande en question :
composer dump-autoload -o
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Le cache d’annotations
Les sessions
Autres optimisations
Test des performances d'un site web
Quiz
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
$query->useResultCache(true, 60);
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Le cache d’annotations
Si vous utilisez les annotations pour des tâches comme la définition de routes ou de
règles de validation, vous serez sûrement intéressé par cette optimisation.
Comme nous l’avons déjà expliqué (cf. chapitre Routage et contrôleur -
Définition des routes), le support des annotations n’est pas natif au langage PHP, il est
fourni par Doctrine et se base sur l’API d’introspection de PHP. Cette dernière n’est
pas destinée à être utilisée en production, mais plutôt destinée à des outils d’analyse
de code ou des tests.
C’est pour cette raison que, par défaut, Doctrine met en cache les annotations dans des
fichiers. Ce système de cache peut toutefois être optimisé.
# config/framework.yaml
framework:
annotations:
cache: mon_cache_doctrine
# config/services.yaml
services:
memcached:
class: Memcached
arguments: ['ma_connexion']
calls:
- [addServer, ["localhost", 11211]]
mon_cache_doctrine:
class: Doctrine\Common\Cache\MemcachedCache
calls:
- [setMemcached, ["@memcached"]]
doctrine:
orm:
metadata_cache_driver
type: service
id: mon_cache_doctrine
result_cache_driver:
type: service
id: mon_cache_doctrine
query_cache_driver:
type: service
id: mon_cache_doctrine
# ...
Cela permet d’instancier un seul objet de cache pour tous ces systèmes.
Précédent
Le cache avec Doctrine
Suivant
Les sessions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Les sessions
Par défaut, les sessions sont enregistrées dans des fichiers. Ce comportement est
facilement modifiable et voici comment sauvegarder les sessions en mémoire partagée
grâce à Memcache :
# config/framework.yaml
framework:
# ....
session:
handler_id: ma_session_memcache
# config/services.yaml
services:
memcached:
class: Memcached
arguments: ['ma_connexion']
calls:
- [addServer, ["localhost", 11211]]
ma_session_memcache:
class:
Symfony\Component\HttpFoundation\Session\Storage\Handler\Memcached
SessionHandler
arguments: [@memcached]
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Autres optimisations
Les optimisations présentées jusqu’à présent sont pour la plupart spécifiques à
Symfony. Néanmoins, un projet sous ce framework n’échappe guère aux
optimisations plus génériques.
La suite de ce chapitre ne concernant pas spécifiquement Symfony, notre
approche sera orientée sur les concepts et la théorie, dans l’objectif de rester concis.
Nous vous suggérerons tout de même des outils adaptés tout au long de nos
explications.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Côté serveur
a. Apache Bench
b. Xhprof
2. Côté client
Quiz
Internationalisation des applications Symfony
Développer une API REST avec Symfony
Annexes
1. Côté serveur
a. Apache Bench
Apache Bench est un outil qui mesure la capacité de votre serveur web à monter en
charge. Il permet de solliciter votre serveur web avec un grand nombre de requêtes.
Si vous utilisez le serveur web Apache, Apache Bench devrait déjà être installé sur
votre machine.
Si vous n’utilisez pas Apache et que vous êtes sur Ubuntu/Debian, vous pouvez le
récupérer en installant le paquet apache2-utils. Si vous êtes sur un autre système
d’exploitation, vous devrez installer Apache.
Utilisation
La commande suivante sollicite votre site web pendant 30 secondes (option -t) avec
un niveau de concurrence de 100 (option -c) :
ab -t 30 -c 100 http://www.mon-projet.demo/
2. Côté client
L’outil Google PageSpeed Insights constitue un intéressant baromètre pour jauger
les performances d’une application web. Il vous suffit de vous rendre à l’adresse
suivante et de renseigner l’URL de votre
site : http://developers.google.com/speed/pagespeed/insights/
Cet outil agit comme un « client intelligent » ; il effectue une requête vers votre site et
analyse le contenu de la réponse, si elle est compressée, etc. Il affiche un compte-
rendu détaillé ainsi que des pistes d’optimisation à explorer.
Précédent
Autres optimisations
Suivant
Quiz
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Introduction
1. Culture, internationalisation et régionalisation
Tout site web contient beaucoup de données textuelles qui sont destinées à être lues
par l’utilisateur. Si l’on retirait tout le code informatique de votre projet, il resterait
tout de même une partie non négligeable de messages, textes et contenus en français,
éparpillés entre vos templates, contrôleurs et bases de données, par exemple.
Si vous souhaitez transformer votre site web exclusivement en français en un site web
multilingue, les couches de l’application à modifier sont donc nombreuses. La tâche
peut rapidement devenir ardue, voire rendre votre projet illisible, si vous ne choisissez
pas les bons outils.
Heureusement, Symfony dispose d’un composant chargé de gérer les traductions d’un
site avec souplesse et simplicité. Avant de le découvrir, il convient tout de même de
définir certains termes.
a. La culture (Locale)
La culture (en anglais Locale) est un identifiant qui représente principalement la
langue de l’utilisateur, mais potentiellement d’autres paramètres linguistiques, comme
le pays.
La valeur de l’identifiant de culture est libre. Cependant, Symfony recommande
l’utilisation du code langue ISO639-1, puis optionnellement, d’un underscore et d’un
code pays ISO3166 Alpha-2.
Voici quelques exemples valides d’identifiants de culture :
fr : un utilisateur français.
fr_CA : un utilisateur français au Canada.
en_US : un utilisateur anglais aux États-Unis.
de : un utilisateur allemand.
b. Internationalisation
L’internationalisation (souvent abrégée i18n, car le mot commence par i, continue
avec 18 caractères et finit par n) désigne l’ensemble des processus qui préparent une
application à être traduite en plusieurs langues.
C’est ce concept que nous allons principalement détailler au cours de ce chapitre.
c. Régionalisation
La régionalisation (localization en anglais, souvent abrégée l10n) consiste à créer
une version d’un site (qui a été préalablement internationalisé) pour une culture
donnée.
Concrètement, cette étape consiste à créer des fichiers avec du texte traduit, ne
nécessite que peu de connaissances informatiques et est souvent réalisée par un
traducteur.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Les techniques
a. Négociation de contenu
b. Par l’URL
2. En pratique
2. En pratique
Au cours de ce chapitre, nous utiliserons la technique de l’URL. C’est ce que
recommande également Symfony. Nous verrons d’ailleurs que le framework dispose
d’un paramètre spécial de routage très utile pour l’internationalisation : _locale.
Précédent
Introduction
Suivant
Activation des traductions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le composant translator
2. Configuration du framework
On y trouve la locale par défaut (default_locale) ainsi que le répertoire par défaut des
traductions (default_path). Ce répertoire est le répertoire translations situé à la racine
de votre projet.
Une première adaptation importante de ce fichier consiste évidemment à ajuster la
locale par défaut de votre application : fr est une bonne valeur si vous souhaitez que
votre application soit nativement en français.
Il est également important de noter le paramètre fallbacks, qui permet d’indiquer une
liste de langues vers laquelle Symfony va se rabattre si jamais une traduction dans la
locale par défaut n’est pas trouvée.
# config/packages/translation.yaml
framework:
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
en
Les traductions sont maintenant activées pour votre site. Pour l’instant, elles vous
permettent uniquement de traduire automatiquement en français le texte de bundles
tiers ainsi que d’avoir les messages d’erreur par défaut du composant validator.
Précédent
Détecter la culture d'un utilisateur
Suivant
Les routes et les traductions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Ici, nous définissons une route multilingue pour l’action home d’un de nos
contrôleurs. Cette action sera accessible par deux URL :
http://mon-projet.local/en/home
http://mon-projet.local/fr/home
En accédant à l’une ou l’autre de ces routes, Symfony place la culture de
l’utilisateur sur en ou fr, pour respectivement anglais et français.
Précédent
Activation des traductions
Suivant
Les fichiers de traductions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Cette vérification s’applique aussi aux composants (bundles) que vous utilisez.
b. Générer un fichier de traduction
L’option --force de la commande translation:update permet la génération de fichiers
de traduction pour votre application. Par défaut, elle génère des fichiers au format
XLIFF, mais il est possible d’agir sur le format de sortie grâce à l’option --output-
format. Par exemple, pour générer le fichier de traduction en français au format
YAML pour l’application, on utilisera la commande :
php bin/console translation:update --force --output-format=yaml fr
En utilisant l’option --help, vous en apprendrez plus sur les autres options de cette
commande.
Précédent
Les routes et les traductions
Suivant
Traduction d'un message
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le service translator
2. Les paramètres de substitution (placeholders)
3. Utilisation dans les templates Twig
Quiz
Développer une API REST avec Symfony
Annexes
...
Bonjour: Hello
Si vous ouvrez cette page avec la culture en dans l’URL, vous verrez « Hello! »
s’afficher à l’écran.
Si ce n’est pas le cas, essayez de vider votre cache.
Le fichier utilise le domaine messages (et donc le fichier messages.en.yaml) car c’est
le domaine par défaut. Voici comment en utiliser un autre au moment de la
traduction :
$this->get('translator')->trans('Bonjour', array(), 'le_domaine');
Ici, c’est le domaine le_domaine qui sera pris en compte. Nous allons
maintenant nous pencher sur le rôle du tableau vide passé en deuxième paramètre.
Si la culture de l’utilisateur courant est en, le message est traduit, malgré sa partie
dynamique.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
D’un point de vue des technologies, l’application cliente peut revêtir plusieurs formes.
Il peut s’agir d’une application mobile sur un smartphone ou une tablette, d’une
application de bureau déployée sur un poste de travail utilisateur ou bien encore d’un
site web distant.
3. Les Single-Page Applications
Une grande majorité des applications et sites web actuels utilisent une approche un
peu différente de l’approche MVC évoquée tout au long de cet ouvrage ; ce sont les
Single-Page Applications. Le principe repose sur l’usage d’une structure HTML
unique pour afficher toutes les informations. Cette structure HTML est
dynamiquement modifiée par du code JavaScript en fonction des actions de
l’utilisateur. Le changement de page est donc transparent pour l’utilisateur (il ne
constate pas de rafraîchissement complet des pages), car seules les données devant
être mises à jour sont modifiées dans la structure HTML.
Ces Single-Page Applications utilisent massivement des frameworks basés sur le
langage JavaScript afin d’optimiser le développement côté client : on parle de
développement FrontEnd.
Ainsi toute la logique de modification de cette structure HTML est pilotée par des
frameworks tels qu’Angular, VueJS ou React. Ils se chargent également d’apporter
des éléments d’interface graphique évolués, simplifiant ainsi la conception des pages.
Afin d’obtenir les données, ces frameworks vont solliciter la plupart du temps une
API REST mise à disposition sur un serveur web, comme illustré précédemment, ce
sont donc des clients à part entière.
Des composants émettent des requêtes HTTP à destination de l’API et reçoivent des
réponses encapsulant les données (souvent en JSON), ces données servant ensuite à
mettre à jour la structure HTML de la page.
Précédent
Quiz
Suivant
La gestion du format JSON
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
b. Types de données
Les types de données définis par JSON sont :
les chaînes de caractères : elles sont exprimées en Unicode et entre guillemets,
les numériques : ce sont des nombres entiers ou décimaux,
les booléens : ils sont exprimés avec les mots réservés true et false,
les objets : ils représentent la base de la notation de JSON,
les tableaux : ce sont des ensembles de valeurs des types précédents.
c. Structures
Les paires de clé/valeur de JSON sont exprimées entre accolades ({}), c’est la
manière de représenter un objet. Ainsi, si l’on souhaite exprimer les données d’un
objet personne qualifié par un nom, un prénom et un âge, on peut utiliser la structure
suivante :
{
"nom": "DUPONT",
"prenom": "Robert",
"age": 56
}
Dans cet exemple, les valeurs pour le nom et le prénom sont des chaînes de
caractères, l’âge est un entier.
Les tableaux sont, quant à eux, exprimés entre crochets ([]), comme par exemple :
...
"clé1": [ "Première valeur de clé1", "Deuxième valeur de clé1" ]
...
Bien évidemment, les valeurs de tableaux peuvent elles-mêmes être des tableaux ou
bien des objets. Prenons l’exemple d’une structure complexe qui représenterait un
objet client possédant des attributs sur son identité, ainsi qu’un attribut comptes ayant
comme valeur un tableau d’objets compte.
On pourrait représenter la structure de la manière suivante :
{
"id": 1,
"nom": "DUPONT",
"prenom": "Robert",
"adresse": "40 rue de la Paix",
"codePostal": "75000",
"ville": "Paris",
"comptes": [
{
"numero": 245646786,
"solde": 8300.0
},
{
"numero": 263434345,
"solde": 20100.0
}
]
}
3. Et dans Symfony ?
Dans Symfony le support de JSON est natif. Il permet aussi bien de manipuler des
flux que de gérer des requêtes et des réponses dans ce format. Symfony utilise pour
cela un sérialiseur.
Un sérialiseur (serializer en anglais) est un outil logiciel permettant de réaliser des
opérations de sérialisation et désérialisation.
La sérialisation est le mécanisme permettant de transformer en flux de données la
structure d’un objet logiciel résidant en mémoire afin de pouvoir stocker ce flux dans
un fichier ou bien de le transporter sur le réseau. La désérialisation est l’opération
inverse : on construit l’état d’un objet en mémoire à partir d’un flux de données
provenant d’un fichier ou bien du réseau.
Symfony vient avec un sérialiseur par défaut ; ce dernier est capable de gérer les
formats XML, YAML, CSV et évidemment JSON.
Précédent
Introduction à REST et concepts fondamentaux
Suivant
Mise en place d’une API REST
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Tout déplier | Tout replier
Informations générales
Introduction
Mise en place d’un projet Symfony
Architecture du framework
Le modèle de conception MVC
Architecture de Symfony
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
Fonctionnement du routage dans Symfony
Définition des routes
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Le modèle de conception IoC : Inversion Of Control
L’injection de dépendances
Le Service Container
Créer un service et configurer ses injections
Le chargement automatique de services
Créer des services réutilisables et distribuables
Quiz
Les templates avec Twig
Présentation et concepts
Twig
Les gabarits de pages (layouts) et les blocks
Le langage Twig
Les filtres et les fonctions
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Quiz
Accéder aux bases de données avec Doctrine
Présentation et concepts
Installer et configurer Doctrine
Définition des entités et de leur mapping
Manipulation des entités avec l’EntityManager
Récupérer des entités
Fonctionnalités avancées
Quiz
La gestion des événements applicatifs
Concepts et écoute d’événement applicatifs
Les événements du Kernel
Les événements de la console
Quiz
Les formulaires
Un composant MVC
Fonctionnement du composant
Les types de champs de formulaire
Créer des formulaires réutilisables
Validation des données
Personnaliser le rendu - thèmes de formulaires
Quiz
La sécurité dans une application Symfony
Présentation et concepts de sécurité
Authentification
Utilisateurs et rôles
Autorisations
Quiz
Tester son application Symfony
Introduction au test logiciel
Les tests unitaires avec PHPUnit
Les tests fonctionnels
Quiz
Journalisation et surveillance avec Symfony
Générer des journaux avec Monolog
Le monitoring avec Prometheus et Grafana
Quiz
Amélioration des performances
La mise en cache de pages
L’autochargement des classes
Le cache avec Doctrine
Le cache d’annotations
Les sessions
Autres optimisations
Test des performances d'un site web
Quiz
Internationalisation des applications Symfony
Introduction
Détecter la culture d'un utilisateur
Activation des traductions
Les routes et les traductions
Les fichiers de traductions
Traduction d'un message
Quiz
Développer une API REST avec Symfony
Introduction à REST et concepts fondamentaux
La gestion du format JSON
Mise en place d’une API REST
1. Le service serializer
a. Sérialiser des données
b. Désérialiser des données
2. Adaptation des contrôleurs
1. Le service serializer
Avant de nous permettre de manipuler des données en JSON, il est nécessaire
d’installer le sérialiseur Symfony. Ce sérialiseur sera ensuite disponible sous forme
d’un service injectable dans les classes de votre application.
Pour installer ce service nommé serializer, il faut exécuter une recette Flex avec la
commande suivante :
composer require symfony/serializer-pack
Une fois installé, le service est injectable dans les actions de contrôleur via
l’interface Symfony\Component\Serializer\SerializerInterface :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Serializer\SerializerInterface;
La variable $article est alors une référence sur un objet de type Article.
Tout le principe d’exposition de fonctionnalités dans une API REST repose donc sur
l’usage de cette annotation.
Précédent
La gestion du format JSON
Suivant
Les objets de requête et de réponse
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
...
L’objet de requête n’est pas systématiquement nécessaire. En effet, il n’est utile que
lorsque des données seront transportées dans le corps de la requête, typiquement sur
des actions liées aux méthodes HTTP PUT et POST.
1. Le contenu et les en-têtes de requête
Lors de la manipulation des données entrantes, il est nécessaire de pouvoir
récupérer les en-têtes et le corps de la requête.
Les en-têtes contiennent notamment le type de données envoyé par le client. Cette
information est accessible via la méthode getContentType().
Dans le cas où des données JSON sont transmises dans la requête, elles le sont dans le
corps ; ce contenu est accessible avec la méthode getContent().
Voici comment pourrait être structuré le code d’une action recevant un flux JSON en
HTTP POST :
/**
* @Route(
* "/api/article",
* name="api_article_creer",
* methods={"POST"}
* )
*/
public function creer(
Request $request, SerializerInterface $serializer
): Response
{
// On contrôle le format de données entrant...
if($request->getContentType() ==='application/json') {
// On récupère le contenu de la requête...
$data = $request->getContent();
// On désérialise en JSON...
$article = $serializer->deserialize(
$data,
Article::class,
"json"
);
...
/**
* @Route(
* "/api/article/{id}",
* name="api_article_lire",
* methods={"GET"}
* )
*/
public function lire($id): Response
{
$article = // Récupération d'un article par son id...
...
/**
* @Route(
* "/api/article/{id}",
* name="api_article_lire",
* methods={"GET"}
* )
*/
public function lire($id): Response
{
$article = // Récupération d'un article par son id...
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Quiz
Annexes
2. Les outils
Les outils de test des API REST ne sont ni plus ni moins que des générateurs de
requêtes HTTP permettant de recevoir et d’explorer des réponses HTTP. En véritables
outils pour le développeur, ils permettent de regrouper les requêtes en projets ainsi
que de spécifier les éventuels en-têtes et contenu de ces requêtes.
a. Postman
Postman est un outil développé par Google et utilisable gratuitement. Il nécessite un
compte Google pour être utilisé ce qui permet de sauvegarder ses projets et de pouvoir
y accéder depuis partout.
Postman peut être utilisé en ligne si votre application est déjà déployée et
disponible sur Internet, ou bien il peut être téléchargé et installé localement sur votre
machine de développement. Le site officiel de Postman est disponible à
l’adresse https://www.postman.com. Le téléchargement de l’application autonome se
fait à partir de https://www.postman.com/downloads/.
Une fois téléchargée et installée, l’application vous demande de vous connecter grâce
à votre compte Google. Quand cela est fait, l’interface de Postman apparaît.
Postman organise les différentes requêtes de test dans des collections, il faut donc
commencer par créer une collection pour votre API. Pour cela, cliquez sur
l’icône Collections dans la barre située à gauche, puis sur le petit bouton + ; la
nouvelle collection apparaît et vous pouvez la nommer à votre guise.
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
Tout déplier | Tout replier
Informations générales
Introduction
Mise en place d’un projet Symfony
Architecture du framework
Le modèle de conception MVC
Architecture de Symfony
Symfony Flex
Les environnements
Le chargement automatique de classes
La console
Les outils pour le débogage
Quiz
Routage et contrôleur
Fonctionnement du routage dans Symfony
Définition des routes
Configurer le path
Routage par nom de domaine
Le contrôleur
Quiz
L’injection de dépendances
Le modèle de conception IoC : Inversion Of Control
L’injection de dépendances
Le Service Container
Créer un service et configurer ses injections
Le chargement automatique de services
Créer des services réutilisables et distribuables
Quiz
Les templates avec Twig
Présentation et concepts
Twig
Les gabarits de pages (layouts) et les blocks
Le langage Twig
Les filtres et les fonctions
La gestion des ressources statiques (images, feuilles de style, scripts JS…)
Quiz
Accéder aux bases de données avec Doctrine
Présentation et concepts
Installer et configurer Doctrine
Définition des entités et de leur mapping
Manipulation des entités avec l’EntityManager
Récupérer des entités
Fonctionnalités avancées
Quiz
La gestion des événements applicatifs
Concepts et écoute d’événement applicatifs
Les événements du Kernel
Les événements de la console
Quiz
Les formulaires
Un composant MVC
Fonctionnement du composant
Les types de champs de formulaire
Créer des formulaires réutilisables
Validation des données
Personnaliser le rendu - thèmes de formulaires
Quiz
La sécurité dans une application Symfony
Présentation et concepts de sécurité
Authentification
Utilisateurs et rôles
Autorisations
Quiz
Tester son application Symfony
Introduction au test logiciel
Les tests unitaires avec PHPUnit
Les tests fonctionnels
Quiz
Journalisation et surveillance avec Symfony
Générer des journaux avec Monolog
Le monitoring avec Prometheus et Grafana
Quiz
Amélioration des performances
La mise en cache de pages
L’autochargement des classes
Le cache avec Doctrine
Le cache d’annotations
Les sessions
Autres optimisations
Test des performances d'un site web
Quiz
Internationalisation des applications Symfony
Introduction
Détecter la culture d'un utilisateur
Activation des traductions
Les routes et les traductions
Les fichiers de traductions
Traduction d'un message
Quiz
Développer une API REST avec Symfony
Introduction à REST et concepts fondamentaux
La gestion du format JSON
Mise en place d’une API REST
Les objets de requête et de réponse
Tester une API REST
Quiz
Annexes
Créer une commande pour la console
namespace App\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
if ($input->hasOption('mon_option')) {
$output->writeln('Mon option a été définie.');
}
}
}
Nous utilisons également l’objet output pour écrire dans la sortie standard : la
méthode writeln() écrit, en ajoutant un saut de ligne automatiquement à la fin.
Pour écrire dans la sortie d’erreur standard, vous devez procéder comme ci-dessous :
namespace App\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
3. Le Service Container
Tout comme les contrôleurs, les commandes sont capables d’accéder au
Service Container. Ceci vous permet de travailler avec vos services, donc de persister
des données en base de données, d’écrire des logs, etc.
Pour accéder au Service Container, vous devez invoquer la
méthode getContainer() de la classe parente de votre commande (à savoir Symfony\
Bundle\FrameworkBundle\Command\ContainerAwareCommand) :
namespace App\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
4. Commande d’exemple
À titre d’exemple, voici une commande affichant les températures minimale et
maximale à Paris, grâce à l’API météorologique de Yahoo
(http://developer.yahoo.com/weather/) :
namespace App\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
$aujourdhui = $rss
->xpath('/rss/channel/item/yweather:forecast[1]')
;
$output->writeln(sprintf(
'Aujourd\'hui, la température oscille entre %s et %s
degrés.',
(string) $aujourdhui[0]['low'],
(string) $aujourdhui[0]['high']
));
}
}
Précédent
Quiz
Suivant
Envoyer des e-mails grâce à SwiftMailer
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le protocole SMTP
2. Le transport
a. Le transport smtp
b. Le transport sendmail
3. Envoi d’un e-mail
1. Le protocole SMTP
Avant de rentrer dans le vif du sujet avec la configuration et l’utilisation de
SwiftMailer, il est important de présenter le protocole SMTP.
Le protocole SMTP (Simple Mail Transfer Protocol) est le protocole de
communication standard utilisé pour l’envoi d’e-mails. Son objectif est de définir les
mécanismes par lesquels une application (appelée MUA, pour Mail User Agent)
soumet un e-mail à un serveur de messagerie (appelé MTA, pour Mail Transfer
Agent).
Pour envoyer des e-mails, votre application a donc besoin d’établir une
communication avec un serveur de messagerie.
Si vous ne disposez pas de votre propre serveur de messagerie (ce qui est fort
probable), rassurez-vous, nous verrons qu’il existe une multitude de serveurs de
messagerie sur lesquels vous pourrez vous connecter gratuitement.
2. Le transport
Voici la configuration par défaut du bundle SwiftMailer stockée dans le
fichier config/packages/mailer.yaml :
framework
mailer:
dsn: '%env(MAILER_DSN)%'
a. Le transport smtp
Le transport smtp est le plus simple à mettre en place. Avec lui, votre application
communique directement avec le serveur de messagerie (au travers d’un socket
TCP/IP et en respectant le protocole SMTP).
La directive host spécifie l’hôte du serveur de messagerie. Elle vaut par défaut
127.0.0.1 (localhost). En la laissant telle quelle, SwiftMailer s’attend à trouver un
serveur de messagerie sur la machine courante.
Si ce n’est pas le cas, vous pouvez très facilement configurer une communication avec
un serveur de messagerie tiers. Pour cela, il existe deux principales solutions :
les fournisseurs d’accès à Internet (FAI) ou les services de messagerie. En effet,
ces derniers proposent des serveurs de messagerie à leurs clients/utilisateurs.
Fournisseurs d’accès à Internet (FAI)
La plupart des fournisseurs d’accès à Internet mettent à disposition de leur clientèle
un serveur de messagerie. C’est par exemple le cas si vous êtes abonnés chez Free.
Vous pouvez alors utiliser ces informations pour construire la vaiable
MAILER_DSN.
Services de messagerie
Il est également possible d’intégrer SwiftMailer à des services de messagerie tels que
Gmail ou Yahoo.
b. Le transport sendmail
Sendmail est un serveur de messagerie qui a la particularité d’offrir une interface plus
simple que le protocole SMTP. Il est principalement utilisé sur les plateformes UNIX.
Pour envoyer un e-mail avec Sendmail, il suffit d’invoquer la commande sendmail :
echo "Voici mon message." | sendmail destinataire@example.com
$this->get('mailer')->send($message);
return array();
}
La méthode setSubject() de l’objet Swift_Message indique le sujet de l’e-mail, la
méthode setFrom() spécifie l’expéditeur, setTo() contient le destinataire, tandis
que setBody() comporte le corps du message.
Les e-mails peuvent être envoyés depuis n’importe quelle partie de votre
application. Vous pouvez injecter le service mailer dans vos autres services qui ont
besoin d’envoyer des e-mails. Au niveau de vos commandes personnalisées, vous
pouvez récupérer le service mailer de cette manière : $this->getContainer()-
>get(’mailer’)
Précédent
Créer une commande pour la console
Suivant
Travailler avec les sessions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Introduction
2. Intégration des sessions dans Symfony
3. Configuration du gestionnaire de sauvegarde
a. Avec PHP
b. Avec Symfony
4. Les messages « flash »
framework:
...
session:
handler_id: null
cookie secure: auto
cookie samesite: lax
...
b. Avec Symfony
Configurer son gestionnaire de sauvegarde avec Symfony permet de rendre
l’application plus portable. Vous aurez la garantie que, quel que soit l’environnement
où votre application est déployée, les sessions seront stockées comme vous le
souhaitez.
Voici comment configurer un gestionnaire de sauvegarde utilisant des fichiers :
# config/packages/framework.yaml
framework:
session:
handler_id: session.handler.native_file
save_path: "/tmp/sessions"
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
// traitement du formulaire...
$this->get('session')->getFlashBag()->add(
'info',
'Votre demande a bien été envoyée.'
);
return $this->redirect($this->generateUrl('accueil'));
}
Template
En pratique, vous aurez plutôt besoin de les afficher au niveau des templates :
{% for flashMessage in app.session.flashbag.get('info') %}
<div class="info">
{{ flashMessage }}
</div>
{% endfor %}
Accueil
Explorer
Cours et livres
Symfony 4 (LTS)
-
Développez des sites web PHP structurés et performants
Sommaire
Notes
Compléments
1. Le déploiement
2. Faut-il déployer par FTP ?
3. Les différentes étapes
4. Capistrano et Capifony
a. Installation
b. Configuration
c. Déploiement
5. Fonctionnalités avancées
5. Fonctionnalités avancées
Cet exemple de déploiement en local depuis le serveur de production a uniquement
valeur d’initiation.
Capifony dispose de nombreuses options de configuration, permettant notamment des
déploiements vers plusieurs machines ou la gestion des migrations de bases de
données. Pour plus d’informations, reportez-vous à la documentation officielle
de Capifony (https://everzet.github.io/capifony/) et/ou à la documentation
de DoctrineMigrationsBundle.
Précédent
Travailler avec les sessions
Conditions générales d'utilisationCopyright - © Editions ENI||Politique de protection des données du groupe
ENI