formations/developpement/symfony3/presentation/slides.md

33 KiB
Raw Blame History

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

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)

symfony new <my_project_name> 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

bin/console generate:controller

Déclaration des contrôleurs


Actions, routes et verbes HTTP avec les annotations (1)

Action annotée basique

class DemoController extends Controller
{
    /**
     * @Route("/demo")
     */
    public function demoAction()
    {
        // ...
    }

}

Actions, routes et verbes HTTP avec les annotations (2)

Action avec méthodes HTTP contraintes

class DemoController extends Controller
{
    /**
     * @Route(
     * 	"/demo", 
     * 	methods = { "POST", "PUT" }
     * )
     */
     public function demoAction()
    {
        // ...
    }
}

Actions, routes et verbes HTTP avec les annotations (3)

Action nommée

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

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

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

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

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

bin/console debug:container

Utilisation des services (2)

Utiliser un service (méthode classique)

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")

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

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

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

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

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:

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

security:
  firewalls:
    main:
      anonymous: ~
      http_basic: ~

Firewalls (2)

Multiple firewalls

security:

 firewalls:
 
   authenticated_area:
     pattern: ^/secured
     http_basic: ~
     
   public_area:
     anonymous: ~
     

Providers (1)

Utilisation du provider memory

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

security:
  encoders: 
    Symfony\Component\Security\Core\User\User: plaintext

Providers (3)

Utilisation d'un encoder apportant un meilleur niveau de sécurité

security:
  encoders: 
    Symfony\Component\Security\Core\User\User:
      algorithm: bcrypt
      cost: 12

Calcul de l'empreinte d'un mot de passe

bin/console security:encode-password

Gestion des rôles et contrôle des accès (1)

Définition de rôles

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

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

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


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

<html>
  <body>
    <p>{{ myVar }}</p>
    <ul>
      {% for i in items %}
      <li>{{ i.text }}</li>
      {% endfor %}
    </ul>
  </body>
</html>

Héritage et composition (1)

Héritage

// parent.html.twig
<html>
	<body>
    	{% block body %}
        <div>
        	Default content
        </div>
        {% endblock %}
    </body>
</html>
// child.html.twig
{% extends 'parent.html.twig' %}
{% block body %}
	<div>
    	Child content
    </div>
{% endblock %}

Héritage et composition (21)

Composition

// layout.html.twig
<html>
  <body>
    {% for i in items %}
    	{{ include('_partial.html.twig', { 'item': i }) }}
    {% endfor %}
  </body>
</html>
// _partial.html.twig
<div>
	Item #{{ item.id }}
</div>

Étendre Twig (1)

Créer son extension

// 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

// app/config/services.yml

services:

  AppBundle\Twig\AppExtension:
    tags: [ twig.extension ]

Étendre Twig (3)

Utiliser le filtre

<p>{{ "1" | pad(10, "0") }}</p>

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

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]
    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

bin/console doctrine:generate:entity

Entités et dépôts (2)

La classe de l'entité

