# Remise à niveau Symfony3 ## William Petit - S.C.O.P. Cadoles --- # Les principales nouveautés de Symfony 3 - Simplification de l'authentification avec le système de "Guard" - Composant LDAP pour l'authentification (compatible Active Directory) - Amélioration des mécanismes d'injection de dépendances: "auto-wiring", services dépréciés... - Un "MicroKernel" pour créer des applications minimalistes basées sur Symfony3 - Et beaucoup d'autres améliorations et composants mineurs... --- # Structure d'un projet, générateurs et "bundles" ## Amorçage d'un projet ## Notion de "bundle" ## Structuration d'un projet ## La console, les commandes et les générateurs --- ## Amorçage d'un projet ### Récupération de l'installeur Symfony ```bash sudo mkdir -p /usr/local/bin sudo curl -LsS https://symfony.com/installer \ -o /usr/local/bin/symfony sudo chmod a+x /usr/local/bin/symfony ``` ### Création du projet (avec la dernière LTS) ```bash symfony new 3.4 ``` --- ## Notion de "bundle" > Un "bundle" est un ensemble de fichiers (code, templates, etc) représentant une unité fonctionnelle dans le framework Symfony. **Avant Symfony 3** Les "bundles" étaient le moyen utilisé pour structurer le code d'une application. Une application pouvait donc avoir plusieurs bundles "internes", i.e. ne provenant pas d'un développeur tiers. **Après Symfony 3** Les bundles ne sont plus considérés à titre organisationnel mais uniquement comme un moyen de diffuser le code sous forme d'unité réutilisable. Une application ne devrait donc plus qu'avoir un seul bundle interne: `AppBundle`. --- ## Structuration d'un projet (1) ### Les principaux répertoires et leur rôle |Répertoire|Rôle| |:-|:-| |`app/config/`|Configuration de l'application| |`app/Resources/`|Depuis Symfony3, répertoire contenant les vues ainsi les "assets" de l'application| |`src/AppBundle/`|Sources de l'application| --- ## Structuration d'un projet (2) |Répertoire|Rôle| |:-|:-| |`tests/`|Répertoire contenant les tests de l'application (unitaire comme fonctionnels)| |`var/`|Répertoire contenant toutes les données "vivantes" de l'application| |`vendor/`| Dépendances Composer |`web/`| Répertoire des ressources publiques de l'application --- ## La console, les commandes et les générateurs (1) ### Quelques commandes (très) utiles |Commande|Description| |:-|:-| |`server:run`|Exécuter l'application avec le serveur HTTP PHP| |`security:check`|Vérifier que les dépendances du projet ne comportent pas de vulnérabilités connues| |`debug:container`|Retrouver le mapping services <-> classe PHP| |`router:match`|Vérifier quelle controleur/action sera utilisée pour un chemin donné| --- ## La console, les commandes et les générateurs (2) ### Les générateurs par défaut |Commande|Description| |:-|:-| |`generate:bundle`|Créer un nouveau bundle| |`generate:controller`|Créer un nouveau contrôleur| |`generate:command`|Créer une nouvelle commande| |`doctrine:generate:entity`|Créer une nouvelle entité Doctrine| --- # Le routage et les contrôleurs ## Création d'un nouveau contrôleur ## Déclaration des contrôleurs ## Actions, routes et verbes HTTP avec les annotations --- ## Création d'un nouveau contrôleur ```bash bin/console generate:controller ``` --- ## Déclaration des contrôleurs --- ## Actions, routes et verbes HTTP avec les annotations (1) ### Action annotée basique ```php class DemoController extends Controller { /** * @Route("/demo") */ public function demoAction() { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (2) ### Action avec méthodes HTTP contraintes ```php class DemoController extends Controller { /** * @Route( * "/demo", * methods = { "POST", "PUT" } * ) */ public function demoAction() { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (3) ### Action nommée ```php class DemoController extends Controller { /** * @Route("/demo-named", name="my_demo_action") */ public function demoAction() { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (4) ### Action avec paramètres ```php class DemoController extends Controller { /** * @Route("/demo/{myParam}") */ public function demoAction($myParam) { // ... } /** * @Route("/demo/{paramWithDefaultVal}") */ public function demoAction($paramWithDefaultVal = 1) { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (5) ### Action avec paramètres contraints ```php class DemoController extends Controller { /** * @Route( * "/demo/{myParam}", * requirements = { "myParam"="^\d+$" } * ) */ public function demoAction($myParam) { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (6) ### Action avec paramètres contraints ```php class DemoController extends Controller { /** * @Route( * "/demo/{myParam}", * requirements = { "myParam"="^\d+$" } * ) */ public function demoAction($myParam) { // ... } } ``` --- ## Actions, routes et verbes HTTP avec les annotations (7) ### Paramètres spéciaux |Paramètre|Description| |:-|:-| |`_locale`| La "locale" utilisé par l'application pour la requête en cours | |`_format`| Le "format" utilisé par l'application pour la requête en cours | |`_controller`| L'identifiant du contrôleur et son action utilisés pour traiter la requête en cours| --- ## Actions, routes et verbes HTTP avec les annotations (8) ### Générer une URL pour une route nommée ```php use Symfony\Component\Routing\Generator\UrlGeneratorInterface; $this->generateUrl( 'demo', // Nom de la route ['myParam' => 'my-value'], // Paramètres à injecter // L'URL générée doit elle être absolue ou non ? UrlGeneratorInterface::ABSOLUTE_URL ); ``` --- # Services ## Utilisation des services ## Création de nouveaux services --- ## Utilisation des services (1) ### Lister les services existants ```bash bin/console debug:container ``` --- ## Utilisation des services (2) ### Utiliser un service (méthode classique) ```php class DemoController extends Controller { /** * @Route("/demo") */ public function demoAction() { $logger = $this->get('logger'); $logger->info('Hello world !'); } } ``` --- ## Utilisation des services (3) ### Utiliser un service (Via le "type hint") ```php use Psr\Log\LoggerInterface; class DemoController extends Controller { /** * @Route("/demo") */ public function demoAction(LoggerInterface $logger) { $logger->info('Hello world !'); } } ``` **Tip** Pour identifier les différentes interfaces disponibles, utilisez la commande `bin/console debug:autowiring` --- ## Création d'un service (1) ### Création de la classe PHP ```php namespace AppBundle\Service; class MyService { public function doNothing() {} } ``` **Tip** On peut vérifier que le service est bien détecté par l'application avec la commande `bin/console debug:autowiring` --- ## Création d'un service (2) ### Utilisation du service dans un contrôleur ```php use AppBundle\Service\MyService; class DemoController extends Controller { /** * @Route("/demo") */ public function demoAction(MyService $my) { $my->doNothing(); } } ``` --- ## Création d'un service (3) ### Déclaration de dépendances inter-services ```php namespace AppBundle\Service; use Psr\Log\LoggerInterface; class MyService { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function log($message) { $this->logger->info($message); } } ``` --- ## Création d'un service (4) ### Déclaration de dépendances vers des valeurs de configuration ```php namespace AppBundle\Service; class MyService { private $myCustomParameter; public function __construct($myCustomParameter) { $this->myCustomParameter = $myCustomParameter; } } ``` --- ## Création d'un service (5) ### Déclaration explicite de la dépendance Dans `app/config/services.yml`, déclarer la dépendance explicitement: ```yaml services: AppBundle\Service\MyService: arguments: $myCustomParameter: 'test' ``` --- # Authentification et autorisation ## Le fichier `app/config/security.yml` ## Firewalls, providers et encoders ## Gestion des rôles et contrôle des accès ## Méthode d'authentification personnalisée --- ## Le fichier `app/config/security.yml` ### Authentifications et autorisations --- ## Firewalls (1) ### Déclarer une nouvelle méthode d'authentification dans un firewall ```yaml security: firewalls: main: anonymous: ~ http_basic: ~ ``` --- ## Firewalls (2) ### Multiple firewalls ```yaml security: firewalls: authenticated_area: pattern: ^/secured http_basic: ~ public_area: anonymous: ~ ``` --- ## Providers (1) ### Utilisation du provider `memory` ```yaml security: providers: in_memory: memory: users: bob: password: bob roles: 'ROLE_USER' admin: password: 123456 roles: 'ROLE_ADMIN' ``` --- ## Providers (2) ### Déclaration de l'`encoder` pour la gestion des mots de passe ```yaml security: encoders: Symfony\Component\Security\Core\User\User: plaintext ``` --- ## Providers (3) ### Utilisation d'un `encoder` apportant un meilleur niveau de sécurité ```yaml security: encoders: Symfony\Component\Security\Core\User\User: algorithm: bcrypt cost: 12 ``` #### Calcul de l'empreinte d'un mot de passe ```bash bin/console security:encode-password ``` --- ## Gestion des rôles et contrôle des accès (1) ## Définition de rôles ```yaml security: access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY } ``` --- ## Gestion des rôles et contrôle des accès (2) ## Restriction d'accès par adresse IP ```yaml security: access_control: - path: ^/local-api roles: ROLE_API_USER ips: [127.0.0.1] ``` --- ## Gestion des rôles et contrôle des accès (3) ## Définition de règles d'accès pour les actions ```php use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; class DemoController extends Controller { /** * @Route("/demo") * @Security("has_role('ROLE_ADMIN')") */ public function demoAction() { // ... } } ``` --- ## Méthode d'authentification personnalisée ### Exercice Implémenter une classe `Guard` pour créer un firewall pour un serveur d'authentification "passwordless" basé sur JWT. **Ressources** - Tutoriel Symfony3 sur l'utilisation de la classe Guard https://symfony.com/doc/3.4/security/guard_authentication.html - Instance de démonstration du serveur d'authentification: https://forge.cadoles.com/wpetit/ciku - Sources du serveur d'authentification: https://forge.cadoles.com/wpetit/ciku --- # Les vues et le moteur de templating Twig ## Organisation des vues ## Syntaxe Twig ## Héritage et composition ## Étendre Twig --- ## Organisation des vues dans le projet --- ## Syntaxe Twig ```twig

