1829 lines
33 KiB
Markdown
1829 lines
33 KiB
Markdown
<style>pre, table { font-size: 0.6em !important; }</style>
|
||
|
||
# Remise à niveau Symfony3
|
||
## William Petit - S.C.O.P. Cadoles
|
||
|
||
---
|
||
<!-- page_number:true -->
|
||
|
||
# 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 <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
|
||
|
||
```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
|
||
<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
|
||
|
||
```twig
|
||
// parent.html.twig
|
||
<html>
|
||
<body>
|
||
{% block body %}
|
||
<div>
|
||
Default content
|
||
</div>
|
||
{% endblock %}
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
```twig
|
||
// child.html.twig
|
||
{% extends 'parent.html.twig' %}
|
||
{% block body %}
|
||
<div>
|
||
Child content
|
||
</div>
|
||
{% endblock %}
|
||
```
|
||
---
|
||
|
||
## Héritage et composition (21)
|
||
|
||
### Composition
|
||
|
||
```twig
|
||
// layout.html.twig
|
||
<html>
|
||
<body>
|
||
{% for i in items %}
|
||
{{ include('_partial.html.twig', { 'item': i }) }}
|
||
{% endfor %}
|
||
</body>
|
||
</html>
|
||
```
|
||
```twig
|
||
// _partial.html.twig
|
||
<div>
|
||
Item #{{ item.id }}
|
||
</div>
|
||
```
|
||
---
|
||
|
||
## É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
|
||
<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
|
||
|
||
```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
|
||
|
||
### 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
|
||
|
||
```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
|
||
|
||
<!-- 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
|
||
|
||
```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 <language> --output-format=xlf --force
|
||
```
|
||
|
||
---
|
||
|
||
## Manipuler les fichiers XLIFF
|
||
|
||
> Démo de [Virtaal](http://virtaal.translatehouse.org/)
|
||
|
||
---
|
||
|
||
|
||
## Ajout de ressources supplémentaires
|
||
|
||
```yaml
|
||
# 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
|
||
|
||
```bash
|
||
composer require symfony/twig-bundle
|
||
```
|
||
|
||
---
|
||
|
||
## Ajout et configuration de bundles (2)
|
||
|
||
### Enregistrement du bundle
|
||
|
||
```php
|
||
// 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
|
||
|
||
```php
|
||
// 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
|
||
|
||
```php
|
||
// 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
|
||
|
||
```php
|
||
// src/AppBundle/Util/Calculator.php
|
||
|
||
namespace AppBundle\Util;
|
||
|
||
class Calculator
|
||
{
|
||
public function add($a, $b)
|
||
{
|
||
return $a + $b;
|
||
}
|
||
}
|
||
```
|
||
---
|
||
|
||
## Tests unitaires (2)
|
||
|
||
### Classe de test
|
||
|
||
```php
|
||
// 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
|
||
|
||
```php
|
||
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
|
||
|
||
```bash
|
||
npm init
|
||
npm install @symfony/webpack-encore --save-dev
|
||
```
|
||
---
|
||
|
||
## Configuration (1)
|
||
|
||
### Création du fichier webpack.config.js
|
||
```javascript
|
||
// 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
|
||
```yaml
|
||
# app/config/config.yml
|
||
framework:
|
||
# ...
|
||
assets:
|
||
json_manifest_path: '%kernel.project_dir%/web/build/manifest.json'
|
||
```
|
||
---
|
||
|
||
## Générer les assets
|
||
|
||
```bash
|
||
|
||
# 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
|
||
|
||
```html+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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```html+twig
|
||
{% 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
|
||
|
||
```yaml
|
||
# app/config/config.yml
|
||
twig:
|
||
form_themes:
|
||
- 'form/fields.html.twig'
|
||
```
|
||
|
||
---
|
||
|
||
## Créer un thème global
|
||
|
||
```html+twig
|
||
<!-- 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 d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/)
|