/**
 * 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

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

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

$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é

$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

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

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

// 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

// 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é

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


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

// 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

composer require validator

Configurer le composant

# app/config/config.yml
framework:
    validation: { enable_annotations: true }

Validation des données (2)

Les annotations Àssert

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


// 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
    php bin/symfony_requirements
    
  2. Optimisation de l'autoloader composer
    export SYMFONY_ENV=prod
    composer install \
    	--no-dev \
    	--optimize-autoloader \
    	--classmap-authoritative
    
  3. Nettoyer/réamorcer le cache applicatif
    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
    bin/console assetic:dump --env=prod --no-debug
    

  1. S'assurer que la configuration de Doctrine est viable
    bin/console doctrine:ensure-production-settings
    

Sujets spécifiques

Internationalisation

Utilisation du micro-kernel

Les tests

Gestion des assets avec Webpack

Formulaires et gestion des thèmes


Internationalisation

Installation du composant

Le service translator

Gestion des traductions

Exercice


Installation du composant

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

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

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

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

# app/config/config.yml
framework:
    # ...
    translator: { fallbacks: ['%locale%'] }
    
# app/config/parameters.yml
parameters:
    # ...
    locale:     en

Utilisation du service translator


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


<!-- La variable %name% est directement injectée depuis le contexte Twig --!>

{% trans %}Hello %name%{% endtrans %}

<!-- Via la notation "filtre" -->

{{ 'my.translation.key' | trans() }}

<!-- Avec des paramètres supplémentaires -->

{% 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

$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

bin/console translation:update <language> --output-format=xlf --force

Manipuler les fichiers XLIFF

Démo de Virtaal


Ajout de ressources supplémentaires

# app/config/config.yml
framework:
    translator:
        paths:
            - '%kernel.project_dir%/translations'

Exercice

Créer une page de visualisation d'un mini réseau social. Celle ci devra comprendre les fonctionnalités suivantes:

  • Un affichage en 2 langues (français, anglais)
  • Un affichage du nombre de relations total, avec approximation pour un nombre supérieur à 20 ("Vous avez beaucoup de relations")
  • Les possibilité d'ajouter/supprimer un ami par son nom. En cas d'erreur, le message devra être également localisé.


Utilisation du micro-kernel

Initialisation du projet

Ajout et configuration de bundles

Exercice


Initialisation du projet (1)

mkdir my-project
cd my-project
composer init
composer require symfony/symfony
mkdir {web,var}

Initialisation du projet (2)

Voir ressources/s3-micro/index.php


Ajout et configuration de bundles (1)

Installation de Twig

composer require symfony/twig-bundle

Ajout et configuration de bundles (2)

Enregistrement du bundle

// web/index.php

// ...

public function registerBundles()
{
  return array(
    new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
    new Symfony\Bundle\TwigBundle\TwigBundle(),
  );
}

Ajout et configuration de bundles (3)

Configuration du bundle

// web/index.php

// ...

protected function configureContainer(
	ContainerBuilder $c, 
    	LoaderInterface $loader
)
{
  $c->loadFromExtension('twig', [
  	'default_path' => __DIR__.'/../templates',
  ]);
}

Ajout et configuration de bundles (4)

Utilisation du service Twig

// web/index.php

use Symfony\Component\HttpFoundation\Response;

// ...

public function twigAction()
{
  $container = $this->getContainer();
  $twig = $container->get('twig');
  
  $response = new Response();
  $response->setContent($twig->render('index.html.twig'));
  
  return $response;
}

Exercice

Avec le micro kernel Symfony, implémenter une API REST d'envoi de mail. Cette API devra utiliser le bundle Swiftmailer.

Proposition d'appel:

-> POST /send { "to": "...", "from": "...", "body": "..." } 
<- 200 { sent: true }

Les tests

phpunit

Tests unitaires

Tests fonctionnels

Exercice


phpunit

Installation via Composer

composer require phpunit/phpunit
./vendor/bin/phpunit --help

Tests unitaires (1)

Classe testée

// src/AppBundle/Util/Calculator.php

namespace AppBundle\Util;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Tests unitaires (2)

Classe de test

// tests/AppBundle/Util/CalculatorTest.php

namespace Tests\AppBundle\Util;

use AppBundle\Util\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAdd()
    {
        $calc = new Calculator();
        $result = $calc->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

Tests fonctionnels

Créer une classe de test

namespace Tests\AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DefaultControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/');

        $this->assertEquals(200, $client->getResponse()->getStatusCode());
        $this->assertContains(
        	'Welcome to Symfony', 
            	$crawler->filter('#container h1')->text()
        );
    }
}

Exercice

Créer un formulaire de contact basique avec les champs suivants:

  • Nom (obligatoire)
  • Prénom (obligatoire)
  • Adresse courriel (obligatoire, doit être une adresse valide)
  • Message (obligatoire, maximum 300 caractères)

Tester l'application des contraintes en écrivant une classe de test pour ce formulaire.

Ressource: https://symfony.com/doc/3.4/testing.html


Gestion des assets avec Webpack

Installation de Webpack-Encore

Configuration

Générer les assets

Utilisation des assets

Exercice


Installation de Webpack-Encore

npm init
npm install @symfony/webpack-encore --save-dev

Configuration (1)

Création du fichier webpack.config.js

// webpack.config.js

var Encore = require('@symfony/webpack-encore');

Encore
  .setOutputPath('web/build/') 		// Chemin de sortie des assets
  .setPublicPath('/build') 		// Définir le chemin d'accès aux assets
  
  // Activer les sources-map en dev
  .enableSourceMaps(!Encore.isProduction())
  
  // Activer le versioning des assets
  .enableVersioning() 		
  
  
  .addEntry('app', './app/Resources/js/app.js') // Ajouter une entrée
  
;

module.exports = Encore.getWebpackConfig();


Configuration (2)

Activation de la gestion des versions des assets

# app/config/config.yml
framework:
  # ...
  assets:
    json_manifest_path: '%kernel.project_dir%/web/build/manifest.json'

Générer les assets


# Générer les assets pour l'environnement de dev
./node_modules/.bin/encore dev

# Générer automatiquement les assets lorsque les sources sont modifiées
./node_modules/.bin/encore dev --watch

# Générer les assets pour la production
./node_modules/.bin/encore production

Utilisation des assets (1)

Dans les templates Twig

<!-- CSS -->
<link rel="stylesheet" src="{{ asset("build/app.css") }}">

<!-- Javascript -->
<script type="text/javascript" src="{{ asset("build/app.js") }}"></script>

Utilisation des assets (2)

Déclarer les dépendances depuis les fichiers JS

// app/Resources/js/app.js

require('./css/app.css') // Dépendance vers la feuille de style app.css

const $ = require('jquery') // Dépendance vers une librairie NPM


Exercice

Mettre en place deux pages mutualisant le code de jQuery et Bootstrap via le système de "sharedEntry" proposé par Encore.

Ressource: http://symfony.com/doc/3.4/frontend/encore/shared-entry.html


Formulaires et gestion des thèmes

Personnaliser le thème d'un formulaire dans un template

Définir/créer un thème global

Exercice


Personnaliser le thème d'un formulaire dans un template

{% extends 'base.html.twig' %}

<!-- 
	On surcharge le widget "integer" 
   	du thème global pour le formulaire "form" 
-->
{% form_theme form _self %}

{% block integer_widget %}
    <div class="integer_widget">
        {% set type = type|default('number') %}
        {{ block('form_widget_simple') }}
    </div>
{% endblock %}

{% block content %}
    {{ form(form) }}
{% endblock %}

Définir un thème global

# app/config/config.yml
twig:
    form_themes:
        - 'form/fields.html.twig'

Créer un thème global

<!-- app/Resources/form/fields.html.twig -->

<!-- On étend un thème déjà existant -->
{% extends 'form_div_layout.html.twig' %}

<!-- On surcharge les blocs souhaités -->
{% block form_widget_simple %}
    {{ parent() }}

    {% if help is defined %}
        <span class="help-block">{{ help }}</span>
    {% endif %}
{% endblock %}


Exercice

Créer un thème global de formulaire qui préfixe tous les messages d'erreur par le signe unicode "warning": ⚠.

Ressource: https://symfony.com/doc/3.4/form/form_customization.html#customizing-error-output


Licence

CC BY-NC-SA 3.0 FR

Creative Commons - Attribution - Pas dUtilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France