{{ myVar }}

``` --- ## Héritage et composition (1) ### Héritage ```twig // parent.html.twig {% block body %}
Default content
{% endblock %} ``` ```twig // child.html.twig {% extends 'parent.html.twig' %} {% block body %}
Child content
{% endblock %} ``` --- ## Héritage et composition (21) ### Composition ```twig // layout.html.twig {% for i in items %} {{ include('_partial.html.twig', { 'item': i }) }} {% endfor %} ``` ```twig // _partial.html.twig
Item #{{ item.id }}
``` --- ## Étendre Twig (1) ### Créer son extension ```php // src/AppBundle/Twig/AppExtension.php namespace AppBundle\Twig; class AppExtension extends \Twig_Extension { public function getFilters() { return [ new \Twig_SimpleFilter('pad', [$this, 'pad']), ]; } public function pad( $input, $length, $pattern = ' ', $type = STR_PAD_LEFT ) { return str_pad($input, $length, $pattern, $type); } } ``` --- ## Étendre Twig (2) ### Enregistrer l'extension comme service ```yaml // app/config/services.yml services: AppBundle\Twig\AppExtension: tags: [ twig.extension ] ``` --- ## Étendre Twig (3) ### Utiliser le filtre ```twig

{{ "1" | pad(10, "0") }}

