# Remise à niveau Symfony3
## William Petit - S.C.O.P. Cadoles
---
# Les principales nouveautés de Symfony 3
- Simplification de l'authentification avec le système de "Guard"
- Composant LDAP pour l'authentification (compatible Active Directory)
- Amélioration des mécanismes d'injection de dépendances: "auto-wiring", services dépréciés...
- Un "MicroKernel" pour créer des applications minimalistes basées sur Symfony3
- Et beaucoup d'autres améliorations et composants mineurs...
---
# Structure d'un projet, générateurs et "bundles"
## Amorçage d'un projet
## Notion de "bundle"
## Structuration d'un projet
## La console, les commandes et les générateurs
---
## Amorçage d'un projet
### Récupération de l'installeur Symfony
```bash
sudo mkdir -p /usr/local/bin
sudo curl -LsS https://symfony.com/installer \
-o /usr/local/bin/symfony
sudo chmod a+x /usr/local/bin/symfony
```
### Création du projet (avec la dernière LTS)
```bash
symfony new 3.4
```
---
## Notion de "bundle"
> Un "bundle" est un ensemble de fichiers (code, templates, etc) représentant une unité fonctionnelle dans le framework Symfony.
**Avant Symfony 3** Les "bundles" étaient le moyen utilisé pour structurer le code d'une application. Une application pouvait donc avoir plusieurs bundles "internes", i.e. ne provenant pas d'un développeur tiers.
**Après Symfony 3** Les bundles ne sont plus considérés à titre organisationnel mais uniquement comme un moyen de diffuser le code sous forme d'unité réutilisable. Une application ne devrait donc plus qu'avoir un seul bundle interne: `AppBundle`.
---
## Structuration d'un projet (1)
### Les principaux répertoires et leur rôle
|Répertoire|Rôle|
|:-|:-|
|`app/config/`|Configuration de l'application|
|`app/Resources/`|Depuis Symfony3, répertoire contenant les vues ainsi les "assets" de l'application|
|`src/AppBundle/`|Sources de l'application|
---
## Structuration d'un projet (2)
|Répertoire|Rôle|
|:-|:-|
|`tests/`|Répertoire contenant les tests de l'application (unitaire comme fonctionnels)|
|`var/`|Répertoire contenant toutes les données "vivantes" de l'application|
|`vendor/`| Dépendances Composer
|`web/`| Répertoire des ressources publiques de l'application
---
## La console, les commandes et les générateurs (1)
### Quelques commandes (très) utiles
|Commande|Description|
|:-|:-|
|`server:run`|Exécuter l'application avec le serveur HTTP PHP|
|`security:check`|Vérifier que les dépendances du projet ne comportent pas de vulnérabilités connues|
|`debug:container`|Retrouver le mapping services <-> classe PHP|
|`router:match`|Vérifier quelle controleur/action sera utilisée pour un chemin donné|
---
## La console, les commandes et les générateurs (2)
### Les générateurs par défaut
|Commande|Description|
|:-|:-|
|`generate:bundle`|Créer un nouveau bundle|
|`generate:controller`|Créer un nouveau contrôleur|
|`generate:command`|Créer une nouvelle commande|
|`doctrine:generate:entity`|Créer une nouvelle entité Doctrine|
---
# Le routage et les contrôleurs
## Création d'un nouveau contrôleur
## Déclaration des contrôleurs
## Actions, routes et verbes HTTP avec les annotations
---
## Création d'un nouveau contrôleur
```bash
bin/console generate:controller
```
---
## Déclaration des contrôleurs
---
## Actions, routes et verbes HTTP avec les annotations (1)
### Action annotée basique
```php
class DemoController extends Controller
{
/**
* @Route("/demo")
*/
public function demoAction()
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (2)
### Action avec méthodes HTTP contraintes
```php
class DemoController extends Controller
{
/**
* @Route(
* "/demo",
* methods = { "POST", "PUT" }
* )
*/
public function demoAction()
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (3)
### Action nommée
```php
class DemoController extends Controller
{
/**
* @Route("/demo-named", name="my_demo_action")
*/
public function demoAction()
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (4)
### Action avec paramètres
```php
class DemoController extends Controller
{
/**
* @Route("/demo/{myParam}")
*/
public function demoAction($myParam)
{
// ...
}
/**
* @Route("/demo/{paramWithDefaultVal}")
*/
public function demoAction($paramWithDefaultVal = 1)
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (5)
### Action avec paramètres contraints
```php
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{myParam}",
* requirements = { "myParam"="^\d+$" }
* )
*/
public function demoAction($myParam)
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (6)
### Action avec paramètres contraints
```php
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{myParam}",
* requirements = { "myParam"="^\d+$" }
* )
*/
public function demoAction($myParam)
{
// ...
}
}
```
---
## Actions, routes et verbes HTTP avec les annotations (7)
### Paramètres spéciaux
|Paramètre|Description|
|:-|:-|
|`_locale`| La "locale" utilisé par l'application pour la requête en cours |
|`_format`| Le "format" utilisé par l'application pour la requête en cours |
|`_controller`| L'identifiant du contrôleur et son action utilisés pour traiter la requête en cours|
---
## Actions, routes et verbes HTTP avec les annotations (8)
### Générer une URL pour une route nommée
```php
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$this->generateUrl(
'demo', // Nom de la route
['myParam' => 'my-value'], // Paramètres à injecter
// L'URL générée doit elle être absolue ou non ?
UrlGeneratorInterface::ABSOLUTE_URL
);
```
---
# Services
## Utilisation des services
## Création de nouveaux services
---
## Utilisation des services (1)
### Lister les services existants
```bash
bin/console debug:container
```
---
## Utilisation des services (2)
### Utiliser un service (méthode classique)
```php
class DemoController extends Controller
{
/**
* @Route("/demo")
*/
public function demoAction()
{
$logger = $this->get('logger');
$logger->info('Hello world !');
}
}
```
---
## Utilisation des services (3)
### Utiliser un service (Via le "type hint")
```php
use Psr\Log\LoggerInterface;
class DemoController extends Controller
{
/**
* @Route("/demo")
*/
public function demoAction(LoggerInterface $logger)
{
$logger->info('Hello world !');
}
}
```
**Tip** Pour identifier les différentes interfaces disponibles, utilisez la commande `bin/console debug:autowiring`
---
## Création d'un service (1)
### Création de la classe PHP
```php
namespace AppBundle\Service;
class MyService {
public function doNothing() {}
}
```
**Tip** On peut vérifier que le service est bien détecté par l'application avec la commande `bin/console debug:autowiring`
---
## Création d'un service (2)
### Utilisation du service dans un contrôleur
```php
use AppBundle\Service\MyService;
class DemoController extends Controller
{
/**
* @Route("/demo")
*/
public function demoAction(MyService $my)
{
$my->doNothing();
}
}
```
---
## Création d'un service (3)
### Déclaration de dépendances inter-services
```php
namespace AppBundle\Service;
use Psr\Log\LoggerInterface;
class MyService {
private $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function log($message) {
$this->logger->info($message);
}
}
```
---
## Création d'un service (4)
### Déclaration de dépendances vers des valeurs de configuration
```php
namespace AppBundle\Service;
class MyService {
private $myCustomParameter;
public function __construct($myCustomParameter) {
$this->myCustomParameter = $myCustomParameter;
}
}
```
---
## Création d'un service (5)
### Déclaration explicite de la dépendance
Dans `app/config/services.yml`, déclarer la dépendance explicitement:
```yaml
services:
AppBundle\Service\MyService:
arguments:
$myCustomParameter: 'test'
```
---
# Authentification et autorisation
## Le fichier `app/config/security.yml`
## Firewalls, providers et encoders
## Gestion des rôles et contrôle des accès
## Méthode d'authentification personnalisée
---
## Le fichier `app/config/security.yml`
### Authentifications et autorisations
---
## Firewalls (1)
### Déclarer une nouvelle méthode d'authentification dans un firewall
```yaml
security:
firewalls:
main:
anonymous: ~
http_basic: ~
```
---
## Firewalls (2)
### Multiple firewalls
```yaml
security:
firewalls:
authenticated_area:
pattern: ^/secured
http_basic: ~
public_area:
anonymous: ~
```
---
## Providers (1)
### Utilisation du provider `memory`
```yaml
security:
providers:
in_memory:
memory:
users:
bob:
password: bob
roles: 'ROLE_USER'
admin:
password: 123456
roles: 'ROLE_ADMIN'
```
---
## Providers (2)
### Déclaration de l'`encoder` pour la gestion des mots de passe
```yaml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
```
---
## Providers (3)
### Utilisation d'un `encoder` apportant un meilleur niveau de sécurité
```yaml
security:
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12
```
#### Calcul de l'empreinte d'un mot de passe
```bash
bin/console security:encode-password
```
---
## Gestion des rôles et contrôle des accès (1)
## Définition de rôles
```yaml
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
```
---
## Gestion des rôles et contrôle des accès (2)
## Restriction d'accès par adresse IP
```yaml
security:
access_control:
- path: ^/local-api
roles: ROLE_API_USER
ips: [127.0.0.1]
```
---
## Gestion des rôles et contrôle des accès (3)
## Définition de règles d'accès pour les actions
```php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
class DemoController extends Controller
{
/**
* @Route("/demo")
* @Security("has_role('ROLE_ADMIN')")
*/
public function demoAction()
{
// ...
}
}
```
---
## Méthode d'authentification personnalisée
### Exercice
Implémenter une classe `Guard` pour créer un firewall pour un serveur d'authentification "passwordless" basé sur JWT.
**Ressources**
- Tutoriel Symfony3 sur l'utilisation de la classe Guard https://symfony.com/doc/3.4/security/guard_authentication.html
- Instance de démonstration du serveur d'authentification: https://forge.cadoles.com/wpetit/ciku
- Sources du serveur d'authentification: https://forge.cadoles.com/wpetit/ciku
---
# Les vues et le moteur de templating Twig
## Organisation des vues
## Syntaxe Twig
## Héritage et composition
## Étendre Twig
---
## Organisation des vues dans le projet
---
## Syntaxe Twig
```twig
{{ myVar }}
{% for i in items %}
- {{ i.text }}
{% endfor %}
```
---
## Héritage et composition (1)
### Héritage
```twig
// parent.html.twig
{% block body %}
Default content
{% endblock %}
```
```twig
// child.html.twig
{% extends 'parent.html.twig' %}
{% block body %}
Child content
{% endblock %}
```
---
## Héritage et composition (21)
### Composition
```twig
// layout.html.twig
{% for i in items %}
{{ include('_partial.html.twig', { 'item': i }) }}
{% endfor %}
```
```twig
// _partial.html.twig
Item #{{ item.id }}
```
---
## Étendre Twig (1)
### Créer son extension
```php
// src/AppBundle/Twig/AppExtension.php
namespace AppBundle\Twig;
class AppExtension extends \Twig_Extension
{
public function getFilters()
{
return [
new \Twig_SimpleFilter('pad', [$this, 'pad']),
];
}
public function pad(
$input,
$length,
$pattern = ' ',
$type = STR_PAD_LEFT
) {
return str_pad($input, $length, $pattern, $type);
}
}
```
---
## Étendre Twig (2)
### Enregistrer l'extension comme service
```yaml
// app/config/services.yml
services:
AppBundle\Twig\AppExtension:
tags: [ twig.extension ]
```
---
## Étendre Twig (3)
### Utiliser le filtre
```twig
{{ "1" | pad(10, "0") }}
```
---
# L'ORM Doctrine et le modèle de données
## Configuration
## Migration du schéma
## Entités et dépôts
## Les évènements
---
## Configuration
---
## Migration du schéma (1)
### En développement
```bash
bin/console doctrine:schema:update --force
```
---
## Migration du schéma (2)
### En production
1. Récupération du schéma de la base en production en version `[origine]`
2. Création d'un script de migration SQL en fonction des nouvelles entités de la version `[cible]`
```bash
mkdir -p migrations
bin/console doctrine:schema:update \
--dump-sql > migration-[origine]-[cible].sql
```
3. Vérification/modification manuelle du script de migration
4. Passage du script de migration validé en production
---
## Entités et dépôts (1)
### Génération d'entités
```bash
bin/console doctrine:generate:entity
```
---
## Entités et dépôts (2)
### La classe de l'entité
```php
/**
* Post
* @ORM\Table(name="post")
* @ORM\Entity(
* repositoryClass="AppBundle\Repository\PostRepository"
* )
*/
class Post
{
/**
* @var int
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
* @ORM\Column(name="Content", type="text", nullable=true)
*/
private $content;
// [...]
}
```
---
## Entités et dépôts (3)
### Les annotations de classe
|Annotation|Paramètres (non exhaustifs)|Description|
|:-|:-|:-|
|`@ORM\Table`|`name`: nom de la table| Description de la table associée à la base de données|
|`@ORM\Entity`|`repositoryClass`: identifiant de la classe `EntityRepository` associée à cette entité| Métadonnées de contexte liées à l'entité|
---
## Entités et dépôts (4)
### Les annotations d'attributs
|Annotation|Paramètres (non exhaustifs)|Description|
|:-|:-|:-|
|`@ORM\Column`|`name`: nom de la colone, `type`: type de la colone, `nullable`| Description de la colonne dans la base de donnée associée à l'attribut|
|`@ORM\Id`|| Déclare l'attribut comme une clé primaire dans la table|
|`@ORM\GeneratedValue`|`strategy`: stratégie de génération de la valeur de l'attribut | Déclare l'attribut comme généré automatiquement par la base de donnée |
---
## Entités et dépôts (5)
### Accéder aux gestionnaires d'entités dans un contrôleur
```php
use Doctrine\Common\Persistence\ObjectManager;
class DemoController extends Controller
{
/**
* @Route("/demo")
*/
public function demoAction(ObjectManager $em)
{
// Faire quelque chose avec l'entité manager
}
}
```
---
## Entités et dépôts (6)
### Enregistrer un nouvelle entité dans la base
```php
use AppBundle\Entity\Post;
// [...]
// La variable $em est récupéré en tant que service
// On créait une instance de notre entité
$post = new \Post();
// On modifie les valeurs des attributs de notre instance
$post->setContent("hello world");
// On référence notre instance comme nouvelle entité à persister
$em->persist($post);
// On fait appliquer à l'ORM les modifications de l'"unit of work"
$em->flush();
```
---
## Entités et dépôts (7)
### Modifier une entité existante
```php
$repository = $em->getRepository(Post::class);
// On récupère une instance de notre entité à
// partir de son identifiant depuis la base de données
$post = $repository->find($id);
// On modifie les valeurs des attributs de notre instance
$post->setContent("foo bar");
// On fait appliquer à l'ORM les modifications de l'"unit of work"
$em->flush();
```
---
## Entités et dépôts (8)
### Supprimer une entité
```php
$repository = $em->getRepository(Post::class);
// On récupère une instance de notre entité à partir
// de son identifiant depuis la base de données
$post = $repository->find($id);
$em->remove($post);
// On fait appliquer à l'ORM les modifications de l'"unit of work"
$em->flush();
```
---
## Entités et dépôts (9)
### Les relations: exemple `one to many`
```php
use Doctrine\Common\Collections\ArrayCollection;
class Post
{
/**
* @var Doctrine\Common\Collections\ArrayCollection
* @ORM\OneToMany(targetEntity="Comment", mappedBy="post")
*/
private $comments;
}
```
---
### Les relations: `one to many`
```php
class Comment
{
/**
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
private $post;
}
```
---
## Les évènements Doctrine et Symfony (1)
### Création du `Listener`
```php
// src/AppBundle/EventListener/SearchIndexer.php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Post;
class PostUpdateNotifier
{
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Post) {
return;
}
// Faire quelque chose avec le Post...
}
}
```
---
## Les évènements Doctrine et Symfony (2)
### Référencer le service
```yaml
// app/config/services.yml
services:
AppBundle\EventListener\PostCreationNotifier:
tags:
- { name: doctrine.event_listener, event: postPersist }
```
---
# Les formulaires
## Création et traitement de formulaires
## Validation des données
## Les évènements
---
## Création et traitement de formulaires (1)
### Générer les formulaires CRUD pour une entité
```bash
bin/console doctrine:generate:crud
```
---
### Exercice proposé
Créer une micro application de type "TodoList". L'application devra comprendre une entité `Task` avec les attributs suivants:
- Un status: à faire | en cours | fermé
- Un texte de description
- Un label
La mise à jour de l'état d'une tâche devra automatiquement déclencher l'envoi d'un courriel à une adresse donnée (fixe, paramétrée dans le fichier `parameters.yml` de l'application).
**Ressources**
- https://symfony.com/doc/3.4/email.html
- http://symfony.com/doc/3.4/doctrine/event_listeners_subscribers.html
---
## Création et traitement de formulaires (2)
### La classe `AbstractType`
### Construction des champs
### Définition des options
### Utilisation du formulaire dans le contrôleur
### Rendu du formulaire dans un template Twig
---
## Création et traitement de formulaires (3)
### Formulaires en tant que services
#### Déclararation des dépendances
#### Déclarations du services
```yaml
// app/config/services.yml
services:
App\Form\PostType:
tags: [form.type]
```
---
## Validation des données (1)
### Installer le composant `validator`
```bash
composer require validator
```
### Configurer le composant
```yaml
# app/config/config.yml
framework:
validation: { enable_annotations: true }
```
---
## Validation des données (2)
### Les annotations `Àssert`
```php
class MyEntity
{
/**
* @Assert\NotBlank()
*/
public $name;
}
```
Les différentes validations pré-existantes: `NotBlank`, `Blank`, `NotNull`, `IsNull`, `isTrue`, `IsFalse`, `Email`, `Url`...
Voir http://symfony.com/doc/3.4/validation.html#supported-constraints
---
## Validation des données (3)
### Utilisation basique
```php
// On récupère le service "validator"
// depuis le conteneur de services
$validator = $this->get('validator');
$errors = $validator->validate($myObject);
if ( count($errors) > 0 ) {
// Traiter les erreurs
}
```
---
## Validation des données (4)
### Utilisation avec les formulaires
---
# Mise en production
## Gestion des environnements
## Cache applicatif
---
## Gestion des environnements
---
# Licence
## CC BY-NC-SA 3.0 FR
[Creative Commons - Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/)