# 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; } ``` --- ## 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] ``` --- ## 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 ## Gestion des environnements ## Cache applicatif --- ## Gestion des environnements --- # 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/)