``` --- # L'ORM Doctrine et le modèle de données ## Configuration ## Migration du schéma ## Entités et dépôts ## Les évènements --- ## Configuration --- ## Migration du schéma (1) ### En développement ```bash bin/console doctrine:schema:update --force ``` --- ## Migration du schéma (2) ### En production 1. Récupération du schéma de la base en production en version `[origine]` 2. Création d'un script de migration SQL en fonction des nouvelles entités de la version `[cible]` ```bash mkdir -p migrations bin/console doctrine:schema:update \ --dump-sql > migration-[origine]-[cible].sql ``` 3. Vérification/modification manuelle du script de migration 4. Passage du script de migration validé en production --- ## Entités et dépôts (1) ### Génération d'entités ```bash bin/console doctrine:generate:entity ``` --- ## Entités et dépôts (2) ### La classe de l'entité ```php /** * Post * @ORM\Table(name="post") * @ORM\Entity( * repositoryClass="AppBundle\Repository\PostRepository" * ) */ class Post { /** * @var int * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * @ORM\Column(name="Content", type="text", nullable=true) */ private $content; // [...] } ``` --- ## Entités et dépôts (3) ### Les annotations de classe |Annotation|Paramètres (non exhaustifs)|Description| |:-|:-|:-| |`@ORM\Table`|`name`: nom de la table| Description de la table associée à la base de données| |`@ORM\Entity`|`repositoryClass`: identifiant de la classe `EntityRepository` associée à cette entité| Métadonnées de contexte liées à l'entité| --- ## Entités et dépôts (4) ### Les annotations d'attributs |Annotation|Paramètres (non exhaustifs)|Description| |:-|:-|:-| |`@ORM\Column`|`name`: nom de la colone, `type`: type de la colone, `nullable`| Description de la colonne dans la base de donnée associée à l'attribut| |`@ORM\Id`|| Déclare l'attribut comme une clé primaire dans la table| |`@ORM\GeneratedValue`|`strategy`: stratégie de génération de la valeur de l'attribut | Déclare l'attribut comme généré automatiquement par la base de donnée | --- ## Entités et dépôts (5) ### Accéder aux gestionnaires d'entités dans un contrôleur ```php use Doctrine\Common\Persistence\ObjectManager; class DemoController extends Controller { /** * @Route("/demo") */ public function demoAction(ObjectManager $em) { // Faire quelque chose avec l'entité manager } } ``` --- ## Entités et dépôts (6) ### Enregistrer un nouvelle entité dans la base ```php use AppBundle\Entity\Post; // [...] // La variable $em est récupéré en tant que service // On créait une instance de notre entité $post = new \Post(); // On modifie les valeurs des attributs de notre instance $post->setContent("hello world"); // On référence notre instance comme nouvelle entité à persister $em->persist($post); // On fait appliquer à l'ORM les modifications de l'"unit of work" $em->flush(); ``` --- ## Entités et dépôts (7) ### Modifier une entité existante ```php $repository = $em->getRepository(Post::class); // On récupère une instance de notre entité à // partir de son identifiant depuis la base de données $post = $repository->find($id); // On modifie les valeurs des attributs de notre instance $post->setContent("foo bar"); // On fait appliquer à l'ORM les modifications de l'"unit of work" $em->flush(); ``` --- ## Entités et dépôts (8) ### Supprimer une entité ```php $repository = $em->getRepository(Post::class); // On récupère une instance de notre entité à partir // de son identifiant depuis la base de données $post = $repository->find($id); $em->remove($post); // On fait appliquer à l'ORM les modifications de l'"unit of work" $em->flush(); ``` --- ## Entités et dépôts (9) ### Les relations: exemple `one to many` ```php use Doctrine\Common\Collections\ArrayCollection; class Post { /** * @var Doctrine\Common\Collections\ArrayCollection * @ORM\OneToMany(targetEntity="Comment", mappedBy="post") */ private $comments; } ``` --- ### Les relations: `one to many` ```php class Comment { /** * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments") * @ORM\JoinColumn(name="post_id", referencedColumnName="id") */ private $post; } ``` --- ## Exercice: relation `many to many` Créer une entité "SocialUser" ayant un attribut "friends" qui est une collection de "SocialUser". Tester l'ajout de relation et vérifier que la relation est bien bidirectionnelle comme on le souhaiterait dans le cas de la conception d'un réseau social. --- ## Les évènements Doctrine et Symfony (1) ### Création du `Listener` ```php // src/AppBundle/EventListener/SearchIndexer.php namespace AppBundle\EventListener; use Doctrine\ORM\Event\LifecycleEventArgs; use AppBundle\Entity\Post; class PostUpdateNotifier { public function postPersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); if (!$entity instanceof Post) { return; } // Faire quelque chose avec le Post... } } ``` --- ## Les évènements Doctrine et Symfony (2) ### Référencer le service ```yaml // app/config/services.yml services: AppBundle\EventListener\PostCreationNotifier: tags: - { name: doctrine.event_listener, event: postPersist } ``` --- # Les formulaires ## Création et traitement de formulaires ## Validation des données ## Les évènements --- ## Création et traitement de formulaires (1) ### Générer les formulaires CRUD pour une entité ```bash bin/console doctrine:generate:crud ``` --- ### Exercice proposé Créer une micro application de type "TodoList". L'application devra comprendre une entité `Task` avec les attributs suivants: - Un status: à faire | en cours | fermé - Un texte de description - Un label La mise à jour de l'état d'une tâche devra automatiquement déclencher l'envoi d'un courriel à une adresse donnée (fixe, paramétrée dans le fichier `parameters.yml` de l'application). **Ressources** - https://symfony.com/doc/3.4/email.html - http://symfony.com/doc/3.4/doctrine/event_listeners_subscribers.html --- ## Création et traitement de formulaires (2) ### La classe `AbstractType` ### Construction des champs ### Définition des options ### Utilisation du formulaire dans le contrôleur ### Rendu du formulaire dans un template Twig --- ## Création et traitement de formulaires (3) ### Formulaires en tant que services #### Déclararation des dépendances #### Déclarations du services ```yaml // app/config/services.yml services: App\Form\PostType: tags: [form.type] ``` --- ## Création et traitement de formulaires (4) ### Événements et formulaires dynamiques Source: https://symfony.com/doc/current/form/events.html --- ## Validation des données (1) ### Installer le composant `validator` ```bash composer require validator ``` ### Configurer le composant ```yaml # app/config/config.yml framework: validation: { enable_annotations: true } ``` --- ## Validation des données (2) ### Les annotations `Àssert` ```php class MyEntity { /** * @Assert\NotBlank() */ public $name; } ``` Les différentes validations pré-existantes: `NotBlank`, `Blank`, `NotNull`, `IsNull`, `IsTrue`, `IsFalse`, `Email`, `Url`... Voir http://symfony.com/doc/3.4/validation.html#supported-constraints --- ## Validation des données (3) ### Utilisation basique ```php // On récupère le service "validator" // depuis le conteneur de services $validator = $this->get('validator'); $errors = $validator->validate($myObject); if ( count($errors) > 0 ) { // Traiter les erreurs } ``` --- ## Validation des données (4) ### Utilisation avec les formulaires --- # Mise en production ## Configuration de Nginx ## Déploiement --- ## Configuration de Nginx ``` server { server_name myapp.fr www.myapp.fr; root /var/www/myapp/web; location / { try_files $uri /app.php$is_args$args; } location ~ ^/app\.php(/|$) { fastcgi_pass unix:/var/run/php7.1-fpm.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Permet d'empecher les URL du type "http://myapp/index.php/sub-path" internal; } location ~ \.php$ { return 404; } error_log /var/log/nginx/myapp.log; access_log /var/log/nginx/myapp.log; } ``` --- ## Déploiement de l'application ### Checklist après le déploiement d'une nouvelle version 1. Vérifier que l'environnement est viable ```bash php bin/symfony_requirements ``` 2. Optimisation de l'autoloader `composer` ```bash export SYMFONY_ENV=prod composer install \ --no-dev \ --optimize-autoloader \ --classmap-authoritative ``` 3. Nettoyer/réamorcer le cache applicatif ```bash bin/console cache:clear --env=prod --no-debug --no-warmup bin/console cache:warmup --env=prod ``` 4. (Au besoin) Générer les assets via assetic ```bash bin/console assetic:dump --env=prod --no-debug ``` --- 5. S'assurer que la configuration de Doctrine est viable ```bash bin/console doctrine:ensure-production-settings ``` --- ## Sujets spécifiques ### Gestion des traductions ### Utilisation du micro-kernel ### Les tests ### Gestion des assets avec Webpack ### Formulaires et gestion des thèmes --- ## Gestion des traductions ### Installation du composant ### Le service `translator` ### Gestion des traductions --- ## Installation du composant ```bash composer require symfony/translation ``` --- ## Le service `translator` ### Gestion de la `locale` ### Utilisation du service `translator` ### Utilisation dans un template Twig ### Pluralisation ### Organisation par domaine --- ## Gestion de la `locale` (1) ### Récupérer la locale dans un controleur ```php use Symfony\Component\HttpFoundation\Request; public function indexAction(Request $request) { $locale = $request->getLocale(); } ``` --- ## Gestion de la `locale` (2) ### Modifier la `locale` via un listener sur l'évènement `kernel.request` ```php public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $request->setLocale($locale); } ``` > **Attention** La modification de la `locale` dans un contrôleur sera ignorée par les services dans la majoritée des cas. --- ## Gestion de la `locale` (3) ### Définir la `locale` via les paramètres de l'action ```php class LocalizedController extends Controller { /** * @Route( * "/{_locale}/my-page", * requirements = { "_locale"="fr|en" } * ) */ public function localizedAction() { // ... } } ``` --- ## Gestion de la `locale` (4) ### Définir la `locale` par défaut ```php # app/config/config.yml framework: # ... translator: { fallbacks: ['%locale%'] } # app/config/parameters.yml parameters: # ... locale: en ``` --- ## Utilisation du service `translator` ```php use Symfony\Component\Translation\TranslatorInterface; //... public function localizedAction(TranslatorInterface $translator) { $translated = $translator->trans( 'my.translation.key', // Clé de la chaine à traduire ['key' => $val ], // Variables à injecter, optionnel ); } ``` --- ## Utilisation dans un template Twig ```twig {% trans %}Hello %name%{% endtrans %} {{ 'my.translation.key' | trans() }} {% trans with { '%foo%': 'bar' } %}Hello %foo%{% endtrans %} {{ 'my.translation.key' | trans({ '%foo%': 'bar' }) }} ``` --- ## Pluralisation (1) ### Simple ``` Il y a une pomme|Il y a %count% pommes ``` ### Intervalles et valeurs spécifiques ``` {0} Il n'y a aucune pomme. | [1,20[ Il y %count% pommes. | [20,Inf[ Il y a beaucoup de pommes. ``` --- ## Pluralisation (2) ### Utilisation ```php $translator->transchoice( 'my.translation.key', // Clé de la chaine à traduire $count, // Nombre d'occurences pour la pluralisation [ 'key' => $val ], // Variables à injecter ); ``` --- ## Organisation par domaine --- ## Gestion des traductions ### Générer/mettre à jour les fichiers XLIFF ### Manipuler les fichiers XLIFF ### Ajout de ressources supplémentaires --- ## Générer/mettre à jour les fichiers XLIFF ```bash bin/console translation:update --output-format=xlf --force ``` --- ## Manipuler les fichiers XLIFF > Démo de [Virtaal](http://virtaal.translatehouse.org/) --- ## Utilisation du micro-kernel --- ## Ajout de ressources supplémentaires ```yaml # app/config/config.yml framework: translator: paths: - '%kernel.project_dir%/translations' ``` --- ## Les tests --- ## Gestion des assets avec Webpack --- ## Formulaires et gestion des thèmes --- # Licence ## CC BY-NC-SA 3.0 FR [Creative Commons - Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/)