Programmation PHP avec Symfony/Version imprimable
Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_PHP_avec_Symfony
Introduction
Présentation
modifierSymfony (parfois abrégé SF) est un cadriciel MVC libre écrit en PHP (> 5). En tant que framework, il facilite et accélère le développement de sites et d'applications Internet et Intranet. Il propose en particulier :
- Une séparation du code en trois couches, selon le modèle MVC, pour une plus grande maintenabilité et évolutivité.
- Des performances optimisées et un système de cache pour garantir des temps de réponse optimums.
- Le support de l'Ajax.
- Une gestion des URL parlantes (liens permanents), qui permet de formater l'URL d'une page indépendamment de sa position dans l'arborescence fonctionnelle.
- Un système de configuration en cascade qui utilise de façon extensive le langage YAML.
- Un générateur de back-office et un "démarreur de module" (scaffolding).
- Un support de l'I18N - Symfony est nativement multi-langue.
- Une architecture extensible, permettant la création et l'utilisation de composants, par exemple un mailer ou un gestionnaire de fichiers .css et .js (minification).
- Des bundles :
- Un templating simple, basé sur PHP et des jeux de "helpers", ou fonctions additionnelles pour les gabarits... Comme alternative au PHP, on peut aussi utiliser le moteur de templates Twig dont la syntaxe est plus simples.
- Une couche de mapping objet-relationnel (ORM) et une couche d'abstraction de données (cf. Doctrine et son langage DQL[1]).
Utilisations
modifierPlusieurs autres projets notables utilisent Symfony, parmi lesquels :
- https://github.com/drupal/drupal : le système de gestion de contenu (CMS) Drupal.
- https://github.com/joomla/joomla-cms : le CMS Joomla.
- https://github.com/sulu/sulu : le CMS Sulu.
- https://github.com/Sylius/Sylius : Sylius, un CMS d'e-commerce.
- https://github.com/x-tools/xtools : Xtools, un compteur d'éditions des wikis.
Différences entre les versions
modifierDepuis la version 4, des pages récapitulant les nouvelles fonctionnalités sont mises à disposition :
- https://symfony.com/blog/symfony-4-1-curated-new-features
- https://symfony.com/blog/symfony-4-2-curated-new-features
- https://symfony.com/blog/symfony-4-3-curated-new-features
- https://symfony.com/blog/symfony-4-4-curated-new-features
- https://symfony.com/blog/symfony-5-0-curated-new-features
- https://symfony.com/blog/symfony-5-1-curated-new-features
- https://symfony.com/blog/symfony-5-2-curated-new-features
- https://symfony.com/blog/symfony-5-3-curated-new-features
- https://symfony.com/blog/symfony-6-1-curated-new-features
- https://symfony.com/blog/symfony-6-2-curated-new-features
- https://symfony.com/blog/symfony-6-3-curated-new-features
Créer un projet
modifierVous trouverez sur la page dédiée Symfony 4 comment installer cette version sortie en 2017, et Symfony 3 ici. La v5 s'installe comme la v6, mais pour migrer de la v5 à la v6 il faut vérifier plusieurs choses.
Pour créer un nouveau projet sous Symfony 6, tapez la commande :
composer create-project "symfony/skeleton:^6" mon_projet
ou avec la commande "symfony" :
wget https://get.symfony.com/cli/installer -O - | bash symfony new mon_projet
Cette commande a pour effet la création d'un dossier contenant les bases du site web à développer.
Lancer le projet
modifierOn entend par cette expression le lancement d'un serveur web local pour le développement de l'application et le choix d'un hébergeur pour la déployer (autrement dit "la mettre en production").
Serveur web de développement
modifierSymfony intègre un serveur web local qu'on peut lancer avec la commande (se placer dans le répertoire du projet auparavant) :
$ symfony server:start -d
En passant open:local
en argument de la commande symfony
, le projet s'ouvre dans un navigateur :
$ symfony open:local
Ou bien en utilisant le serveur web intégré à php
$ php -S localhost:8000 -t public
Serveur web de production
modifierPour le déploiement dans le monde "réel", il faut choisir un hébergeur web sur internet supportant PHP (nous l’appellerons "serveur web distant" pour le distinguer du précédent). Voici quelques exemples :
- https://www.lws.fr/hebergement_web.php
- https://www.hostinger.fr/hebergeur-web
- et surtout... https://symfony.com/cloud/
Autrement il est aussi possible d'installer un deuxième serveur web (autre que celui intégré à Symfony) sur sa machine pour se rendre compte du résultat final. Par exemple... Apache qui est très répandu chez les hébergeurs profesionnels. Il faudra alors ajouter un vhost et un nom de domaine dédiés au site Symfony[2][3]. Pour le test, le domaine peut juste figurer dans /etc/hosts
.
Le nom de domaine du site doit absolument rediriger vers le dossier /public. En effet, si on cherche à utiliser le site Symfony dans le sous-répertoire "public" d'un autre site, la page d'accueil s'affichera mais le routing ne fonctionnera pas.
Configurer le projet
modifierParamètres dev et prod
modifierLes différences de configuration entre le site de développement et celui de production (par exemple les mots de passe) peuvent être définies de deux façons :
- Dans le dossier
config/packages
. config.yml contient la configuration commune aux sites, config_dev.yml celle de développement et config_prod.yml celle de production. - Via le composant Symfony/Dotenv (abordé au chapitre suivant).
Par exemple, on constate l'absence de la barre de débogage (web_profiler) par défaut en prod. Une bonne pratique serait d'ajouter au config_dev.yml :
web_profiler:
toolbar: true
intercept_redirects: false
twig:
cache: false
# Pour voir tous les logs dans la console shell (sans paramètre -vvv)
monolog:
handlers:
console:
type: console
process_psr_3_messages: false
channels: ['!event', '!doctrine', '!console']
verbosity_levels:
VERBOSITY_NORMAL: DEBUG
Les fichiers .yml contenant les variables globales sont dans app\config\.
Par exemple en SF2 et 3, le mot de passe et l'adresse de la base de données sont modifiables en éditant parameters.yml
(non versionné et créé à partir du parameters.yml.dist
). L'environnement de test passe par web/app_dev.php, et le mode debug y est alors activé par la ligne Debug::enable();
(testable avec %kernel.debug% = 1
).
Depuis SF4, il faut utiliser un fichier .env non versionné à la racine du projet, dont les lignes sont injectées ensuite dans les .yaml avec la syntaxe : '%env(APP_SECRET)%'
. Le mode debug est activé avec APP_DEBUG=1
dans ce fichier .env.
Les variables d'environnement du système d'exploitation peuvent remplacer celles des .env.
Références
modifier- « Wiki officiel »
- « Tutoriel openclassrooms.com »
- « Tutoriel developpez.com »
- (en) « Symfony 3.1 cookbook » : livre officiel de 500 pages
- (en) « Charming Development in Symfony 5 » (texte et vidéo)
Voir aussi
modifier- #symfony : canal IRC (#symfony sur Freenode)
- #symfony-fr : canal IRC francophone (#symfony-fr sur Freenode)
- https://sonata-project.org/get-started : un CMS basé sur Symfony
Service
Principe
modifierLe principe des services Symfony est d'éviter d'instancier la plupart des classes avec des "new" dispersés dans le code, pour les déclarer une seule fois, grâce au container. Ils sont alors instanciés uniquement s'ils sont utilisés (ex : sur la page web courante), grâce au lazy loading du container[1].
Cette déclaration peut se faire en PHP, en YAML ou en XML. On baptise alors le service (il peut y en avoir plusieurs par classe), et on appelle ses arguments par leur nom de service. Exemple :
services:
app.my_namespace.my_service:
class: App\myNamespace\myServiceClass
arguments:
- '%parameter%'
- '@app.my_namespace.my_other_service'
Pas de include ou require
modifierLes classes natives de PHP doivent être introduites par leur namespace ou bien par l'espace de nom global. Ex :
use DateTime;
echo new DateTime();
ou
echo new \DateTime();
Autowiring
modifierAvant SF2.8, il était obligatoire de déclarer chaque service dans les fichiers de configuration .yml ou .yaml, en plus de leurs classes .php (qui peuvent se contredire), et de les mettre à jour à chaque changement de structure.
Depuis SF2.8, l'"autoconfigure: true" permet de déclarer automatiquement chaque service à partir de sa classe, et l'"autowiring: true" d'injecter automatiquement les arguments connus (ex : une autre classe appelée par son espace de nom et son nom), donc sans déclaration manuelle[2].
Depuis SF4, cette déclaration est par défaut sans le fichier services.yaml, mais on peut la placer dans un autre fichier qui sera importé par le premier, par exemple avec :
imports:
- { resource: services1.yaml }
- { resource: services2.yaml }
ou :
imports:
- { resource: services/* }
Cette séparation des services en plusieurs .yaml nécessite par contre d'exclure les dossiers de ces services de l'autowiring, et de reprendre la section _defaults
dans le nouveau .yaml.
Exemple d'exclusion récursive de plusieurs dossiers de même nom, avec ** :
App\:
resource: '../src/*'
exclude:
- '../src/UnDossier'
- '../src/**/Entity' # Tous les sous-dossiers "Entity"
bind
modifierPar défaut, l'autowiring ne fonctionne pas avec les classes avec des tags, ou ayant autre chose que des services dans leurs constructeurs[3]. Néanmoins pour injecter des scalaires automatiquement, il suffit que ces derniers soit déclarés aussi. Ex :
services:
_defaults:
bind:
$salt: 'ma_chaine_de_caractères'
$variableSymfony: '%kernel.project_dir%'
$variableDEnvironnement: '%env(resolve:APP_DEBUG)%'
_instanceof
modifierPour ajouter un tag ou injecter un service si on implémente une interface. Ex :
services:
_instanceof:
Psr\Log\LoggerAwareInterface:
calls:
- [ 'setLogger', [ '@logger' ] ]
Ici, toutes les classes qui implémentent LoggerAwareInterface
verront leurs méthodes setLogger(LoggerInterface $logger)
appelées automatiquement à l’instanciation.
En SF <2.8
modifierLes contrôleurs sont des services qui peuvent en appeler avec la méthode héritée de leur classe mère :
$this->get('app.my_namespace.my_service')
Pour déterminer si un service existe depuis un contrôleur :
$this->getContainer->hasDefinition('app.my_namespace.my_service')
Paramètres
modifierChaque service doit donc être déclaré avec un paramètre "class", puis peut ensuite facultativement contenir les paramètres suivants :
Nom | Rôle |
---|---|
class | Nom de la classe instanciée par le service. |
arguments | Tableau des arguments du constructeur de la classe, services ou variables. |
calls | Tableau des méthodes de la classe à lancer après l'instanciation, généralement des setters. |
factory | Instancie la classe depuis une autre classe donnée. Méthode statique de la classe qui sera renvoyée par le service[4]. |
configurator | Exécute un invocable donné après l'instanciation de la classe[5]. |
alias | Crée un autre nom pour un service, qui peut alors être modifié par d'autres paramètres de déclaration (ex : créer une version publique d'un service privé dans services_test.yaml[6]). |
parent | Nom de la superclasse. |
abstract | Booléen indiquant si la méthode est abstraite. |
public | Booléen indiquant une portée publique du service. |
shared | Booléen indiquant un singleton. |
tags | Quand on doit injecter un nombre indéterminé de services dans un autre, il est possible de le définir avec chacun des services à injecter, en y ajoutant un tag avec le nom du service qui peut les appeler. Ce tag doit néanmoins être défini dans un CompilerPass[7]. |
autowire | Booléen vrai par défaut, spécifiant si le framework doit injecter automatiquement les arguments du constructeur. |
decorates | Remplace un service par sa version décorée (mais l'ancien est toujours accessible an ajoutant le suffixe .inner au service décorateur)[8] |
Injecter des services tagués
modifierDans un constructeur :
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator app.factory
Dans une autre méthode :
App\Service\FactoriesHandler:
calls:
- [ 'setFactories', [!tagged_iterator app.factory] ]
Par défaut, l'itérateur contient des clés numériques, mais on peut les personnaliser[9]. Ex :
App\Factory\FactoryOne:
tags:
- { name: 'app.factory', my_key: 'factory_one' }
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator { tag: 'app.factory', key: 'my_key' }
Service abstrait
modifierUn service abstrait est un système de factorisation des injections par l'intermédiaire d'une classe abstraite. Par exemple si on veut que tous les contrôleurs héritent du service logger
(comme l'exemple _instanceof
ci-dessus), plus la méthode setLogger()
de leur classe abstraite, sans avoir à toucher à leurs constructeurs :
App\Controller\:
resource: '../src/Controller'
parent: App\Controller\AbstractEntitiesController
tags: ['controller.service_arguments']
App\Controller\AbstractEntitiesController:
abstract: true
autoconfigure: false
calls:
- [ 'setLogger', [ '@logger' ] ]
Références
modifier- ↑ https://symfony.com/doc/3.4/service_container.html
- ↑ https://symfony.com/doc/current/service_container/autowiring.html
- ↑ https://symfony.com/doc/current/service_container/autowiring.html#fixing-non-autowireable-arguments
- ↑ https://symfony.com/doc/current/service_container/factories.html
- ↑ https://symfony.com/doc/current/service_container/configurators.html
- ↑ https://symfony.com/doc/current/testing.html
- ↑ https://symfony.com/doc/current/service_container/compiler_passes.html
- ↑ https://symfony.com/doc/current/service_container/service_decoration.html
- ↑ https://symfony.com/doc/5.4/service_container/tags.html#tagged-services-with-index
Contrôleur
Principe
modifierLes contrôleurs Symfony sont les classes qui définissent les opérations à réaliser quand on visite les pages du sites[1] : elles transforment une requête HTTP en réponse (JSON, XML (dont HTML), etc.).
Par convention, leurs noms se terminent par Controller, les noms de leurs méthodes se terminent par "Action", et les URL qui provoquent leurs exécutions sont définies dans leurs annotations. L'exemple suivant affiche un texte quand on visite l'adresse "/" ou "/helloWorld" :
class HelloWorldController extends AbstractController
{
#[Route(path: '/', name: 'helloWorld')]
#[Route(path: '/helloWorld', name: 'helloWorld')]
public function indexAction(Request $request): Response
{
return new Response('Hello World!');
}
}
NB : en PHP < 8, remplacer l'attribut par une annotation :
/** * @Route("/", name="helloWorld") * @Route("/helloWorld") */
Retours
modifierCes méthodes peuvent déboucher sur plusieurs actions :
Response()
: affiche un texte, et facultativement un code HTTP en deuxième paramètre (ex : erreur 404).JsonResponse()
ou$this->json()
: affiche du JSON.RedirectResponse()
: renvoie vers une autre adresse. Si elle se trouve dans la même application, on peut aussi utiliser le$this->forward()
hérité du contrôleur abstrait.BinaryFileResponse()
: renvoie un fichier à télécharger (à partir de son chemin).
$this->redirect('mon_url')
: redirige à une autre adresse.$this->redirectToRoute('nom_de_la_route');
: redirige vers une route du site par son nom.$this->generateUrl('app_mon_chemin', []);
: redirige vers une URL relative (ajouterUrlGeneratorInterface::ABSOLUTE_URL
en paramètre 3 pour l'absolue, car il est àUrlGeneratorInterface::ABSOLUTE_PATH
par défaut dans SF3).$this->container->get('router')->generate('app_mon_chemin', ['paramètre' => 'mon_paramètre']);
.$this->render()
: affiche une page à partir d'un template, par exemple HTML ou Twig.
$response = new JsonResponse(); $response->setEncodingOptions(JSON_UNESCAPED_UNICODE); $response->setData($data); return $response;
Requêtes
modifierL'objet Request est à préférer à la variable superglobale $_REQUEST, car il fournit une sécurité et des méthodes de manipulation. Ex :
- $request->getMethod() : la méthode HTTP utilisée.
- $request->query : les arguments $_GET (query param).
- $request->request : les arguments $_POST (lui préférer $request->getContent()).
- $request->files : les fichiers $_FILES (dans un itérable FileBag).
ParamConverter
modifierOn peut injecter un ID dans l'URL ou la requête pour le CRUD d'une entité, mais grâce au paramConverter on peut aussi injecter directement l'entité. Ex :
#[Route('/my_entity/{id}', methods: ['GET'])] public function getProduct(MyEntity $myEntity): JsonResponse { return new JsonResponse($myEntity); }
Avant Symfony 6.2 cela fonctionne avec un composer require sensio/framework-extra-bundle
.
Flashbag
modifierOn peut aussi ajouter un bandeau de message temporaire en en-tête via :
$this->addflash('success', 'mon_message');
Le Twig peut les récupérer ensuite avec[2] :
{% for flashMessage in app.session.flashbag.get('success') %} {{ flashMessage }} {% endfor %}
En effet, ils sont stockés dans un Flashbag : un objet de session.
De plus, il en existe plusieurs types (chacun avec une couleur) : success, notice, info, warning, error.
Le fait de lire les flash (au moins depuis les Twig avec app.flashes) vide leur tableau.
Accès aux paramètres et services
modifierLes contrôleurs étendent la classe abstraite Symfony\Bundle\FrameworkBundle\Controller\AbstractController
. Cela leur permettait entre autres dans Symfony 2, de récupérer les services et paramètres ainsi :
dump($this->get('session'));
dump($this->getParameter('kernel.project_dir'));
Depuis Symfony 4, il faut injecter le service service_container pour accéder à la liste des services publiques (public: true
en YAML), mais la bonne pratique est d'injecter uniquement les services nécessaires dans le constructeur[3][4].
Les paramètres sont ceux des fichiers .yml du dossier "config", mais plusieurs autres paramètres sont fournis par Symfony :
bin/console debug:container --parameters
- kernel.debug : renvoie vrai si le site est en préprod et faux en prod.
- kernel.project_dir : dossier racine (qui contient bin/, config/, src/, var/, vendor/).
- kernel.build_dir.
- kernel.cache_dir.
- kernel.logs_dir.
- kernel.root_dir : deprecated en SF5.3. Chemin du site dans le système de fichier.
- kernel.bundles : liste JSON des bundles chargés.
Routing
modifierPar exemple pour créer une nouvelle page sur l'URL :
http://localhost:8000/test
Installer le routage :
composer require sensio/framework-extra-bundle composer require symfony/routing
Par défaut, la page renvoie l'exception No route found for "GET /test". Pour la créer, il faut d'abord générer un fichier contrôleur (rôle MVC), qui fera le lien entre les URL, les données (modèle) et les pages (vue).
Les URL définies dans l'attribut (ou l'annotation) "route" d'une méthode exécuteront cette dernière :
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class TestController extends AbstractController
{
#[route('/test/{numero}', name: 'test', requirements: ['id' => '\d*'], methods: ['GET', 'POST'], priority: -1)]
public function HelloWorldAction(int $numero = 0)
{
return new Response('Hello World! '.$numero);
}
}
NB : en PHP < 8, remplacer l'attribut par une annotation :
/** * @Route("/test/{numero}", name="test", requirements={"id"="\d*"}, methods={"GET|POST"}, priority=-1) */
Autres exemples de prérequis :
requirements={"id"="fr|en"}
requirements={"id"="MaClasse::MA_CONSTANTE1|MaClasse::MA_CONSTANTE2"}
requirements={"id"="(?!api/doc|_profiler).*"}
if ($request->get('_route') === 'test')
.Alias
modifierPour créer des alias, c'est-à-dire plusieurs autres URL pointant vers la page ci-dessus, on peut l'ajouter dans les annotations des contrôleurs, ou bien dans config/routes.yaml (anciennement app\config\routing.yml sur Symfony < 4) :
test:
path: /test/{numero}
defaults: { _controller: AppBundle:Test:HelloWorld }
À présent http://localhost:8000/test/1 ou http://localhost:8000/test/2 affichent "Hello World!".
- Une fois le YAML sauvegardé, l'URL fournie en annotation (/test) ne fonctionne plus.
- S'il y a des annotations précédant @Route dans le même bloc, cela peut inhiber son fonctionnement.
Redirection vers la dernière page visitée
modifierUne astuce pour rediriger l'utilisateur vers la dernière page qu'il avait visité :
$router = $this->get('router');
$lastPage = $request->getSession()->get('last_view_page');
$parameterLastPage = $router->match($lastPage);
$routeLastPage = $parameterLastPage['_route'];
unset($parameterLastPage['_route']); // Pour ne pas la voir dans l'URL finale
return $this->redirect(
$this->generateUrl($routeLastPage, $parameterLastPage)
);
Annotations.yaml
modifierCe fichier permet de définir des groupes de contrôleurs, dont les routes sont préfixées. Ex :
back_controllers:
resource: ../../src/Controller/BackOffice
type: annotation
prefix: admin
front_controllers:
resource: ../../src/Controller/FrontOffice
type: annotation
prefix: api
Dans le cas où les contrôleurs ont des contrôles d'accès différents dans security.yaml, il est impératif de les préfixer ainsi pour éviter toute collision des gardiens.
Paramètres spéciaux
modifierIl existe quatre paramètres spéciaux que l'on peut placer dans routes.yaml ou en argument des méthodes des contrôleurs[6] :
- _controller : contrôleur appelé par le chemin.
- _ format : format de requête (ex : html, xml).
- _fragment : partie de l'URL après "#".
- _locale : langue de la requête (code ISO, ex : fr, en).
Exemple :
#[Route('/controller_route', requirements: ['_locale' => 'en|fr'])] class MyController extends AbstractController
Vue
modifierPour commencer à créer des pages plus complexes, il suffit de remplacer :
return new Response('Hello World!');
par une vue issue d'un moteur de template. Celui de Symfony est Twig :
return $this->render('helloWorld.html.twig');
Pour installer les bibliothèques JavaScript qui agiront sur ces pages, se positionner dans /public. Exemple :
cd public/
sudo apt-get install npm
npm install --save jquery
npm install --save bootstrap
Ensuite il suffit de les appeler dans /templates/helloWorld.html.twig pour pouvoir les utiliser :
<link rel="stylesheet" href="{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}">
<script type="text/javascript" src="{{ asset('node_modules/jquery/dist/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('node_modules/bootstrap/dist/js/bootstrap.min.js') }}"></script>
Modèle
modifierPour gérer le modèle du MVC, c'est-à-dire la structure des données stockées, l'ORM officiel de Symfony se nomme Doctrine.
Par défaut, ses classes sont :
- src/Entity : les entités, reflets des tables.
- src/Repository : les requêtes SELECT SQL (ou find MongoDB).
Tester un contrôleur
modifier- Pour plus de détails voir : Programmation PHP avec Symfony/HttpClient#Tests.
Références
modifier- ↑ https://symfony.com/doc/current/controller.html
- ↑ https://stackoverflow.com/questions/14449967/symfony-setting-flash-and-checking-in-twig
- ↑ https://symfony.com/doc/current/best_practices.html#use-dependency-injection-to-get-services
- ↑ https://symfony.com/doc/4.0/best_practices/controllers.html#fetching-services
- ↑ https://symfony.com/doc/current/security/voters.html
- ↑ https://symfony.com/doc/current/routing.html#special-routing-parameters
Commande
Principe
modifierLes commandes sont, avec les contrôleurs, les seuls points d'entrée permettant de lancer le programme. Ce sont aussi des services mais elles se lancent via la console (en CLI).
La liste des commandes disponibles en console est visible avec :
- Sur Linux :
bin\console
- Sur Windows :
php bin\console
Dans Symfony 2 c'était php app\console
.
Parmi les principales commandes natives au framework et à ses bundles, on trouve :
php bin/console list
: liste toutes les commandes du projet.php bin/console debug:router
: liste toutes les routes (URL) du site.php bin/console debug:container
: liste tous les services avec leurs alias (qui sont des instanciations des classes).php bin/console debug:container --parameters
: liste les paramètres.php bin/console debug:container --env-vars
: liste les variables d'environnement.php bin/console debug:autowiring --all
: liste tous les services automatiquement déclarés.php bin/console debug:config NomDuBundle
: liste tous les paramètres disponibles pour paramétrer un bundle donné. Ex :bin/console debug:config FrameworkBundle
php bin/console cache:clear
: vide la mémoire cache du framework.php bin/console generate:bundle
: crée un bunble (surtout pour SF2).php bin/console generate:controller
: crée un contrôleur (en SF2).php bin/console doctrine:migrations:generate; chown 1001:1001 -R app/DoctrineMigrations
: génère un fichier vide de migration SQL ou DQL.php bin/console doctrine:migrations:list
: liste les noms des migrations disponibles (utiles car selon la configuration on doit les appeler par leur namespace ou juste par numéro).
Toutes les commandes peuvent être abrégées, par exemple "doctrine:migrations:generate" fonctionne avec "d:m:g" ou "do:mi:ge".
Créer une commande
modifierLors du lancement d'une commande, on distingue deux types de paramètres[1] :
- Les arguments : non nommés
- Les options : nommées.
Exemple :
bin/console app:ma_commande argument1 --option1=test
#[AsCommand(name: 'app:ma_commande')]
class HelloWorldCommand extends Command
{
protected function configure(): void
{
$this
->addArgument(
'argument1',
InputArgument::OPTIONAL,
'Argument de test'
)
->addOption(
'option1',
null,
InputOption::VALUE_OPTIONAL,
'Option de test'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
echo 'Hello World! '.$input->getOption('option1').' '.$input->getArgument('argument1');
return self::SUCCESS;
}
}
NB : en SF < 6.1, remplacer l'attribut AsCommand par une propriété connue de la classe mère :
protected static $defaultName = 'app:ma_commande';
Pour définir un argument tableau, utiliser InputArgument::IS_ARRAY
et séparer les valeurs par un espace. Ex :
bin/console app:my_command arg1.1 arg1.2 arg1.3
Ajout de logs
modifierPour que la commande logue ses actions, la documentation de Symfony propose deux solutions[2] :
- $output->writeln()
- $io = new SymfonyStyle($input, $output);
Cette deuxième option permet aussi d'afficher une barre de progression, ou d'interagir avec l'utilisateur :
$io->confirm(Êtes vous sûr de vouloir faire ça ? (Yes/No)'); $io->choice('Choisissez l\'option', ['première ligne', 'toutes les lignes'])
Ensuite il y a plusieurs niveaux de log pouvant colorer la console qui le permet :
$io->info('Commentaire'); $io->success('Succès'); $io->warning('Warning'); $io->error('Echec');
Toutefois ce n'est pas conforme à la PSR3[3] et si on veut utiliser ces logs comme ceux des autres services (pour les stocker ailleurs par exemple), mieux vaut utiliser LoggerInterface $logger
(en plus c'est horodaté).
Pour affichage les logs dans la console, utiliser le paramètre -v :
- -v affiche tous les logs "NOTICE" ou supérieurs.
- -vv les "INFO".
- -vvv les "DEBUG", c'est le mode le plus verbeux possible.
Tester une commande
modifierEx :
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @see https://symfony.com/doc/current/console.html#testing-commands
*/
class CommandTest extends KernelTestCase
{
public function testExecute()
{
$kernel = self::bootKernel();
$monService = static::getContainer()->get('test.mon_service_public'); // En Symfony 6.3 on n'est plus obligé de créer un service public pour le test
$application = new Application($kernel);
$command = $application->find('app:ma_commande');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'--option1' => 'option1',
'--dry-run' => 'true',
]
);
$commandTester->assertCommandIsSuccessful();
$output = $commandTester->getDisplay();
$hasErrors = str_contains($output, 'ERROR');
$this->assertFalse($hasErrors, $output);
}
Le getDisplay affiche ce que l'on voit sur le dernier écran de la console (cela n'affiche pas tout l'output). Pour voir les logs de Monolog, il faut ajouter les lignes suivantes dans la commande[4] :
if ($this->logger instanceof Logger) { $this->logger->pushHandler(new ConsoleHandler($output)); }
Références
modifier
Composant
Description
modifierLe framework Symfony permet nativement les fonctionnalités minimum dans un souci de performances, à l'instar d'un micro-framework. Par exemple son compilateur permet d'utiliser plusieurs patrons de conception (design patterns) via des mots réservés dans services.yaml :
- arguments : Injection de dépendance
- decorator : Décorateur.
- shared : Singleton.
- factory : Fabrique[1].
Toutefois, on peut lui ajouter des composants[2], dont il convient de connaitre les fonctionnalités pour ne pas réinventer la roue. Pour les installer :
composer require symfony/nom_du_composant
Les quatre premiers ci-dessous sont inclus par défaut dans le microframework symfony/skeleton
.
framework-bundle
modifierStructure la configuration principale du framework sans laquelle aucun composant n'est installable[3].
console
modifierPatrons de conception "Commande".
Fournit la possibilité d'exécuter le framework avec des commandes shell[4]. Par exemple pour obtenir la liste de toutes les commandes disponibles dans un projet :
php bin/console help list
dotenv
modifierGère les variables d'environnement non versionnées, contenues dans un fichier .env[5]. Elles peuvent aussi bénéficier de type checking en préfixant les types avec ":". Ex de .env :
IS_DEV_SERVER=1
Le services.yaml, parameters: récupère ensuite cette valeur et vérifie qu'il s'agit d'un booléen (via le processeur de variable d'environnement "bool") :
is_dev_server: '%env(bool:IS_DEV_SERVER)%'
Il existe plusieurs processeurs de variable d'environnement (en plus de "bool" et des autres types)[6] :
base64:
encode en base64.default:
remplace le deuxième paramètre par le premier si absent. Ex :$addTestValues: '%env(bool:default::ADD_TEST_VALUES)%'
injecte "null" si ADD_TEST_VALUES n'est pas défini.$addTestValues: '%env(bool:default:ADD_TEST_VALUES2:ADD_TEST_VALUES1)%'
injecte le contenu de ADD_TEST_VALUES2 si ADD_TEST_VALUES1 n'est pas défini.
file:
remplace le chemin d'un fichier par son contenu.not:
renvoie l'inverse.require:
fait un require() PHP.resolve:
remplace le nom d'une variable par sa valeur.trim:
fait un trim() PHP.
Pour définir une valeur par défaut en cas de variable d'environnement manquante (sans utiliser default:
), dans services.yaml, parameters: :
env(MY_MISSING_CONSTANT): '0'
yaml
modifierAjoute la conversion de fichier YAML en tableau PHP[7]. Ce format de données constitue une alternative plus lisible au XML pour renseigner la configuration des services. Par défaut le framework se configure avec config.yaml.
routing
modifierpatron de conception "Façade".
Installe les annotations permettant de router des URLs vers les classes des contrôleurs MVC.
- Pour plus de détails voir : Programmation PHP avec Symfony/Contrôleur#Routing.
serializer
modifierPermet de convertir des objets en tableaux ou dans les principaux formats de notation : JSON, XML, YAML et CSV[8].
composer require symfony/serializer
Ce composant est notamment utilisé pour créer des APIs.
form
modifierConstruit des formulaires HTML.
- Pour plus de détails voir : Programmation PHP avec Symfony/Formulaire.
validator
modifierFournit des règles de validation pour les données telles que les adresses emails ou les codes postaux. Utile à coupler avec les formulaires pour contrôler les saisies.
Ces règles peuvent porter sur les propriétés ou les getters.
Il permet aussi de créer des groupes de validateurs, et de les ordonner par séquences. Par défaut chaque classe a automatiquement deux groupes de validateurs : "default" et celui de son nom. Si une séquence est définie, le groupe "default" n'est plus égal au groupe de la classe (celui par défaut) mais à la séquence par défaut[9].
Exemples
modifier- Dans une entité :
use Symfony\Component\Validator\Constraints as Assert; ... #[Assert\Email] private ?string $email = null;
- Dans un formulaire (inutile à faire si c'est déjà dans l'entité) :
use Symfony\Component\Validator\Constraints\Email; ... $builder->add('email', EmailType::class, [ 'required' => false, 'constraints' => [new Email()], ])
translation
modifierLes traductions sont stockées dans un fichier différent par domaine et par langue (code ISO 639). Les formats acceptés sont YAML, XML, PHP[10].
On peut ensuite récupérer ces dictionnaires en Twig (via le filtre "trans"), ou en PHP (via le service "translator").
Par exemple, le domaine par défaut étant "messages", le français se trouve donc dans translations/messages.fr.yml
ou translations/messages.fr-FR.yml
.
Installation
modifiercomposer require symfony/translation
Pour avoir les traductions inutilisées en anglais :
bin/console debug:translation en --only-unused
Pour les traductions manquantes en anglais :
bin/console debug:translation en --only-missing
On peut restreindre à un seul domaine avec une option : --domain=mon_domaine
Traduction en PHP
modifierLe domaine et la langue sont facultatifs (car ils ont des valeurs par défaut) :
$translator->trans('Hello World', domain: 'login', locale: 'fr_FR');
Traduction en Twig
modifierLes traductions en Twig sont appelées par le filtre "trans" :
{% trans_default_domain 'login' %} {{ 'Hello World' |trans }}
Ou :
{{ 'Hello World' |trans({}, 'login', 'fr-FR') }}
Variables
modifierHello World: 'Hello World name!'
- Twig :
{{ Hello World |trans({"name": userName}) }}
- PHP
$translator->trans('Hello World', ['name' => $userName]);
Dans un formulaire Symfony :
$builder ->add('hello', TextType::class,([ 'label' => 'Hello World', 'label_translation_parameters' => [ 'name' => $userName, ] ])) ;
Par défaut le domaine de traduction est "message" mais on peut désactiver ces dernières avec : choice_translation_domain => false
.
event-dispatcher
modifierPatrons de conception "Observateur"[12] et "Médiateur"[13].
Assure la possibilité d'écouter des évènements pour qu'ils déclenchent des actions.
- Pour plus de détails voir : Programmation PHP avec Symfony/Évènement.
process
modifierPermet de lancer des sous-processus en parallèle[14]. Exemple qui lance une commande shell :
$process = new Process(['ls']);
$process->run();
En l'absence de $process->stop()
ou de timeout, le sous-processus peut être stoppé en redémarrant le serveur PHP.
Exemple de requête SQL asynchrone[15] :
$sql = 'SELECT * FROM ma_table LIMIT 1';
$process = Process::fromShellCommandline(sprintf('../bin/console doctrine:query:sql "%s"', $sql));
$process->setTimeout(3600);
$process->start();
cache
modifierGère les connexions, lectures et écritures vers des serveurs de mémoire caches tels que Redis ou Memcached.
Il fournit une classe cacheItem conforme à la PSR, instanciable par plusieurs adaptateurs.
Le cache ne sert qu'à accélérer l'application donc une panne sur celui-ci ne doit pas la bloquer. C'est pourquoi il vaut mieux avoir un ou plusieurs caches de secours, même moins rapides, pour prendre le relais dans une chaine de caches.
Pour mettre cela en place sur Symfony, définir le chaine et ses composants dans cache.yaml.
- Pour plus de détails voir : Programmation PHP/Redis#Dans Symfony.
asset
modifierAjoute la fonction Twig asset()
pour accéder aux fichiers CSS, JS ou images selon leurs versions[16].
webpack-encore
modifierIntégration de Webpack pour gérer la partie front end (ex : minifications des CSS et JS).
composer require symfony/webpack-encore-bundle yarn install yarn build
NB : si Yarn n'est pas installé, le faire avec npm : apt install nodejs npm; npm install --global yarn
.
Cela crée les fichiers package.json et yarn.lock contenant les dépendances JavaScript, le dossier assets/ contenant les JS et CSS versionnés, et le fichier webpack.config.js dans lequel ils sont appelés.
De plus, des fonctions Twig permettent d'y accéder depuis les templates : encore_entry_link_tags()
et encore_entry_script_tags()
.
Par ailleurs, cela installe le framework JS Stimulus, et interprète les attributs de données pour appeler ses contrôleurs ou méthodes.
Rebuild
modifierPour que le code se build en cours de frappe, deux solutions[18] :
- Avec Yarn :
- yarn watch
- yarn dev-server
- Avec npm :
- npm watch
- npm run dev-server
La différence entre les deux est que le dev-server peut mettre à jour la page sans même la rafraichir.
messenger
modifierPatrons de conception "Chaîne de responsabilité".
Messenger permet d'utiliser des queues au protocole AMQP. En résumé, il gère l'envoi de messages dans des bus, ces messages transitent par d'éventuels middlewares puis arrivent à destination dans des handlers[19]. On peut aussi persister ces messages en les envoyant dans des transports via un DSN, par exemple dans RabbitMQ, Redis ou Doctrine (donc une table des SGBD les plus populaires).
php bin/console debug:messenger
Chaque middleware doit passer le relais au suivant ainsi :
return $stack->next()->handle($envelope, $stack);
Pour stopper le message dans un middleware sans qu'il arrive aux handlers :
return $envelope;
workflow
modifierCe composant nécessite de créer (en YAML, XML ou PHP) la configuration d'un automate fini[20], c'est-à-dire la liste de ses transitions et états (appelés "places").
Ces graphes sont ensuite visualisables en image ainsi :
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\StateMachineGraphvizDumper;
class WorkflowDisplayer
...
$definition = new Definition($places, $transitions);
echo (new StateMachineGraphvizDumper())->dump($definition);
sudo apt install graphviz
php WorkflowDisplayer.php | dot -Tpng -o workflow.png
browser-kit
modifierSimule un navigateur pour les tests d'intégration.
config
modifierPermet de manipuler des fichiers de configurations.
contracts
modifierPour la programmation par contrat.
css-selector
modifierPour utiliser XPath.
debug
modifierFournit des méthodes statiques pour déboguer le PHP.
dependency-injection
modifierNormalise l'utilisation du container de services.
Permet aussi d'exécuter du code pendant la compilation via un compiler pass, en implémentant l'interface CompilerPassInterface avec sa méthode process[21].
dom-crawler
modifierFournit des méthodes pour parcourir le DOM.
expression-language
modifierPatrons de conception "Interpréteur".
Expression language sert à évaluer des expressions, ce qui peut permettre de définir des règles métier[22].
Installation :
composer require symfony/expression-language
Exemple :
$el = new ExpressionLanguage();
$operation = '1 + 2';
echo(
sprintf(
"L'opération %s vaut %s",
$el->compile($operation));
$el->evaluate($operation));
)
);
// Affiche : L'opération 1 + 2 vaut 3
filesystem
modifierMéthodes de lecture et écriture dans les dossiers et fichiers.
finder
modifierRecherche dans les dossiers et fichiers.
security
modifierEnsemble de sous-composants assurant la sécurité d'un site. Ex : authentification, anti-CSRF ou droit des utilisateurs d'accéder à une page.
Dans security.yaml, on peut par exemple définir les classes qui vont assurer l'authentification (guard), ou celle User qui sera instanciée après.
Pour obtenir l'utilisateur ou son token, on peut injecter :
TokenStorageInterface $tokenStorage
pour avoir l'utilisateur courant avec $this->tokenStorage->getToken()->getUser()
.
guard
modifierExtension de sécurité pour des authentifications complexes.
http-client
modifierPour lancer des requêtes HTTP depuis l'application.
- Pour plus de détails voir : Programmation PHP avec Symfony/HttpClient.
http-foundation
modifierFournit des classes pour manipuler les requêtes HTTP, comme Request et Response que l'on retrouve dans les contrôleurs.
Par exemple :
use Symfony\Component\HttpFoundation\Response;
//...
echo Response::HTTP_OK; // 200
echo Response::HTTP_NOT_FOUND; // 404
http-kernel
modifierPermet d'utiliser des évènements lors des transformations des requêtes HTTP en réponses.
inflector
modifierDeprecated depuis Symfony 5.
Accorde les mots anglais au pluriel à partir de leurs singuliers.
intl
modifierInternationalisation, comme par exemple la classe "Locale" pour gérer une langue.
ldap
modifierConnexion aux serveur LDAP.
lock
modifierPour verrouiller les accès aux ressources[23].
Par exemple, pour ne pas qu'une commande soit lancée deux fois simultanément, bien que le composant console aie aussi cette fonctionnalité :
use Symfony\Component\Console\Command\LockableTrait; ... protected function execute(InputInterface $input, OutputInterface $output): int { if ($this->lock() === false) { return Command::SUCCESS; } ... $this->release(); return Command::SUCCESS; }
Maker bundle
modifierPour créer ou recréer des classes à partir de déductions[24].
composer require --dev symfony/maker-bundle
NB : ce composant ne permet pas de générer des entités à partir d'une base de données.
mailer
modifierPour envoyer des emails.
mime
modifierManipulation des messages MIME.
notifier
modifierPour envoyer des notifications telles que des emails, des SMS, des messages instantanés, etc.
options-resolver
modifierGère les remplacements de propriétés par d'autres, avec certaines par défaut.
phpunit-bridge
modifierPatron de conception "Pont" qui apporte plusieurs fonctionnalités liées aux tests unitaires, telles que la liste des tests désuets ou des mocks de fonctions PHP natif.
property-access
modifierPour lire les attributs de classe à partir de leurs getters, ou des tableaux.
property-info
modifierPour lire les métadonnées des attributs de classe.
stopwatch
modifierChronomètre pour mesurer des temps d'exécution.
string
modifierAPI convertissant certains objets en chaine de caractères. Ex :
use Symfony\Component\String\Slugger\AsciiSlugger;
$slugger = new AsciiSlugger();
echo $slugger->slug('caractères spéciaux € $');
Résultat :
caracteres-speciaux-EUR
templating
modifierExtension de construction de templates.
- Pour plus de détails voir : Programmation PHP avec Symfony/Templating.
var-dumper
modifierAjoute une fonction globale dump()
pour déboguer des objets en les affichant avec une coloration syntaxique et des menus déroulant.
Ajoute aussi dd()
pour dump() and die().
var-exporter
modifierPermet d'instancier une classe sans utiliser son constructeur.
polyfill*
modifierOn trouve aussi une vingtaine de composants polyfill, fournissant des fonctions PHP retirées dans les versions les plus récentes.
Composants désuets
modifierlocale (<= v2.3)
modifierArrêté en 2011, car remplacé par le composant intl[25].
icu (<= v2.6)
modifierArrêté en 2014, car remplacé par le composant intl[26].
class-loader (<= v3.3)
modifierArrêté en 2011, car remplacé par composer.json[27].
Ajoutés en 2020
modifierUid (sic) (>= v5.1)
modifierRateLimiter (>= v5.2)
modifierPatron de conception "Proxy", qui permet de limiter la consommation de ressources du serveur par les clients[29]
Semaphore (>= v5.2)
modifierPour donner l'exclusivité d'accès à une ressource[30].
Ajoutés en 2021
modifierPasswordHasher (>= v5.3)
modifierPour gérer les chiffrements[31].
Runtime (>= v5.3)
modifierPour le démarrage (bootstrap) : permettre de découpler l'application de son code de retour. [32].
Ajoutés en 2022
modifierHtmlSanitizer (>= v6.1)
modifierClock (>= v6.2)
modifierSymfony UX (>= v5.4)
modifierux-autocomplete
modifierux-chartjs
modifierUtilise Chart.js via Stimulus pour afficher des graphiques, via la fonction Twig render_chart()
[33].
ux-react
modifierAjoute le framework React.js.
- Pour plus de détails voir : Programmation PHP avec Symfony/Stimulus.
ux-vue
modifierAjoute le framework Vue.js.
Ajoutés en 2023
modifierWebhook et RemoteEvent (>= v6.3)
modifierAssetMapper (>= v6.3)
modifierScheduler (>= v6.3)
modifierComposants non listés comme tels
modifierapache-pack
modifierPour faire tourner le site sans passer par le serveur symfony server:start
.
Références
modifier- ↑ https://symfony.com/doc/current/service_container/factories.html
- ↑ https://symfony.com/components
- ↑ https://symfony.com/doc/current/reference/configuration/framework.html
- ↑ https://symfony.com/doc/current/components/console.html
- ↑ https://symfony.com/doc/current/components/dotenv.html
- ↑ https://symfony.com/doc/current/configuration/env_var_processors.html
- ↑ https://symfony.com/doc/current/components/yaml.html
- ↑ https://symfony.com/doc/current/components/serializer.html
- ↑ https://symfony.com/doc/current/validation/sequence_provider.html
- ↑ https://symfony.com/doc/current/translation.html
- ↑ https://symfony.com/doc/current/reference/formats/message_format.html
- ↑ http://www.jpsymfony.com/design_patterns/le-design-pattern-observer-avec-symfony2
- ↑ https://github.com/certificationy/symfony-pack/blob/babd3fee68a7e793767f67c6df140630f52e7f8d/data/architecture.yml#L13
- ↑ https://symfony.com/doc/current/components/process.html
- ↑ https://gist.github.com/appaydin/42eaf953172fc7ea6a8b193694645324
- ↑ https://symfony.com/doc/current/components/asset.html
- ↑ https://symfonycasts.com/screencast/stimulus/encore
- ↑ https://symfony.com/doc/current/frontend/encore/simple-example.html
- ↑ https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/
- ↑ https://symfony.com/doc/current/workflow.html
- ↑ https://symfony.com/doc/current/components/dependency_injection/compilation.html
- ↑ https://symfony.com/doc/current/components/expression_language.html
- ↑ https://symfony.com/doc/current/components/lock.html
- ↑ https://symfony.com/bundles/SymfonyMakerBundle/current/index.html
- ↑ https://symfony.com/components/Locale
- ↑ https://symfony.com/components/Icu
- ↑ https://symfony.com/components/ClassLoader
- ↑ https://symfony.com/doc/current/components/uid.html
- ↑ https://symfony.com/doc/current/rate_limiter.html
- ↑ https://symfony.com/doc/current/components/semaphore.html
- ↑ https://symfony.com/blog/new-in-symfony-5-3-passwordhasher-component
- ↑ https://symfony.com/blog/new-in-symfony-5-3-runtime-component
- ↑ https://symfony.com/bundles/ux-chartjs/current/index.html
- ↑ https://symfony.com/blog/new-in-symfony-6-3-webhook-and-remoteevent-components
- ↑ https://symfony.com/blog/new-in-symfony-6-3-assetmapper-component
- ↑ https://symfony.com/blog/new-in-symfony-6-3-scheduler-component
HttpClient
Installation
modifierComposant pour lancer des requêtes HTTP depuis l'application, avec gestion des timeouts, redirections, cache, protocole et en-tête HTTP. Il est configurable en PHP ou dans framework.yaml.
Depuis Symfony 4[1] :
composer require symfony/http-client
Utilisation
modifierDeux solutions :
HttpClient::create();
ou
public function __construct(private readonly HttpClientInterface $httpClient)
Par défaut, l'appel statique à la classe HttpClient instancie un CurlHttpClient, alors que l'injection du service via HttpClientInterface récupère un TraceableHttpClient. Ce dernier est préférable puisqu'il affiche toutes les requêtes dans le profiler de Symfony.
GET
modifierOn peut forcer l'utilisation de HTTP 2 à la création :
$httpClient = HttpClient::create(['http_version' => '2.0']);
$response = $httpClient->request('GET', 'https://fr.wikibooks.org/');
if (200 == $response->getStatusCode()) {
dd($response->getContent());
} else {
dd($response->getInfo('error'));
}
Ce code ne lève pas les exceptions de résolution DNS.
POST
modifierExemple en POST avec authentification :
$response = $httpClient->request('POST', 'https://fr.wikibooks.org/w/api.php', [
'auth_bearer' => 'mon_token',
'jsonA' => $keyValuePairs,
]);
Pour lancer plusieurs appels asynchrones, il suffit de placer leurs $response->getContent()
ensemble, après tous les $httpClient->request()
.
Pour envoyer un fichier il y a plusieurs solutions :
- Utiliser le type MIME correspondant à son extension (ex : 'application/pdf', 'application/zip'...). Mais on ne peut envoyer que le fichier dans la requête.
- Utiliser le type MIME 'application/json' et l'encoder en base64. Il peut ainsi être envoyé avec d'autres données.
- Utiliser le type MIME 'multipart/form-data'[2].
Problèmes connus
modifierCe composant est relativement jeune et souffre d'incomplétudes.
- On peut avoir du "null given" à tort sur un mapping DNS, solvable en rajoutant une option :
$options = array_merge($options, [
'resolve' => ['localhost' => '127.0.0.1']
]);
$httpClient->request()
renvoie uneSymfony\Contracts\HttpClient\ResponseInterface
, mais en cas d'erreur, elle ne contient qu'une ligne de résumé, soit moins d'informations qu'un client comme Postman.
Tests
modifierCe composant peut aussi serveur aux tests fonctionnels via PhpUnit. On l'appelle alors avec static::createClient
si le test extends WebTestCase
. Dans le cas d'un projet API Platform, on l'appelle de la même manière mais le test extends ApiTestCase
.
Exemple :
$client = static::createClient();
$client->request('GET', '/home');
var_dump($client->getResponse()->getContent());
Pour simuler plusieurs clients en parallèle : $client->insulate()
.
Pour simuler un utilisateur : $client->loginUser($monUser)
.
Pour un test de bundle, il faut créer une classe Kernel qui charge les routes en plus[3].
Références
modifier
Évènement
Un évènement est une action pouvant en déclencher d'autres qui l'attendaient, à la manière du patron de conception observateur, via un hook.
Installation
modifiercomposer require symfony/event-dispatcher
Commande
modifierPour lister les évènements et écouteurs d'un projet (avec leurs priorités) :
php bin/console debug:event-dispatcher
Ex :
"console.terminate" event
-------------------------
------- ----------------------------------------------------------------------------- ----------
Order Callable Priority
------- ----------------------------------------------------------------------------- ----------
#1 Symfony\Component\Console\EventListener\ErrorListener::onConsoleTerminate() -128
#2 Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate() -255
------- ----------------------------------------------------------------------------- ----------
Event
modifierPour utiliser ce système, la première étape consiste à déterminer si on souhaite utiliser un évènement existant, ou en créer un nouveau.
- Pour un existant, son nom est obtenu par le commande ci-dessus.
- Pour un nouveau, voici un exemple de conception pilotée par le domaine où l'on souhaite qu'une condition du core soit traitée dans des modules en fonction du groupe utilisateur, sans les lister dans le core :
class AddExtraDataEvent
{
/** @var string */
private $userGroup;
public function __construct(string $userGroup)
{
$this->userGroup = $userGroup;
}
public function getUserGroup(): string
{
return $this->usernGroup;
}
public function setUserGroup(string $usernGroup): AddExtraDataEvent
{
$this->userGroup = $userGroup;
return $this;
}
}
Une fois la classe crée, il faut choisir où l'instancier :
use Symfony\Component\EventDispatcher\EventDispatcher;
...
$this->eventDispatcher->dispatch(new AddExtraDataEvent($userGroup));
Listener
modifierPour exécuter une ou plusieurs classes au moment du dispatch, il faut créer maintenant en créer une qui écoute l'évènement. Elle doit peut être reliée à son évènement, soit dans sa déclaration de service pour un écouter (event listener[1]), soit dans son constructeur pour un souscripteur (event subscriber).
Le listener a donc l'inconvénient de devoir être déclaré avec un tag, alors que le subscriber lui, est chargé à chaque exécution du programme, ce qui alourdit légèrement les performances mais évite de maintenir sa déclaration en autowiring.
Exemple de déclaration YAML
modifierservices:
App\EventListener\MyViewListener:
tags:
- { name: kernel.event_listener, event: kernel.view }
class MyViewListener
{
public function onKernelException(ExceptionEvent $event)
{
echo "Triggered!";
}
}
Subscriber
modifierUn souscripteur doit forcément implémenter EventSubscriberInterface :
class ViewSubscriber implements EventSubscriberInterface
{
public function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onView']
];
}
public function onView(ViewEvent $event): void
{
echo "Triggered!";
}
}
Autre exemple où on veut embarquer dans un évènement maison une information de ses souscripteurs :
class ClientXUserSubscriber implements EventSubscriberInterface
{
...
public static function getSubscribedEvents(): array
{
return [
ClientXEvent::class => 'getProperty',
];
}
public function getProperty(ClientXUserEvent $event): void
{
if ('X' === $this->user->getCompany()) {
$event->setProperty('XX');
}
}
}
Débogage
modifierLes erreurs qui surviennent selon certains évènements ne sont pas faciles à provoquer ou visualiser. Pour les voir sans passer par le profiler, on peut ajouter temporairement dans un contrôleur :
$this->getEventDispatcher()->dispatch('mon_service');
Références
modifier
Formulaire
Principe
modifierLe principe est d'ajouter des champs de formulaire en PHP, qui seront automatiquement convertis en code HTML correspondant.
En effet, en HTML on utilise habituellement la balise <form>
pour afficher les champs à remplir par le visiteur. Puis sur validation on récupère leurs valeurs en PHP avec la superglobale $_REQUEST
(ou ses composantes $_GET
et $_POST
). Or ce système ne fonctionne pas en $_POST
dans Symfony : si on affiche un tel formulaire et qu'on le valide, $_POST
est vide, et l'équivalent Symfony de $_REQUEST
, $request->request
[1] aussi.
Les formulaires doivent donc nécessairement être préparés en PHP.
Installation
modifierForm
modifier
composer require symfony/form
Les formulaires présents sont ensuite listables avec :
bin/console debug:form
Et vérifiables individuellement :
bin/console debug:form "App\Service\Form\MyForm"
Avec le composant maker, on peut créer un formulaire pour chaque entité Doctrine à modifier :
composer require symfony/maker-bundle bin/console make:form
Validator
modifierPour ajouter des contrôles sur les champs, il existe un deuxième composant Symfony[2] :
composer require symfony/validator
Contrôleur
modifierInjection du formulaire dans un Twig
modifierclass HelloWorldType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
->add('save', SubmitType::class)
;
}
}
class HelloWorldController extends AbstractController
{
#[Route('/helloWorld/{id}, requirements: ['id' => '\d*']')]
public function indexAction(Request $request, ?HelloWorld $helloWorld = null): Response
{
$form = $this->createForm(HelloWorldType::class, $helloWorld);
return $this->render('helloWorld.html.twig', [
'form' => $form->createView(),
]);
}
}
Le second paramètre de createForm() est facultatif est sert à préciser des valeurs initiales dans le formulaire qui seront injectées en Twig, mais elles peuvent aussi l'être via le fichier du formulaire dans les paramètres de chaque champ.
Traitement post-validation
modifierDans la même méthode du contrôleur qui injecte le formulaire, il faut prévoir le traitement post-validation. Par exemple pour mettre à jour l'entité en base :
if (empty($myEntity)) {
$myEntity = new MyEntity();
}
$form = $this->createForm(MyEntityType::class, $myEntity);
$form->handleRequest($request); // Cette méthode remplit l'objet avec les valeurs postées dans $request pour les champs du formulaires mappés
if ($form->isSubmitted() && $form->isValid()) {
// Mise à jour d'un champ non mappé (ex : car absent de $myEntity)
$email = $form->get('email')->getData();
$this->em->persist($email);
$this->em->flush();
return $this->redirectToRoute('home');
}
Fichier du formulaire
modifierDans SF4, l'espace de nom Symfony\Component\Form\Extension\Core\Type propose 35 types de champ, tels que :
- Text
- TextArea
- Email (avec validation en option de la présence d'arrobase ou de domaine)
- Number
- Date
- Choice (menu déroulant)
- Checkbox (cases à cocher et boutons radio)
- Hidden (caché)
- Submit (bouton de validation).
TextType
modifierExemple[3] :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', TextType::class, [
'required' => true,
'empty_data' => 'valeur par défaut si vide à la validation',
'data' => 'valeur par défaut préremplie à la création',
'constraints' => [new Assert\NotBlank()],
'attr' => ['class' => 'ma_classe_CSS'],
]);
}
Pour préremplir des valeurs dans les champs :
$form->get('email')->setData($user->getEmail());
L'attribut "required" peut être interprété par les navigateurs comme un "NotBlank", mais il faut tout de même le compléter avec la contrainte sans quoi un simple retrait du "required" de la page web par la console du navigateur pourrait contourner l'obligation.
NumberType
modifierCette classe génère une balise input type="number"
, qui empêche donc les navigateurs d'écrire des lettres dedans en HTML5.
D'autre part, il y a aussi les problématiques des nombres minimum et maximum, et des séparateurs décimaux et de milliers.
Ex :
$builder ->add('email', NumberType::class, [ 'html5' => true, 'constraints' => [new Assert\Positive()], 'attr' => [ 'onkeypress' => 'return (event.charCode > 47 && event.charCode < 58) || event.charCode == 44 || event.charCode == 45', ], ]);
ChoiceType
modifierIl faut injecter le tableau des choix du menu déroulant dans la clé "choices", avec en clé ce qui sera visible dans la liste et en valeur ce qui sera envoyé à la soumission[4].
Ex :
$builder ->add('civility', ChoiceType::class, [ 'choices' => ['Choisir' => null, 'M.' => 'M.', 'Mme' => 'Mme'], ])
Dans le cas où une valeur par défaut est définie dans 'data', elle doit appartenir aux valeurs du tableau de "choices", sans quoi elle ne sera pas prise en compte.
Si une valeur absente de la liste des choix est envoyée à la soumission, on peut la faire accepter en l'ajoutant à la volée avec[5] :
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { ... })
EntityType
modifierDe plus, en installant Doctrine, il est possible d'ajouter un type de champ "entité" directement relié avec un champ de base de données[6].
Ex :
$builder->add('company', EntityType::class, ['class' => Company::class]);
En SF4, il n'y avait pas encore les types CheckboxType ou RadioType : il fallait jouer sur deux paramètres de EntityType
ainsi :
Élément | Expanded | Multiple |
---|---|---|
Sélecteur | false | false |
Sélecteur multiple | false | true |
Boutons radio | true | false |
Cases à cocher | true | true |
Exemple :
$builder->add('gender', EntityType::class, ['expanded' => true, 'multiple' => false]);
Pour lui donner une valeur par défaut, il faut lui injecter un objet :
$builder->add('company', EntityType::class, [ 'class' => Company::class, 'choice_label' => 'name', 'data' => $company, ]);
Sous-formulaire
modifierUtiliser le nom du sous-formulaire comme type :
$builder->add('company', MySubformType::class, [ 'label' => false, ]);
Validation
modifierValidation depuis les entités
modifierLe validateur de formulaire d'entité peut utiliser les annotations des entités. Ex :
use Symfony\Component\Validator\Constraints as Assert;
...
#[Assert\Type('string')]
#[Assert\NotBlank]
#[Assert\Length(
min: 1,
max: 255,
)]
En PHP < 8 :
use Symfony\Component\Validator\Constraints as Assert;
...
/**
* @Assert\Type("string")
* @Assert\NotBlank
* @Assert\Length(
* min = 2,
* max = 50
* )
*/
Plusieurs types de données sont déjà définis, comme l'email ou l'URL[7]. Ex :
@Assert\Email()
Validation depuis les formulaires
modifierSinon il permet aussi des contrôles plus personnalisés dans les types (qui étendent Symfony\Component\Form\AbstractType). Ex :
'constraints' => [
new Assert\NotBlank(),
new GreaterThanOrEqual(2),
new Assert\Callback([ProductChecker::class, 'check']),
],
Validation avec un service
modifierPour valider une entité depuis le service validateur[8] :
use Symfony\Component\Validator\Validator\ValidatorInterface; ... $validator->validate( $entity, $entityConstraint );
NB : le second paramètre est optionnel.
Bien que l'on voit des services correspondant aux contraintes du validateur, on ne peut pas les injecter comme les autres services mais uniquement les utiliser via le validateur général.
Exemple pour valider un email :
php bin/console debug:container |grep -i validator |grep -i email validator.email Symfony\Component\Validator\Constraints\EmailValidator
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validator\ValidatorInterface;
...
$this->validator->validate(
'mon_email@example.com',
new Email()
);
Appel du formulaire Symfony dans la vue
modifierLes fonctions Twig permettant d'ajouter les éléments du formulaire sont :
- form_start
- form_errors
- form_row
- form_widget
- form_label
Pour afficher tout le formulaire, dans l'ordre où les champs ont été définis en PHP :
{{ form_start(form) }}
{{ form_end(form) }}
Pour n'afficher qu'un seul champ :
{{ form_widget(form.choosen_credit_card) }}
Les mêmes attributs qu'en PHP peuvent être définis en paramètre. Ex :
{{ form_widget(form.name, {'attr': {'class': 'address', 'placeholder': 'Entrer une adresse'} }) }}
{{ form_label(form.name, null, {'label_attr': {'class': 'address'}}) }}
Exemple complet :
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_label(form.name, 'Label du champ "name" écrasé ici') }}
{{ form_row(form.name) }}
{{ form_widget(form.message, {'attr': {'placeholder': 'Remplacez ce texte par votre message'} }) }}
{{ form_rest(form) }}
{{ form_row(form.submit, { 'label': 'Submit me' }) }}
{{ form_end(form) }}
Références
modifier- ↑ https://symfony.com/doc/current/components/http_foundation.html
- ↑ https://symfony.com/doc/current/forms.html#form-validation
- ↑ https://symfony.com/doc/current/reference/forms/types/form.html#empty-data
- ↑ https://symfony.com/doc/current/reference/forms/types/choice.html
- ↑ https://github.com/symfony/symfony/issues/42451
- ↑ https://symfony.com/doc/master/reference/forms/types/entity.html
- ↑ https://symfony.com/doc/current/validation.html#string-constraints
- ↑ https://symfony.com/doc/current/validation.html#using-the-validator-service
Mailer
Mailer
modifierDepuis Symfony 4.3, un composant Symfony Mailer a été ajouté.
Pour l'installer[1] :
composer require symfony/mailer
Ajouter ensuite le SMTP dans le .env :
MAILER_DSN=smtp://mon_utilisateur:mon_mot_de_passe@smtp.example.com
Utilisation
modifier private MailerInterface $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function send(string $message): void
{
$email = (new Email())
->from('no-reply@example.com')
->to('target@example.com')
->subject('Test Symfony Mailer')
->text($message)
;
$this->mailer->send($email);
}
Swift Mailer
modifierAvant Symfony 4.3 et la création du composant Mailer[2], on pouvait utiliser Swift Mailer.
Swift Mailer est ensuite remplacé en novembre 2021 par le composant Mailer.
Installation
modifiercomposer require symfony/swiftmailer-bundle
Utilisation
modifierPar exemple, pour un envoi d'email sans passer par config.yml :
$transport = (new \Swift_SmtpTransport('mon_smtp.com', 25));
$mailer = new \Swift_Mailer($transport);
$message = (new \Swift_Message('Hello World from Controller'))
->setFrom('mon_email@example.com')
->setTo('mailcatcher@example.com')
->setBody('Hello World',
'text/html'
)
;
$mailer->send($message);
Templates
modifierPour simplifier les templates d'email, une alternative au HTML / CSS existe, il s'agit de Inky[4].
Elle utilise d'autres balises XML, comme callout
ou spacer
[5].
Installation :
composer require twig/extra-bundle twig/inky-extra
Utilisation :
{% apply inky_to_html %} ...
Références
modifier
Stimulus
Introduction
modifierStimulus est le framework JavaScript officiel de Symfony[1]. Il est installé avec Webpack :
composer require symfony/webpack-encore-bundle
Pour utiliser le framework React.js dans Symfony[2] :
composer require symfony/ux-react
Lancer ensuite npm run watch
pour que le code JS exécuté soit toujours identique à celui écris. Cela va lancer le npm run build
en cours de frappe.
Hello World on ready
modifierPartie Twig
modifierLa première étape consiste à connecter un contrôleur Stimulus depuis un fichier Twig, en lui injectant les variables dont il a besoin. Ex :
<div {{ stimulus_controller('ticket', { subject: 'Hello World' } )}}> </div>
Une syntaxe alternative est :
<div data-controller="ticket" data-ticket-subject-value="Hello World" > </div>
Partie Stimulus
modifierDans le fichier assets/controllers/ticket_controller.js, créer une classe héritant de Stimulus :
import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static values = { subject: String, body: String, }; connect() { alert(this.subjectValue); } }
Rafraichir la page du Twig pour voir le message du code exécuté par Stimulus.
Explication : la fonction connect est un mot réservé désignant une fonction prédéfinie qui s'exécute automatiquement quand le contrôleur Stimulus est connecté au DOM de la page[3]. C'est donc un mécanisme similaire à la méthode magique PHP __contruct. De plus, il existe aussi disconnect comparable à la méthode PHP __destruct.
Si le contrôleur Stimulus est dans un sous-dossier, la syntaxe des séparateurs de dossiers côté Twig n'est pas "/" mais "--".
Ex : stimulus_controller('sousDossier--ticket', ...)
connectera le fichier assets/controllers/sousDossier/ticket_controller.js.
Hello World on click
modifierOn utilise l'action "click"[4].
Partie Twig
modifier<div {{ stimulus_controller('ticket', { subject: 'Hello World' } )}}> <button {{ stimulus_action('ticket', 'onCreate', 'click') }}> Créer un ticket </button> </div>
Une syntaxe alternative est :
<div data-controller="ticket" data-ticket-subject-value="Hello World" > <button data-action="click->ticket#onCreate" > Créer un ticket </button> </div>
Partie Stimulus
modifierPar rapport au premier exemple, on remplace juste "connect" par une méthode maison.
import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static values = { subject: String, body: String, }; onCreate() { alert(this.subjectValue); } }
Rafraichir la page du Twig et cliquer sur le bouton pour voir le message du code exécuté par Stimulus.
Exemple où Stimulus appelle React
modifierOn veut maintenant déclencher l'ouverture d'une fenêtre modale React.js en cliquant sur un bouton de la page du Twig. Il faut donc que le contrôleur Stimulus appelle une classe React.
- ticket_controller.js :
import { Controller } from "@hotwired/stimulus"; import ReactDOM from "react-dom"; import React from "react"; import HelloWorld from "./HelloWorld"; export default class extends Controller { static values = { subject: String, body: String, }; onCreate() { ReactDOM.render(<HelloWorld subject={this.subjectValue} />, this.element); } }
- HelloWorld.js :
export default function (props) { alert(props.subject); }
react_component()
.Références
modifier- ↑ https://symfony.com/blog/new-in-symfony-the-ux-initiative-a-new-javascript-ecosystem-for-symfony#symfony-ux-building-highly-interactive-applications-by-leveraging-javascript-giants
- ↑ https://symfony.com/bundles/ux-react/current/index.html
- ↑ https://stimulus.hotwired.dev/reference/lifecycle-callbacks
- ↑ https://stimulus.hotwired.dev/reference/actions
Bundle
Dans Symfony, on appelle bundle une bibliothèque prévue pour être installée dans Symfony comme module complémentaire au framework.
Configurer un bundle
modifierAprès installation avec composer, il doit généralement être configuré dans le dossier config/ par un fichier YAML à son nom. Pour connaître les configurations possibles :
php bin/console config:dump mon_bundle
La classe du bundle est instanciée dans :
config/bundles.php
Pour l'activer ou le désactiver de certains environnements, il suffit de l'ajouter un paramètre. Ex :
<?php
return [
// À instancier tout le temps
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
// À instancier seulement si dans le .env, APP_ENV=dev ou APP_ENV=test (les autres sont "false" par défaut)
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
// À ne pas instancier dans les environnements de dev
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
];
Créer un bundle
modifierPar rapport à une application classique, la création d'un bundle possède des particularités du fait qu'il n'est prévu pour être utilisé que comme dépendance d'applications tierces[1]. Par exemple :
- Ses namespaces doivent démarrer par le nom du vendor et se terminer par le mot Bundle (ex : Symfony\Bundle\FrameworkBundle).
- Il doit contenir un fichier point d'entrée dans sa racine (ex : FrameworkBundle.php).
- Il peut avoir un .yaml de configuration dans config/packages à créer automatiquement à l'installation grâce à une classe étendant ConfigurationInterface[2].
Principaux bundles
modifierPackagist propose une liste des bundles Symfony les plus utilisés[3].
SensioFrameworkExtraBundle
modifierPermet de créer des annotations[4].
FOS
modifierFriendsOfSymfony[5] propose plusieurs bundles intéressants, parmi lesquels :
- FOSUserBundle : pour gérer des utilisateurs.
- FOSRestBundle : pour les API REST.
KNP
modifierKNP Labs offre également plusieurs bundles connus, dont un paginateur[6].
SonataAdmin
modifierCe bundle permet de créer rapidement un back-office pour lire ou modifier une base de données[7].
EasyAdmin
modifierMêmes principales fonctions que SonataAdmin mais plus léger[8].
Installation :
composer require easycorp/easyadmin-bundle
Pour l'accueil :
bin/console make:admin:dashboard
Pour une liste paginée d'entités Doctrine modifiables, avec liens vers leurs CRUD :
bin/console make:admin:crud
The PHP League
modifierVoir https://github.com/thephpleague.
Références
modifier- ↑ https://symfony.com/doc/current/bundles/best_practices.html
- ↑ https://symfony.com/doc/current/bundles/configuration.html#processing-the-configs-array
- ↑ https://packagist.org/packages/symfony/?query=symfony%20bundle&tags=symfony
- ↑ https://symfony.com/bundles/SensioFrameworkExtraBundle/current/index.html
- ↑ https://github.com/FriendsOfSymfony
- ↑ https://github.com/KnpLabs
- ↑ https://github.com/sonata-project/SonataAdminBundle
- ↑ https://symfony.com/bundles/EasyAdminBundle/current/index.html
Twig
Installation
modifierTwig est un moteur de templates pour le langage de programmation PHP, utilisé par défaut par le framework Symfony. Son livre officiel faisant 156 pages[1], la présente pas aura plutôt un rôle d'aide mémoire et d'illustration.
Pour exécuter du code sans installer Twig, il existe https://twigfiddle.com/.
composer require symfony/templating
Anciennement sur Symfony 3 :
composer require twig/twig
Syntaxe native
modifierLes mots réservés suivants s'ajoutent au HTML déjà interprété :
- {{ ... }} : appel à une variable ou une fonction PHP, ou un template Twig parent (
{{ parent() }}
). - {# ... #} : commentaires.
- {% ... %} : commande, comme une affectation, une condition, une boucle ou un bloc HTML.
- {% set foo = 'bar' %} : assignation[2].
- {% if (i is defined and i == 1) or j is not defined or j is empty %} ... {% endif %} : condition.
- {% for i in 0..10 %} ... {% endfor %} : compteur dans une boucle.
- ' : caractère d'échappement.
Chaines de caractères
modifierConcaténation
modifierIl existe de multiples manière de concaténer des chaines[3]. Par exemple avec l'opérateur de concaténation ou par interpolation :
"{{ variable1 ~ variable2 }}" "#{variable1} #{variable2}"
Les apostrophes ne fonctionnent pas avec l'interpolation.
Tableaux
modifierCréation
modifierPour créer un tableau itératif :
{% set myArray = [1, 2] %}
Un tableau associatif :
{% set myArray = {'key': 'value'} %}
À plusieurs lignes :
{% set months = {
1: 'janvier',
2: 'février',
3: 'mars',
} %}
{{ dump(months[1]) }} {# 'janvier' #}
Ajouter une ligne :
{% set months = months|merge({4: 'avril'}) %}
Ajouter une ligne avec clé variable :
{% set key = 5 %}
{% set months = months|merge({(key): 'mai'}) %}
Ajouter une ligne en préservant les clés numériques :
{% set key = 6 %}
{% set months = months + {(key): 'juin'} %}
Multidimensionnel :
{% set myArray = [
{'key1': 'value1'},
{'key2': 'value2'}
] %}
Dans un "for ... in", pour séparer chaque élément avec une virgule :
{% if loop.first != true %}
,
{% endif %}
Pour créer un tableau associatif JavaScript à partir d'un tableau Twig :
<script type="text/javascript">
const monTableauJs = JSON.parse('{{ monTableauTwig |json_encode |raw }}');
for (const maLigneJs in monTableauJs) {
console.log(maLigneJs);
console.log(monTableauJs[maLigneJs]);
}
</script>
Modification d'une ligne
modifierPour modifier une ligne, utiliser "merge()"[4]. Ex :
{% set tests = {'a': 1} %} {% set tests = tests|merge({'b': 2}) %} {{ dump(tests) }} {% set tests = tests|merge({'b': 3}) %} {{ dump(tests) }}
array:2 [▼ "a" => 1 "b" => 2 ] array:2 [▼ "a" => 1 "b" => 3 ]
La clé de la ligne ne doit pas être numérique (même convertie en chaine) sinon Twig modifie les clés, donc cela ajoute une ligne :
{% set tests = {'1': 1} %} {% set tests = tests|merge({'2': 2}) %} {{ dump(tests) }} {% set tests = tests|merge({'2': 3}) %} {{ dump(tests) }}
array:2 [▼ 0 => 1 1 => 2 ] array:3 [▼ 0 => 1 1 => 2 2 => 3 ]
Modification des lignes
modifierPour ajouter une ou plusieurs lignes à un tableau, utiliser "merge()" aussi :
{% set oldArray = [1] %}
{% set newArray = oldArray|merge([2,3]) %}
{{ dump(newArray) }}
0 => 1 1 => 2 2 => 3
Pour ajouter une ligne associative :
{% set oldArray = {'key1': 'value1'} %}
{% set newArray = oldArray|merge({'key2': 'value2'}) %}
{{ dump(newArray) }}
[ "key1" => "value1" "key2" => "value2" ]
Pour ajouter une ligne de sous-tableau :
{% set oldArray = [{'key1': 'value1'}] %}
{% set newArray = oldArray|merge([{'key2': 'value2'}]) %}
{{ dump(newArray) }}
[ 0 => ["key1" => "value1"] 1 => ["key2" => "value2"] ]
Lecture
modifierPour savoir si une variable est un tableau :
if my_array is iterable
Pour savoir :
- si un tableau est vide, utiliser empty comme pour les chaines de caractères. Par exemple pour savoir si un tableau est vide ou null :
my_array is empty
- la taille du tableau :
my_array |length
- si un élément est dans un tableau :
my_item in my_array
- si un élément n'est pas dans un tableau :
my_item not in my_array
- si un élément est dans les clés d'un tableau :
my_item in my_array|keys
Pour filtrer le tableau, utiliser filter[5]. Par exemple pour savoir si un tableau multidimensionnel a ses sous-tableaux vides :
my_array|filter(v => v is not empty) is empty
Précédence des opérateurs
modifierDu moins au plus prioritaire[6] :
Opérateur | Rôle |
---|---|
b-and | Et booléen |
b-xor | Ou exclusif |
b-or | Ou booléen |
or | Ou |
and | Et |
== | Est-il égal |
!= | Est-il différent |
< | Inférieur |
> | Supérieur |
>= | Supérieur ou égal |
<= | Inférieur ou égal |
in | Dans (ex : {% if x in [1, 2] %} )
|
matches | Correspond |
starts with | Commence par |
ends with | Se termine par |
.. | Séquence (ex : 1..5 )
|
+ | Plus |
- | Moins |
~ | Concaténation |
* | Multiplication |
/ | Division |
// | Division arrondie à l'inférieur |
% | Modulo |
is | Test (ex : is defined ou is not empty )
|
** | Puissance |
| | Filtre |
[] | Entrée de tableau |
. | Attribut ou méthode d'un objet (ex : country.name )
|
Pour afficher la valeur NULL dans un opérateur ternaire, il faut la mettre entre apostrophes :
{{ (myVariable is not empty) ? '"' ~ myVariable.value ~ '"' : 'null' }}
Fonctions usuelles
modifierChemins, routes et URLs
modifierurl('route_name')
: affiche l'URL complète d'une route. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :url('ma_route_de_controleur', {'parametre1': param1})
).absolute_url('path')
: affiche l'URL complète d'un chemin.path('route_name')
: affiche le chemin, en absolu par défaut, mais il existe le paramètrerelative=true
. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :path('ma_route_de_controleur', {'parametre1': param1}
).asset('path')
: pointe le dossier des "assets" ("web" dans SF2, "public" dans SF4). Ex :<img src="{{ asset('images/mon_image.png') }}" />
.controller('controller_name')
: exécute la méthode d'un contrôleur. Ex :{{ render(controller('App\\Controller\\DefaultController:indexAction')) }}
.
absolute_url()
renvoie l'URL de l'application si l'appel provient d'un contrôleur, mais http://localhost s'il vient d'une commande (CLI)[7]. La solution est donc de définir l'URL de l'environnement dans une variable, soit default_uri
de routing.yaml, soit maison et injectée par le contrôleur dans le Twig.
Divers
modifierconstant(constant_name)
: importe une constante d'une classe PHP[10].attribute(object, method)
: accède à l'attribut d'un objet PHP. C'est équivalent au "." mais la propriété peut être dynamique[11].date()
: convertit en date, ce qui permet leur comparaison. Ex :{% if date(x) > date(y) %}
. NB : comme en PHP, "d/m/Y" correspond au format "jj/mm/aaaa".- min() : renvoie le plus petit nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
- max() : renvoie le plus grand nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
Filtres
modifierLes filtres fournissent des traitements sur une expression, si on les place après elle séparés par des pipes. Par exemple :
capitalize
: équivaut au PHPucfirst()
, met une majuscule à la première lettre d'une chaine de caractères, et passe les autres en minuscules.upper
: équivaut au PHPstrtoupper()
, met la chaine en lettres capitales. Exemple pour ne mettre la majuscule que sur la première lettre :{{ variable[:1]|upper ~ variable[1:] }}
.first
: affiche la première ligne d'un tableau, ou la première lettre d'une chaine.length
: équivaut au PHPsizeof()
, renvoie la taille de la variable (chaine ou tableau).format
: équivaut au PHPprintf()
.date
: équivaut au PHPdate()
mais son format est du type DateInterval[12].date_modify
: équivaut au PHP DateTime->modify(). Ex :{% set tomorrow = 'now'|date_modify("+1 day") %}
.replace
: équivaut au PHPstr_replace()
. Ex :{{ 'Mon titre %tag%.'|replace({'%tag%': '1'}) }}
.join
: équivaut au PHPimplode()
: convertit un tableau en chaine avec un séparateur en paramètre.split
: équivaut au PHPexplode()
: convertit une chaine en tableau avec un séparateur en paramètre.slice(début, fin)
: équivaut au PHParray_slice()
+substr()
: découpe un tableau ou une chaine selon deux positions[13].trim
: équivaut au PHPtrim()
.raw
: ne pas échapper les balises HTML.json_encode
: transforme un tableau en chaine de caractères JSON.default
: ce filtre lève les exceptions sur les variables non définies ou vides[14]. Ex :
{{ variable1 |default(null) }}
Variables spéciales
modifierloop
contient les informations de la boucle dans laquelle elle se trouve. Par exempleloop.index
donne le nombre d'itérations déjà survenue (commence par 1 et pas par 0).- Les variables globales commencent par des underscores, par exemple[15] :
_route
: partie de l'URL située après le domaine._self
: nom de du fichier courant._charset
: jeu de caractères de la page. Ex : UTF-8._context
: variables injectées dans le template. Cela peut donc permettre d'y accéder en variables variables. Ex :{{ attribute(_context, 'constante'~variable) }}
{{ attribute(form, 'constante'~variable) }}
pour un champ de formulaire.
- Les variables d'environnement CGI, telles que
{{ app.request.server.get('SERVER_NAME') }}
Pour obtenir la route d'une page : {{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}
L'URL courante : {{ app.request.uri }}
La page d'accueil du site Web : url('homepage')
app.environment
renvoie la valeur de APP_ENV.
Gestion des espaces
modifierspaceless
modifierUn Twig bien formaté ne correspond pas forcément au rendu qu'il doit apporter. Pour supprimer les espaces du formatage dans ce rendu :
{% apply spaceless %}
<b>
Hello World!
</b>
{% endspaceless %}
NB : en Twig < 2.7, c'était[16] :
{% spaceless %}
{% autoescape false %}
<b>
Hello World!
</b>
{% endspaceless %}
Par ailleurs, il existe un filtre |spaceless[17].
-
modifierDe plus, on peut apposer le symboles "-" aux endroits où ignorer les espacements (dont retours chariot) du formatage :
Hello {% ... -%}
{%- ... %} World!
Cela fonctionne aussi entre {{- -}}
.
Utilisation du traducteur
modifierConfiguration
modifierLe module de traduction Symfony s'installe avec :
composer require translator
Quand une page peut apparaitre dans plusieurs langues, inutile d'injecter la locale dans le Twig depuis le contrôleur PHP, c'est une variable d'environnement que l'on peut récupérer avec :
{{ app.request.getLocale() }}
{{ app.request.get('mon_query_param') }}
Le fichier YAML contenant les traductions dans cette langue sera automatiquement utilisé s'il est placé dans le dossier "translations" apparu lors de l'installation. En effet, il est identifié par le code langue ISO de son suffixe (ex : le Twig de la page d'accueil pourra être traduit dans homepage.fr.yml, homepage.en.yml, etc.).
Pour définir le préfixe des YAML auquel un Twig fera appel, on le définit sans suffixe en début de fichier Twig :
{% trans_default_domain 'homepage' %}
Par ailleurs, la commande PHP pour lister les traductions les traductions d'une langue est[18] :
php bin/console debug:translation en --only-unused // Pour les inutilisées
php bin/console debug:translation en --only-missing // Pour les manquantes
Filtre trans
modifierUne fois la configuration effectuée, on peut apposer le filtre trans
aux textes traduis dans le Twig.
{{ MessageInMyLanguage |trans }}
Parfois, il peut être utile de factoriser les traductions de plusieurs Twig dans un seul YAML. Pour piocher dans un YAML qui n'est pas celui par défaut, il suffit de le nommer en second paramètre du filtre trans
:
{{ 'punctuation_separator'|trans({}, 'common') }}
Si le YAML contient des balises HTML à interpréter, il faut apposer le filtre raw
après trans
.
Si une variable doit apparaitre dans une langue différente de celle de l'utilisateur, on le précisera dans le troisième paramètre du filtre trans
:
{{ FrenchMessage |trans({}, 'common', 'fr') }}
Si le YAML doit contenir une variable, on la place entre pourcentages pour la remplacer en Twig avec le premier paramètre du filtre trans
:
{{ variableMessage |trans({"%price%": formatPrice(myPrice)}) }}
Si la clé à traduire doit être variable, on ne peut pas réaliser la concaténation dans la même commande que la traduction : il faut décomposer en deux lignes :
{% set variableMessage = 'constante.' ~ variable %}
{{ variableMessage |trans }}
Opération trans
modifierIl existe aussi une syntaxe alternative au filtre. Par exemple les deux paragraphes ci-dessous sont équivalents :
{{ 'punctuation_separator'|trans({}, 'common') }}
{% trans from 'common' %}
punctuation_separator
{% endtrans %}
De plus, on peut injecter une variable avec "with". Voici deux équivalents :
{{ 'Bonjour %name% !' |trans({"%name%": name}) }}
{% trans with {'%name%': name}%}Bonjour %name% !{% endtrans %}
Méthodes PHP appelables en Twig
modifierEn PHP, on peut définir des fonctions invocables en Twig, sous forme de fonction ou de filtre selon la méthode parente surchargée. Exemple :
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('getPrice', [$this, 'getPrice']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('getPrice', [$this, 'getPrice']),
];
}
public function getPrice($value): string
{
return number_format($value, 2, ',', ' ') . ' €';
}
}
Héritages et inclusions
modifierextends
modifierSi une fichier appelé doit être inclus dans un tout, il doit en hériter avec le mot extends
. Le cas typique est celui d'une "base.html.twig" qui contient l'en-tête et le pied de page HTML commun à toutes les pages d'un site. Ex :
{% extends "base.html.twig" %}
Twig ne supporte pas l'héritage multiple[19].
Il est possible de surcharger totalement ou en partie les blocs du template parent. Exemple depuis le template qui hérite :
{% block header %}
Mon en-tête qui écrase le parent
{% endblock %}
{% block footer %}
Mon pied de page qui complète le parent
{{ parent() }}
{% endblock %}
include
modifierÀ contrario, si un fichier doit en inclure un autre (par exemple pour qu'un fragment de vue soit réutilisable dans plusieurs pages), on utilise le mot include
. Ex :
{% include("partials/footer.html.twig") %}
En lui injectant des paramètres :
{% include("partials/footer.html.twig") with {'clé': 'valeur'} %}
embed
modifierEnfin, embed
combine les deux précédentes fonctions :
{% embed "footer.html.twig" %}
...
{% endembed %}
import
modifierimport
récupère certaines fonctions d'un fichier en contenant plusieurs :
{% from 'mes_macros.html' import format_price as price, format_date %}
Macros
modifierLes macros sont des fonctions globales, appelables depuis un fichier Twig[22].
Exemple :
{% macro format_price(price, currency = '€') %}
{% set locale = (app.request is null) ? 'fr_FR' : app.request.locale %}
{% if locale == 'fr_FR' %}
{{ price|number_format(2, ',', ' ') }} {{ currency }}
{% else %}
{{ price|number_format(2, '.', ' ') }}{{ currency }}
{% endif %}
{% endmacro %}
Lors de l'appel, les paramètres nommés ne fonctionnent que si 100 % des paramètres appelés le sont.
Exemples
modifier{% extends "base.html.twig" %}
{% block navigation %}
<ul id="navigation">
{% for item in navigation %}
<li>
<a href="{{ item.href }}">
{% if item.level == 2 %} {% endif %}
{{ item.caption|upper }}
</a>
</li>
{% endfor %}
</ul>
{% endblock navigation %}
Pour ne pas qu'un bloc hérité écrase son parent, mais l'incrémente plutôt, utiliser :
{{ parent() }}
Bonnes pratiques
modifierLes noms des fichiers .twig doivent être rédigés en snake_case[23].
Références
modifier- ↑ https://twig.symfony.com/pdf/2.x/Twig.pdf
- ↑ https://twig.sensiolabs.org/doc/2.x/tags/set.html
- ↑ https://www.designcise.com/web/tutorial/how-to-concatenate-strings-and-variables-in-twig
- ↑ https://twig.symfony.com/doc/3.x/filters/merge.html
- ↑ https://twig.symfony.com/doc/3.x/filters/filter.html
- ↑ http://twig.sensiolabs.org/doc/templates.html
- ↑ https://stackoverflow.com/questions/73026340/absolute-url-in-template-returns-localhost-in-email-templates
- ↑ https://symfony.com/doc/current/reference/twig_reference.html
- ↑ https://symfony.com/doc/current/http_cache/esi.html
- ↑ https://twig.symfony.com/doc/2.x/functions/constant.html
- ↑ https://twig.symfony.com/doc/2.x/functions/attribute.html
- ↑ https://twig.symfony.com/doc/3.x/filters/date.html
- ↑ https://twig.symfony.com/doc/3.x/filters/slice.html
- ↑ https://twig.symfony.com/doc/2.x/filters/default.html
- ↑ https://twig.symfony.com/doc/3.x/templates.html#global-variables
- ↑ https://twig.symfony.com/doc/2.x/tags/spaceless.html
- ↑ https://twig.symfony.com/doc/2.x/filters/spaceless.html
- ↑ https://symfony.com/doc/current/translation/debug.html
- ↑ https://twig.symfony.com/doc/3.x/tags/extends.html
- ↑ https://twig.symfony.com/doc/1.x/functions/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/macro.html
- ↑ https://symfony.com/doc/current/contributing/code/standards.html
Doctrine
Installation
modifierDoctrine est l'ORM par défaut de Symfony. Il utilise PDO. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la chaîne de responsabilité.
Installation en SF4[1] :
composer require symfony/orm-pack composer require symfony/maker-bundle --dev
Renseigner l'accès au SGBD dans le .env :
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
Ensuite la base de données doit être créée avec :
php bin/console doctrine:database:create
- doctrine/doctrine-bundle
- doctrine/doctrine-migrations-bundle
- doctrine/orm
- symfony/proxy-manager-bridge
Commandes Doctrine
modifierExemples de commandes :
php bin/console doctrine:query:sql "SELECT * FROM ma_table" php bin/console doctrine:query:sql "$(< mon_fichier.sql)" # Ces deux commandes sont équivalentes des précédentes php bin/console dbal:run-sql "SELECT * FROM ma_table" php bin/console dbal:run-sql "$(< mon_fichier.sql)" php bin/console doctrine:cache:clear-metadata php bin/console doctrine:cache:clear-query php bin/console doctrine:cache:clear-result
Entity
modifierUne entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs getters et setters respectifs. Pour en générer une :
php bin/console generate:doctrine:entity
Cette association est définie par des annotations Doctrine. Pour vérifier les annotations :
php bin/console doctrine:schema:validate
Exemple
modifierVoici par exemple plusieurs types d'attributs :
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
Format avant PHP 8
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
Et leurs modificateurs (getters et setters) :
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec $word->setLanguage($language)
.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi une deuxième annotation @ORM\JoinColumn
mentionnant la clé étrangère en base de données (et pas en PHP).
Dans les relations *toMany :
- il faut initialiser l'attribut dans le constructeur en
ArrayCollection()
. - on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
- cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec length=0
[2]. Le type "text" par contre n'a pas de limite.
#[ORM\Table(name: 'word')]
(anciennement l'annotation @ORM\Table(name="word")
) était facultative dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.L'annotation code>@ORM\Table peut servir à définir des clés composites :
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
ArrayCollection
modifierCet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
- Dans une entité :
@ORM\OrderBy({"sort_order" = "ASC"})
- Sinon, instancier un critère :
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
GeneratedValue
modifierL'annotation GeneratedValue peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID[3]. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur Integrity constraint violation puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
Triggers
modifierLes opérations en cascade sont définies sous deux formes d'attributs :
#[ORM\OneToMany(cascade: ['persist', 'remove'])]
: au niveau ORM.#[ORM\JoinColumn(onDelete: 'CASCADE')]
: au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
Concepts avancés
modifierPour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware[4].
Les types des attributs peuvent être quelque peu différents du SGBD[5].
Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").
L'autojointure est appelé self-referencing association mapping par Doctrine[6]).
Héritage
modifierUne entité peut hériter d'une classe si celle-ci contient l'annotation suivante[7] :
/** @MappedSuperclass */
class MyEntityParent
...
EntityManager
modifierL'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données, définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
- persist() : prépare un INSERT SQL (rattache une entité à un entity manager).
- remove() : prépare un DELETE SQL.
- flush() : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
- merge() : fusionne une entité absent de l'em dedans.
- refresh() : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger after insert sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par
$createdDate
:
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
Repository
modifierOn appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de Doctrine\ORM\EntityRepository
. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés NomDeLEntitéRepository.
CarFactory
fera un new Car()
mais aussi créera et lui associera ses composants : new Motor()
...@ORM\Entity(repositoryClass="App\Repository\WordRepository")
SQL
modifierDepuis Doctrine
modifier$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
Sans Doctrine
modifierPour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
return $this->connection->fetchAll($sql);
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut entity manager (em, ou _em pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
2. En injectant le service de connexion dans le constructeur ('@database_connection'
) :
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
DQL
modifierMéthodes magiques
modifierDoctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
$repo->find($id)
: cherche par la clé primaire définie dans l'entité.$repo->findAll()
: récupère tous les enregistrements (sans clauseWHERE
).$repo->findById($id)
: engendre automatiquement unSELECT * WHERE id = $id
dans la table associée au repo.$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])
engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname
.$repo->findOneById($id)
: engendre automatiquement unSELECT * WHERE id = $id LIMIT 1
.$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])
: engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1
.
Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "findById
" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser findBy()
.
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
createQuery
modifierDQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec IDENTITY()
pour les jointures. Ex :
public function findComplicatedStuff() { $em = $this->getEntityManager(); $query = $em->createQuery(" SELECT u.last_name, u.first_name FROM App\Entity\Users u INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users) WHERE i.status='waiting' "); return $query->getResult(); }
createQueryBuilder
modifierL'autre syntaxe du DQL est en POO. Les méthodes des repos font appel createQueryBuilder()
:
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
Pour éviter le SELECT *
dans cet exemple, on peut y ajouter la méthode ->select()
.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
Jointures
modifierQuand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
Résultats
modifierDoctrine peut renvoyer avec :
getResult()
: un objet ArrayCollection (iterable, pour rechercher dedans :->contains()
), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;getArrayResult()
ougetScalarResult()
: un tableau de tableaux (entité normalisée) ;getSingleColumnResult()
: un tableau unidimensionnel.
Cache
modifierConfiguration globale
modifierDoctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
framework:
cache:
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
Puis dans doctrine.yaml, les utiliser :
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
À partir de là le cache des métadonnées est utilisé partout.
Configuration par entité
modifierPar contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
- Dans l'entité, avec une annotation
@ORM\Cache(usage="READ_ONLY", region="write_rare")
[8], utilisant la configuration doctrine.yaml :
doctrine: orm: second_level_cache: enabled: true regions: write_rare: lifetime: 864000 cache_driver: { type: service, id: cache.app }
- Dans le repository :
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
Dans cet exemple, on n'utilise pas cache.system
pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. cache.app
pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Expressions
modifierPour ajouter une expression en DQL, utilise $qb->expr()
. Ex[9] :
$qb->expr()->count('u.id')
$qb->expr()->between('u.id', 2, 10)
(entre 2 et 10)$qb->expr()->gte('u.id', 2)
(plus grand ou égal à 2)$qb->expr()->like('u.name', '%son')
$qb->expr()->lower('u.name')
$qb->expr()->substring('u.name', 0, 1)
Injection de dépendances
modifierLes repository DQL deoivent ServiceEntityRepository :
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
- Étendre une classe qui étend ServiceEntityRepository.
- Le redéfinir dans services.yaml.
- Utiliser un trait.
Transactions
modifierPour garantir d'intégrité d'une transaction[10] :
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
Il existe aussi une syntaxe alternative :
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
Évènements
modifierPour ajouter des triggers sur la mise à jour d'une table, ajouter dans son entité l'attribut #[ORM\HasLifecycleCallbacks]
(anciennement l'annotation @ORM\HasLifecycleCallbacks()
). Voici les évènements utilisables ensuite (dans les listeners / subscribers) :
prePersist
modifierSe produit avant la persistance d'une entité (paramètre : PrePersistEventArgs $args
).
postPersist
modifierSe produit après la persistance d'une entité (PostPersistEventArgs $args
).
preUpdate
modifierSe produit avant l'update d'une entité (PreUpdateEventArgs $args
).
postUpdate
modifierSe produit après l'update d'une entité (PostUpdateEventArgs $args
).
preRemove
modifierSe produit avant l'update d'une entité (PreRemoveEventArgs $args
).
postRemove
modifierSe produit après l'update d'une entité (PostRemoveEventArgs $args
).
preFlush
modifierSe produit avant la sauvegarde d'une entité (PreFlushEventArgs $args
).
- Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
- Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
- Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler
$unitOfWork->computeChangeSet($classMetadata, $entity)
[11].
Ex :
$uow = $em->getUnitOfWork(); $uow->computeChangeSets(); if ($uow->isEntityScheduled($myEntity)) { //... }
LifecycleEventArgs $args
dans ces fonctions.
Parfois le $object::class
peut renvoyer Proxies\__CG__\App\Entity\MyEntity
au lieu de App\Entity\MyEntity
, selon le cache utilisé.
postFlush
modifierSe produit après la sauvegarde d'une entité (PostFlushEventArgs $args
).
Migrations
modifierPour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
composer require doctrine/doctrine-migrations-bundle
Création
modifierEnsuite, on peut créer un squelette de "migration" :
php bin/console doctrine:migrations:generate
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée migration_versions
enregistre leur succession).
Exemple SQL
modifierfinal class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
Exemple DQL
modifierfinal class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
Exemple PHP
modifierfinal class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.
$this->containergetParameter()
ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple $_SERVER['SUBAPI_URI']
renvoie la variable d'environnement et $this->containergetParameter('env(SUBAPI_URI)')
sa valeur par défaut (définie dans services.yaml).
Exécution
modifierLa commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
php bin/console doctrine:migrations:migrate
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
Pour le rollback :
php bin/console doctrine:migrations:execute --down 20170321095644
Pour éviter que Doctrine pose des questions durant les migrations, ajouter --no-interaction
(ou -n
).
Pour voir le code SQL au lieu de l'exécuter : --write-sql
.
Sur plusieurs bases de données
modifierPour exécuter sur plusieurs bases :
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
Avec des migrations.yaml de type :
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
Synchronisation
modifierVers le code
modifierVers les entités
modifierphp bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer Rector pour convertir les annotations. Ex :
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable vendor/bin/rector process src/Entity/MyNewEntity.php
Vers les migrations
modifierPour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
Vers la base
modifierÀ contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
Fixtures
modifierIl existe plusieurs bibliothèques pour créer des fixtures, dont une de Doctrine[13] :
composer require --dev orm-fixtures
Pour charger les fixtures du code dans la base :
php bin/console doctrine:fixtures:load -n
Types de champ
modifierLa liste des types de champ Doctrine se trouve dans Doctrine\DBAL\Types
. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre JsonType
pour surcharger le type JSON par défaut afin de lui faire faire json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne[14].
Réplication SQL
modifierAnciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml. Ex[15] :
doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' server_version: '8.0.35' replicas: replica1: url: '%env(resolve:REPLICA_DATABASE_URL)%'
Critique
modifier- Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
- Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le lazy loading est contourné par au choix :
- Avant l'appel null, un
ObjetJoint->get()
. - Dans l'entité, un
@ManyToOne(…, fetch="EAGER")
. - Dans le repository, un
$this->queryBuilder->addSelect()
.
- Avant l'appel null, un
- Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
- Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
- Attention aux
$this->queryBuilder->setMaxResults()
et$this->queryBuilder->setFirstResult()
en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar duLIMIT
SQL). La solution consiste à ajouter un paginateur[16]. - L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
- Pas de hints.
- Bug des
UNION ALL
quand on joint deux entités non liées dans le repo.
Références
modifier- ↑ https://symfony.com/doc/current/doctrine.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string
- ↑ https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations
- ↑ https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html
- ↑ https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly
- ↑ https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener
- ↑ https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2
- ↑ https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
- ↑ https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23
- ↑ https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35
- ↑ https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939
API
Pour créer une interface de programmation (API) REST avec Symfony, il existe plusieurs bibliothèques :
- API Platform[1], tout-en-un qui utilise les attributs PHP (ou des annotations en PHP < 8) des entités pour créer les APIs (donc pas besoin de créer des contrôleurs ou autres). Par défaut il permet de sérialiser les flux en JSON (dont JSON-LD, JSON-HAL, JSON:API), XML (dont HTML), CSV, YAML, et même en GraphQL[2].
- Sinon il faut combiner plusieurs éléments : routeur, générateur de doc en ligne et sérialiseur.
API Platform
modifierInstallation
modifiercomposer require api
Utilisation
modifier
Le GraphQL de la version 1.1 passe par le schéma REST, et ne bénéficie donc pas du gain de performances attendu sans overfetching.
En bref, les routes d'API sont définies depuis les entités Doctrine.
Pour ajouter des fonctionnalités supplémentaires aux create/read/update/delete, il faut passer par des data providers[3] ou des data persisters[4], pour transformer les données, respectivement à l'affichage et à la sauvegarde.
Attributs
modifierApiResource
modifierDéfinit les noms et méthodes REST (GET, POST...) des routes de l'API.
Exemple sur la V3[5] :
#[ApiResource( operations: [ new Get(), new GetCollection() ] )] class MyEntity...
Avec personnalisation de la vue OpenAPI :
#[ApiResource( operations: [ new Get(), new GetCollection(), ], openapiContext: [ 'summary' => '', 'tags' => ['Enums'], ] )]
Idem sur la V2
#[ApiResource( collectionOperations: [ 'get' => [ 'openapiContext' => [ 'summary' => '', 'tags' => ['Enums'], ], ], ], itemOperations: [ 'get' => [ 'openapiContext' => [ 'summary' => '', 'tags' => ['Enums'], ], ], ], )]
MaxDepth
modifierDéfinit le niveau de sérialisation d'un élément lié. Par exemple, si un client a plusieurs contrats et que ses contrats ont plusieurs produits, un MaxDepth(1) sur l'attribut client->contrat fera que la liste des clients comprendra tous les contrats mais pas leurs produits.
Évènements
modifierAPI Platform ajoute plus d'une dizaine d'évènements donc les priorités sont définies dans EventPriorities[6].
Par exemple, pour modifier un JSON POST envoyé, utiliser EventPriorities::PRE_DESERIALIZE.
L'évènement suivant POST_DESERIALIZE contient les objets instanciés à partir du JSON.
Triplet de bibliothèques
modifierInstallation
modifierFOS REST
modifierFOSRestBundle apporte des annotations pour créer des contrôleurs d'API[7]. Installation :
composer require "friendsofsymfony/rest-bundle"
Puis dans config/packages/fos_rest.yaml :
fos_rest:
view:
view_response_listener: true
format_listener:
rules:
- { path: '^/', prefer_extension: true, fallback_format: ~, priorities: [ 'html', '*/*'] }
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
Documentation
modifierToute API doit exposer sa documentation avec ses routes et leurs paramètres. NelmioApiDocBundle est un de générateur de documentation automatique à partir du code[8], qui permet en plus de tester en ligne. En effet, pour éviter de tester les API en copiant-collant leurs chemins dans une commande cURL ou dans des logiciels plus complets comme Postman[9], on peut installer une interface graphique ergonomique qui allie documentation et test en ligne :
composer require "nelmio/api-doc-bundle"
Son URL se configure ensuite dans routes/nelmio_api_doc.yml :
app.swagger_ui:
path: /api/doc
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
À ce stade l'URL /api/doc affiche juste un lien NelmioApiDocBundle. Mais si les contrôleurs d'API sont identifiés dans annotations.yaml (avec un préfixe "api"), on peut voir une liste automatique de toutes leurs routes.
Pour documenter /api/* sauf /api/doc, il faut préciser l'exception en regex dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
areas:
path_patterns:
- ^/api/(?!/doc$)
Il faut vider le cache de Symfony à chaque modification de nelmio_api_doc.yaml.
Authentification
modifierPour tester depuis la documentation des routes nécessitant un token JWT, ajouter dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
documentation:
securityDefinitions:
Bearer:
type: apiKey
description: 'Value: Bearer {jwt}'
name: Authorization
in: header
security:
- Bearer: []
Il devient alors possible de renseigner le token avant de tester.
Exemple
modifierDans un contrôleur, au-dessus de l'attribut #[Route]
d'un CRUD :
use OpenApi\Attributes as OA; ... #[OA\Post( requestBody: new OA\RequestBody( required: true, content: [ new OA\JsonContent( examples: [ new OA\Examples('1', summary: 'By ID', value: '{ "myEntity": { "id": 1 }}'), new OA\Examples('2', summary: 'By name', value: '{ "myEntity": { "name": "TEST" }}'), ], type: 'object', ), ], ), tags: ['MyEntities'], responses: [ new OA\Response( response: Response::HTTP_OK, description: 'Returns myEntity information.', content: new OA\JsonContent( properties: [ new OA\Property( property: "id", type: "integer", example: 1, nullable: true ), new OA\Property( property: "name", type: "string", example: 'TEST', nullable: true ), ] ) ), new OA\Response( response: Response::HTTP_NOT_FOUND, description: 'Returns no myEntity.', content: new OA\JsonContent( properties: [ new OA\Property( property: "id", type: "integer", example: null, nullable: true ), ] ) ) ] )]
Sérialiseur
modifierEnfin pour la sérialisation, on distingue plusieurs solutions :
- symfony/serializer, qui donne des contrôleurs
extends AbstractFOSRestController
et des méthodes aux annotations@Rest\Post()
[10]. - jms/serializer-bundle, avec des contrôleurs
extends RestController
et des méthodes aux annotations@ApiDoc()
. - Le service
fos_rest.service.serializer
.
symfony/serializer
modifiercomposer require "symfony/serializer"
jms/serializer-bundle
modifiercomposer require "jms/serializer-bundle"
Utilisation
modifierMaintenant /api/doc affiche les méthodes des différents contrôleurs API. Voici un exemple :
<?php
namespace App\Controller;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class APIController extends AbstractFOSRestController
{
#[Route('/api/test', methods: ['GET'])]
public function testAction(Request $request): View
{
return View::create('ok');
}
}
Dans PHP < 8
...
/**
* @Route("/api/test", methods={"GET"})
*/
public function testAction(Request $request): View
...
}
Maintenant dans /api/doc, cliquer sur /api/test, puis "Ty it out" pour exécuter la méthode de test.
Sécurité
modifierUne API étant stateless, l'authentification est assurée à chaque requête, par l'envoi par le client d'un token JSON Web Token (JWT) dans l'en-tête HTTP (clé Authorization). Côté serveur, on transforme ensuite le JWT reçu en objet utilisateur pour accéder à l'identité du client au sein du code. Ceci est fait en configurant security.yaml, pour qu'un évènement firewall appelle automatiquement un guard authenticator[11] :
composer require "symfony/security-bundle"
security:
firewalls:
main:
guard:
authenticators:
- App\Security\TokenAuthenticator
Manipuler les JWT
modifierPour décrypter le JWT :
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; ... public function __construct( private readonly JWTEncoderInterface $jwtEncoder, ) { } public function decodeJwt(string $jwt): array { return $this->jwtEncoder->decode($jwt): }
Résultat minimum :
array:6 [ "iat" => 1724677672 "exp" => 1724764072 "roles" => array:1 [ 0 => "ROLE_INACTIF" ] "username" => "test" ]
Si on n'a pas besoin de vérifier le JWT (validité, expiration et signature modifiée par un pirate), on peut se passer de Lexik ainsi :
private function decodeJwt(string $jwt): array|bool|null { $tokenParts = explode('.', $jwt); if (empty($tokenParts[1])) { return []; } $tokenPayload = base64_decode($tokenParts[1]); return json_decode($tokenPayload, true); }
Test
modifierPour tester en shell :
TOKEN=123 curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" http://localhost
Références
modifier- ↑ https://api-platform.com/
- ↑ https://api-platform.com/docs/core/content-negotiation/
- ↑ https://api-platform.com/docs/core/data-providers/
- ↑ https://api-platform.com/docs/core/data-persisters/
- ↑ https://api-platform.com/docs/core/operations/
- ↑ https://api-platform.com/docs/core/events/
- ↑ https://github.com/FriendsOfSymfony/FOSRestBundle
- ↑ https://github.com/nelmio/NelmioApiDocBundle
- ↑ https://www.postman.com/
- ↑ https://www.thinktocode.com/2018/03/26/symfony-4-rest-api-part-1-fosrestbundle/
- ↑ https://symfony.com/doc/current/security/guard_authentication.html
GFDL | Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture. |