33 KiB
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
- 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
<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
- Récupération du schéma de la base en production en version
[origine]
- 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
- Vérification/modification manuelle du script de migration
- 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
- 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
// 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
- Vérifier que l'environnement est viable
php bin/symfony_requirements
- Optimisation de l'autoloader
composer
export SYMFONY_ENV=prod composer install \ --no-dev \ --optimize-autoloader \ --classmap-authoritative
- Nettoyer/réamorcer le cache applicatif
bin/console cache:clear --env=prod --no-debug --no-warmup bin/console cache:warmup --env=prod
- (Au besoin) Générer les assets via assetic
bin/console assetic:dump --env=prod --no-debug
- 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