Symfony – Outils Symfony – Contrôleurs



Symfony – Outils Symfony – Contrôleurs

0 0


AmiSymfonyPresentation


On Github Sorendil / AmiSymfonyPresentation

Symfony

avouez... vous regrettez déjà de ne pas l'avoir connu plus tôt.

Une présentation par Anthony Rossi / AMI Software Montpellier, le soleil, tout ça...

Symfony 3, qu'est-ce que c'est ?

Un framework ?

  • Evolution facile
  • Code épuré
  • Boite à outils
  • Automatisation des tâches récurrentes
  • Testé industriellement par plein de lutins
  • Une méthodologie
  • Composants (REDISTRIBUABLE & AUTONOME)

Un des buts principaux d'un framework est de garder le code organisé et permettre à l'application d'évoluer facilement au fil du temps en évitant le mélange des appels de BDD, HTML et autres codes de PHP dans le même script.

Boite à outils : On peut comparer un framework à un ensemble de briques. (= composant).

Un framework est au développeur ce que la boite à outils est au bricoleur.

Automatisation des tâches récurrentes : Pour symfony par exemple, il y a un composant Routing, Validation, etc.

Méthodologie: On développe à l'aide d'un langage. On apprend une syntaxe, des fonctions, des mots-clés... Toute une grammaireC'est là que le framework intervient ! Il énonce des conventions d’écriture et d’organisation destinées à rendre plus efficace, en homogénéisant et clarifiant. Il s’inspire des bonnes pratiques déjà existantes, notamment en termes de style. Enfin, il structure et favorise la discipline du code produit, et son indépendance à l’égard de toute solution logicielle ou matérielle.

Symfony : Principes fondamentaux

Framework PHP !

En tant que dev, lorsque l'on développe une application Symfony, notre réponsabilité est d'écrire un code qui mappe la requête utilisateur (http....) à une ressource associée (la page d'Accueil, par exemple.)

Le code à exécuter (point d'entrée) est une méthode, appelée "Action" d'une classe, appelée "Contrôleur"

Routing qui redirige vers la bonne méthode

Le dev n'a plus qu'à créer la response.

Outils Symfony

Maintenant vous savez que le but de chaque app web est d'interpréter chaque requête afin de créer une réponse appropriée.

Quand une application s'élargit, il devient de plus en plus difficile de maintenant un code organisé et maintenable.

Systématiquement, les mêmes tâches complexes se répètent encore et encore : persistence de données dans la base, rendu et réutilisation des templates, gestion des soumissions de formulaires, envoi d'emails, validation des données utilisateur, gestion de la sécurité...

Félicitations ! Symfony est là pour vous.

Outils standalone: Symfony Components

  • HttpFoundation
  • Routing
  • Form
  • Validator
  • Templating
  • Security
  • Translation

Du coup, c'est quoi Symfony ? Symfony est avant tout une collection de plus de 20 librairies indépendants qui peuvent être utilisées dans n'importe quel projet PHP. Ces librairies, appelées "Composants Symfony".

HttpFoundation: Contient les classes "Request" et "Response" ainsi que d'autres classes utiles pour la gestion des sessions ou de l'upload des fichiers.

Routing: Système de routing puissant et rapide qui permet de mapper une URI spécifique (ex: /capitalized-documents) à des informations concernant la façon dont la requête doit être gérée (ex, exécuter cgetCapitalizedDocumentsAction()).

FORM: Le composant form est un outil qui aide à résoudre le problème de permettre les utilisateurs à interragir avec les données et modifier les données dans l'application. Bien que, traditionnellement ce fut géré via les formulaires HTML, le composant se concentre sur le traitement de la données vers et depuis le client et l'application, que la donnée soit récupérée via un formulaire HTML ou via une API.

Validator: Un système pour créer des règles de validation pour les données afin de les valider.

Templating: Une boîte à outils pour le rendu des templates, qui gère les héritages et qui effectue d'autres tâches communes.

Security: Une puissante librairie pour gérer tous types de sécurité dans une application.

Translation: Un composant pour traduire des chaînes de caractères.

Conclusion: Chacun de ces composants est découplé et peut être utilisé dans n'importe quel projet PHP, avec ou sans le framework Symfony.

Symfony Framework

2 tâches :

  • Fourni une sélection de librairies
  • Fourni une configuration solide et attache tous les composants ensemble.

Du coup, c'est quoi Symfony Framework ? Symfony Framework est une librairie PHP qui rempli deux missions :

  • Fournir une selection de librairies : (Symfony Components) et 3rd-party components (ex: Swift Mailer)
  • Fourni une configuration solide et une librairie "colle" qui attache toutes les librairies ensemble. Par exemple, intégrer tous les composants un à un demande beaucoup plus de boulot qu'en utilisant le framework directement (gestion des caches, etc.)
  • En résumé, le but du framework est d'intégrer plusieurs outils indépendants afin de fournir une expérience cohérente pour le développeur. Même si le framework lui-même est un bundle Symfony (c'est à dire un plugin) qui peut être configuré ou remplacé complètement.
  • Symfony fournit une puissante compilation d'outils pour rapidement développer des applications web sans imposer l'utilisation de certains composants. Les utilisateurs normaux peuvent rapidement commencer des développement en suivant le squelette conseillé. Pour les utilisateurs avancés, il n'y a pas de limites.

Environnements

  • Environnements de base :
    • Prod
    • Dev
    • Test
  • Différents fichiers de configuration :
    • app/config/config_prod.yml
    • app/config/config_dev.yml
    • app/config/config_test.yml
  • Différents points d'entrée :
    • web/app.php
    • web/app_dev.php

Chaque application est une combinaison de code et de configurations qui indique comment l'application se comporte.

Par exemple, le niveau d'erreur change de prod à dev.

Dans Symfony, l'idée des environnements est l'idée que le même code peut être lancé selon des configurations différentes.

Par exemple, l'environnement dev peut utiliser une configuration rendant le développement plus facile tandis que l'environnement de prod permettra d'optimiser l'application pour la rapidité.

Contrôleurs

Généralités

  • Récupère une requête HTTP (objet Request) et retourne une réponse HTTP (objet Response).
<?php
use Symfony\Component\HttpFoundation\Response;

public function helloAction()
{
    return new Response('Hello world!');
}

Un contrôleur est une fonction appelable qui récupère l'information de la requête HTTP afin de créer et retourner une réponse HTTP (grâce à l'objet Response).

La réponse peut être une page HTML, un XML, du Json, une image, une redirection, un poney, etc.

[Donner des exemples : récupération d'un article, création d'une source, ...]

Cycle de vie

  • Chaque requête exécute un fichier du contrôleur frontal (app.php, app_dev.php)
  • Le front contrôleur initialize le kernel Symfony et passe un objet Request à gérer.
  • Symfony core demande au composant router d'inspecter la requête.
  • Le router match l'URL à une route spécifique et retourne les informations sur la route.
  • Le contrôleur retourné par le router est exécuté, qui crée et retourne l'objet Response approprié.
  • Les headers HTTP et le contenu de la Response sont retournés au client.
namespace NinjaFactory\GameBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class GameController extends Controller
{
    public function indexAction(Request $request)
    {
        $games = $this->doctrine->getRepository(Game::class)->findAll();

        return $this->render('game/index.html.twig', array(
            'games' => $games,
        ));
    }
}
                

Routing

  • URLs sexy (index.php?game_slug=halo devient /game/halo)
  • Flexibilité (changement facile des routes)
  • Routes complexes (expressions régulières, etc.)
  • Génération facile
  • Debug inclu

Création de routes

Formats de création
  • PHP
  • YAML
  • XML
  • Annotation (à l'intérieur des contrôleurs)

Création de routes

En YAML...
game_show:
    path:      /games/{slug}
    defaults:  { _controller: NinjaFactory:GameBundle:Game:show }
    methods:  [GET]
article_list:
    path:      /articles/{page}
    defaults:  { _controller: NinjaFactory:BlogBundle:Aticle:list, page: 1 }
    requirements:
        page:  \d+
    methods:  [GET, HEAD]

Génération d'URLs

En PHP
$uri = $this->get('router')->generate('game_show', array(
    'slug' => 'halo-5'
));
// /games/halo-5
En TWIG
<a href="{{ path('game_show', {'slug': 'halo-5'}) }}">
    See Halo 5 page
</a>

Templating : Twig

  • Code clair et lisible
  • Héritage de templates et de layouts
  • Tags et helpers
  • Pleins d'autres trucs en fait

Comme vous le savez, le contrôleur est responsable de la gestion de chaque requête qui arrive. En réalité, le contrôleur délégue presque tout le travail afin que le code puisse être réutilisé et testé.

Quand un contrôleur doit générer du HTML et d'autres assets, il délègue au moteur de template.

Introduction TWIG

{# app/Resources/views/game/list.html.twig #}
{% extends 'layout.html.twig' %}

{% block body %}
    <h1>Last games created<h1>

    {% for game in games %}
        {{ include('game/game_list.html.twig', { 'game': game }) }}
    {% endfor %}
{% endblock %}

Les bundles

  • Plugin-like
  • Tout est bundle
  • Un bundle est un ensemble structuré de fichiers...
  • ...de différents types

Plugin-like: Un bundle ressemble à un plugin, un composant.

Tout est bundle: Le coeur de Symfony ainsi que le code que vous écrivez fait partie d'un bundle.

Les bundles sont la ressource principale de Symfony.

Cela permet d'utiliser des fonctionnalités pré-construites dans des bundles externes.

Un bundle est un ensemble structuré de fichiers: Un bundle est un répertoire contenant une collection de fichiers qui implémentent une fonctionnalité.

Par exemple, on pourrait avoir un BlogBundle, ForumBundle, UserBundle, etc.

...de différents types: Fichiers PHP, Templates, Stylesheets, JS, tests, etc.

Donner exemple d'héritage de bundle externe (comme avec FosUser)

Activation des bundles

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\SecurityBundle\SecurityBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        new Symfony\Bundle\MonologBundle\MonologBundle(),
        new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
        new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
        new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
        new AppBundle\AppBundle(),
    );

    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
    }

    return $bundles;
}

Expliquer que les bundles dépendent de l'environnement.

Donner exemple d'utilisation de bundle (par exemple bundle payant)

Structure de fichiers d'un bundle

Dossiers de base

  • Controller/Contient les... contrôleurs \o/
  • DependencyInjection/Détient certaines classes d'extension du Dependency Injection : importation des configurations des services, enregistrement des passes du compilateur ou autre.
  • Entity/Contient les entités
  • Form/Contient les formulaires
  • Resource/config/Contient la configuration (routing.yml, services.yml, ...)
  • Resource/views/Contient les templates organisés, si possible, par nom de contrôleur.
  • Resource/public/Contient les assets web (images, style, ...). Ce répertoire sera copié dans /web via la commande assets:install.
  • Tests/Une idée ?

La structure d'un bundle est simple et flexible.

Par défaut, un bundle suit des conventions afin de garder le code cohérent entre les bundles...

Mais ce n'est pas obligatoire, on peut créer des dossiers où on veut, les fichiers auront pour namespace l'arborescence du fichier dans le bundle.

Resource/public/: Copié par lien symbolique ou non.

L'ORM : Doctrine

  • Persistance et lecture d'information. Tâche commune et redondance à (presque) chaque application.
  • ORM: Couche d'abstraction
  • ORM supportés par Symfony : Doctrine et Propel
  • Doctrine ORM intégré par défaut dans Symfony Standard Edition.

Une tâche très commune et redondante pour chaque application est de pouvoir persister et lire l'information de/vers une Base de données.

ORM: Un ORM (Object-relational mapping, ou Mapping objet-relationnel en français) est une couche d'abstraction de la base de données visant à créer l'illusion d'une base de données orientée objet à partir d'une base de données relationnelle. L'ORM définit des correspondances entre la base de données et les objets du langage en question, les objets PHP pour notre cas.

Dans cette partie, on apprendra la philosophie de base derrière Doctrine.

Création d'une entité

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

class Product
{
    private $name;
    private $price;
    private $description;

    // Getters and Setters here (can be auto generated by doctrine command)
}

Supposez qu'on crée une application où des produits ont besoin d'être affichés.

Sans penser à Doctrine ni aux BDD, on souhaiterait avoir un objet Product qui représente ces produits.

La classe affichée est appelée "Entité"

Pour l'instant, notre classe n'est qu'une classe.

Ajout de l'information de mapping

Doctrine allows you to work with databases in a much more interesting way than just fetching rows of scalar data into an array. Instead, Doctrine allows you to fetch entire objects out of the database, and to persist entire objects to the database. For Doctrine to be able to do this, you must map your database tables to specific PHP classes, and the columns on those tables must be mapped to specific properties on their corresponding PHP classes.

Ajout de l'information de mapping

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
class Product
{
    /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @ORM\Column(type="string", length=100)
    */
    private $name;

    /**
    * @ORM\Column(type="decimal", scale=2)
    */
    private $price;

    /**
    * @ORM\Column(type="text")
    */
    private $description;

    // Getters and Setters here
}

Enregistrement d'objet dans la base

// src/AppBundle/Controller/DefaultController.php

// ...
use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

// ...
public function createAction()
{
    $product = new Product();
    $product->setName('Keyboard');
    $product->setPrice(19.99);
    $product->setDescription('Ergonomic and stylish!');

    $em = $this->getDoctrine()->getManager();

    // tells Doctrine you want to (eventually) save the Product (no queries yet)
    $em->persist($product);

    // actually executes the queries (i.e. the INSERT query)
    $em->flush();
    // After the flush, product has an ID ($product->getId())

    return new Response('Saved new product with id '.$product->getId());
}

Explications

La création et la modification fonctionnent de la même manière, permettant de regrouper les parties du code concernant l'écriture des objets.

Récupération d'objets dans la base

public function showAction($productId)
{
    $product = $this->getDoctrine()
                    ->getRepository('AppBundle:Product')
                    ->find($productId);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$productId
        );
    }

    // ... do something, like pass the $product object into a template
}

Autres méthodes de récupération

// query for a single product by its primary key (usually "id")
$product = $repository->find($productId);

// dynamic method names to find a single product based on a column value
$product = $repository->findOneById($productId);
$product = $repository->findOneByName('Keyboard');

// dynamic method names to find a group of products based on a column value
$products = $repository->findByPrice(19.99);

// find *all* products
$products = $repository->findAll();

// query for a single product matching the given name and price
$product = $repository->findOneBy(
    array('name' => 'Keyboard', 'price' => 19.99)
);

// query for multiple products matching the given name, ordered by price
$products = $repository->findBy(
    array('name' => 'Keyboard'),
    array('price' => 'ASC')
);

Mise à jour d'objet dans la base

public function updateAction($productId)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository('AppBundle:Product')->find($productId);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$productId
        );
    }

    $product->setName('New product name!');

    $em->flush();

    return $this->redirectToRoute('homepage');
}

Explications

Ici, pas de persist, puisque le persist annonce juste à Doctrine de "watch" l'objet.

Suppression d'objet dans la base

$em->remove($product);
$em->flush();

Explications

Ici, pas de persist, puisque le persist annonce juste à Doctrine de "watch" l'objet.

Requêtage d'objets

Avec DQL
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p
     FROM AppBundle:Product p
     WHERE p.price > :price
     ORDER BY p.price ASC'
)->setParameter('price', '19.99');

$products = $query->getResult(); // Return array of Product objects

Nous avons déjà vu comment effectuer des requêtes simples à l'aide des méthodes de bases, mais il est bien sûr possible de créer ses propres requêtes plus complexes.

Si vous êtes habitué au SQL, alors le DQl vous semblera naturel. La grosse différence est que vous avez besoin de penser "objet" au lieu de rows de la BDD. Par exemple, vous sélectionnez les données de "AppBundle:Product" et pas du nom de la table.

Requêtage d'objets

Avec le QueryBuilder doctrine
$em = $this->getDoctrine()->getManager();

// createQueryBuilder automatically selects FROM AppBundle:Product
// and aliases it to "p"
$query = $repository->createQueryBuilder('p')
                    ->where('p.price > :price')
                    ->setParameter('price', '19.99')
                    ->orderBy('p.price', 'ASC')
                    ->getQuery();

$products = $query->getResult();
// to get just one result:
// $product = $query->setMaxResults(1)->getOneOrNullResult();

La requête effectue la même chose qu'avec l'exemple en DQL précédent.

Contrairement au SQL, QueryBuilder est une API pour construire des requêtes. C'est donc plus facile pour construire des requêtes dynamiques comme itérer sur des paramètres ou des filtres.

Puisqu'il construit la requête à notre place, aucune adaptation de code sera nécessaire pour les requêtes si on passe de MySQL à MongoDB, par ex.

Intégration de doctrine avec profile Symfony

Quand on affiche une page en mode dev, le profiler Symfony est câblé avec Doctrine ce qui nous permet de voir combien de requêtes ont été effectuées.

Si la page affiche beaucoup de requêtes, pour une seule page, il est peut-être temps de regarder si il n'y a pas de problèmes d'optimisations.

Relations et associations

// src/AppBundle/Entity/Category.php

// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
    // ...

    /**
    * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
    */
    private $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}

Supposez que notre objet produit appartient maintenant a exactement une catégorie. Dans ce cas, on voudrait un objet "Category" qui soit relié à notre objet "Product".

// src/AppBundle/Entity/Product.php

// ...
class Product
{
    // ...

    /**
    * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
    * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
    */
    private $category;
}

Expliquer les annotations OneToMany et ManyToOne

[L'annotation JoinColumn sera expliqué dans la page suivante.]

Ignore the Doctrine metadata for a moment. You now have two classes - Category and Product with a natural one-to-many relationship. The Category class holds an array of Product objects and the Product object can hold one Category object. In other words - you've built your classes in a way that makes sense for your needs. The fact that the data needs to be persisted to a database is always secondary. Now, look at the metadata above the $category property on the Product class. The information here tells Doctrine that the related class is Category and that it should store the id of the category record on a category_id field that lives on the product table. In other words, the related Category object will be stored on the $category property, but behind the scenes, Doctrine will persist this relationship by storing the category's id value on a category_id column of the product table.

Sauvegarde des entités liées

// ...

use AppBundle\Entity\Category;
use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Computer Peripherals');

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(19.99);
        $product->setDescription('Ergonomic and stylish!');

        // relate this product to the category
        $product->setCategory($category);

        $em = $this->getDoctrine()->getManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();

        return new Response(
            'Saved new product with id: '.$product->getId()
            .' and new category with id: '.$category->getId()
        );
    }
}

Now, a single row is added to both the category and product tables. The product.category_id column for the new product is set to whatever the id is of the new category. Doctrine manages the persistence of this relationship for you.

Récupération d'objets liés

$product = $productRepository->find($productId);
$categoryName = $product->getCategory()->getName();

Lorsque l'on a besoin de récupérer des objets via des associations, on procède de la même manière qu'avant.

La récupération de la catégorie est implicite grâce à getCategory().

Décrire le workflow en pensant aux proxies, optimisation de requêtes, ...

Deux autres façons de récupérer la catégorie liée au produit sans extra requetes :

  • Utiliser une requête personnalisée qui effectue une jointure.
  • Utiliser une annotation doctrine sur l'entité Product pour dire de TOUJOURS récupérer les catégories.

Lifecycle Callbacks

// src/AppBundle/Entity/Product.php

/**
* @ORM\PrePersist
*/
public function setUpdatedDate()
{
    $this->updatedDate = new \DateTime();
}

Events disponibles : preRemove, postRemove, prePersist, postPersist, preUpdate, postUpdate, postLoad, loadClassMetadata, onClassMetadataNotFound, preFlush, onFlush, postFlush, onClear.

Parfois, vous voulez effectuer une action juste avant qu'une entité soit enregistrée ou supprimée.

Ces types d'actions sont des "lifecycle callbacks" puisque ce sont des méthodes de callback qui seront exécutées durant différentes étapes du cycle de vie d'une entité.

Exemple d'utilisation : création de slug automatique.

Commandes doctrine en vrac

php bin/console doctrine:database:create
php bin/console doctrine:generate:entity
php bin/console doctrine:generate:entities AppBundle/Entity/Product
php bin/console doctrine:schema:update --force

La validation

  • Tâche récurrente dans les applications web.
  • Tâche ingrate
Le composant Validator de Symfony
  • Basé sur la spécification JSR303 Bean Validation. (Oui, c'est une spécification Java.)
  • Déclaration des validations via annotations, YAML ou PHP.

1. Par exemple, les données issues des formulaires ont besoin d'être validées. Les données ont aussi besoin d'etre validées avant d'etre écrire en BDD (255 caractères max par ex)

Généralités

// src/AppBundle/Entity/Author.php

// ...
use Symfony\Component\Validator\Constraints as Assert;

class Author
{
    /**
    * @Assert\NotBlank()
    */
    public $name;
}

Utilisation du service de validation

// ...
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\Author;

public function authorAction()
{
    $author = new Author();

    // ... do something to the $author object

    $validator = $this->get('validator');

    /** @var \Symfony\Component\Validator\ConstraintViolationList */
    $errors = $validator->validate($author);

    if (count($errors) > 0) {
        /*
         * Uses a __toString method on the $errors variable which is a
         * ConstraintViolationList object. This gives us a nice string
         * for debugging.
         */
        $errorsString = (string) $errors;

        return new Response($errorsString);
    }

    return new Response('The author is valid! Yes!');
}

Affichage des erreurs

if (count($errors) > 0) {
    return $this->render('author/validation.html.twig', array(
        'errors' => $errors,
    ));
}
{# app/Resources/views/author/validation.html.twig #}
<h3>The author has the following errors</h3>
<ul>
{% for error in errors %}
    <li>{{ error.message }}</li>
{% endfor %}
</ul>

BON A SAVOIR : Le service validator permet de valider un objet quand on veut.

En réalité, la validation sera pour le plus souvent utilisée avec les formulaires, permettant de valider que la donnée renseignée par l'utilisateur est correcte.

Le composant formulaire de SF utilise en interne le composant validator, lui permettant d'effectuer cette tâche.

Contraintes

Symfony inclu directement les contraintes les plus utilisées :

  • Générales: NotBlank, Blank, NotNull, IsNull, IsTrue, IsFalse, Type
  • String: Email, Length, Url, Regex, Ip, Uuid
  • Nombres: Range
  • Comparaison: EqualTo, NotEqualTo, IdenticalTo, NotIdenticalTo, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual
  • Date: Date, DateTime, Time
  • Collection: Choice, Collection, Count, UniqueEntity, Language, Locale, Country
  • Fichier: File, Image
  • Financier et autres: Bic, CardScheme, Currency, Luhn, Iban, Isbn, Issn
  • Autres: Callback, Expression, All, UserPassword, Valid

Bien sûr, nous pouvons créer nos propres validations... !

Autres fonctionnalités

  • Traduction automatique des messages d'erreur
  • Groupes de validation
  • Validation des entités en profondeur (parcourt d'entités au besoin)

Les formulaires

  • Complexe
  • Génératrice de bugs
  • HTML redondant
  • Validation redondante

Fonctionnalités

  • Validation de formulaires
  • Beaucoup de types de champs pré-existant
  • Rendu du formulaire dans un template extrèmement puissant
  • Système d'événement pour faire des formulaires dynamiques
  • Formulaires intégrés
  • Protection CRSF
  • Prototypes de formulaires \o/

Aperçu

// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class, array('widget' => 'single_text'))
            ->add('save', SubmitType::class, array('label' => 'Create Task'))
        ;
    }
}

Rendu du formulaire

{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

Décrire les fonctions TWIG

La sécurité

Gestion des droits

Chaque utilisateur a des rôles, qui est une collection de string. Par exemple, ROLE_ADMIN, ROLE_COMMENT_WRITE, ...

Sécurisation de certaines URL

# app/config/security.yml
security:
    # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }

Utile pour sécuriser des sections entières

Sécurisation des controleurs

// ...

public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');

    // Old way :
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Unable to access this page!');
    // }

    // ...
}

On peut aussi sécuriser via des annotations ( @Security("has_role('ROLE_ADMIN')"))

Ou dans les templates ({% if is_granted('ROLE_ADMIN') %})

Sécurisation d'objets individuels

Deux options

  • Voters Permet d'écrire votre propre logique business (ex: L'uilisateur peut éditer son post car il en est le créateur) afin de déterminer l'accès à l'utilisateur.
  • ACLs Permet de créer une structure où vous pouvez assigner les accès que vous voulez (EDIT, VIEW) à n'importe quel objet du système. Utile si vous avez un administrateur capable de gérer des accès personnalisés via une interface d'administration
// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $post);

Imagine you are designing a blog where users can comment on your posts. You also want a user to be able to edit their own comments, but not those of other users. Also, as the admin user, you yourself want to be able to edit all comments.

Übersetzungen Les traductions

Fonctionnalités

  • Fallback de langues : fr_CA -> fr -> en
  • Pluralisation (gestion Russe, ...)
  • Surcharge des traductions de bundles externes

Traductions définies en

  • YAML
  • XLIFF (XML)
  • PHP

Exemple basique

use Symfony\Component\HttpFoundation\Response;

public function indexAction()
{
    $translated = $this->get('translator')->trans('app.comment.action.add');

    return new Response($translated);
}
# Obtao\ForumBundle\Resources\translations\messages.fr.yml
app:                                            # Bundle name
    comment:                                    # Object name
        action:                                 # Type: action (button, ...)
            add: Nouveau commentaire
        title:                                  # Type: title (h1, h2...)
            listPage: Tous les commentaires
# ...

PROCESS:

La locale de l'user courant est récupérée via la requête ou d'une autre manière (prefs par exemple)

Un catalogue de messages traduits est chargé grâce aux ressources définies pour la locale (fr_CA).

Les messages fallback sont aussi chargés si ils n'existent pas déjà.

Il en résulte un grand dictionnaire de traductions.

Si la clé de la traduction existe, alors le traducteur retourne la trad. Sinon, elle retourne le message original.

Traductions des messages de validation

// src/AppBundle/Entity/Author.php
use Symfony\Component\Validator\Constraints as Assert;

class Author
{
    /**
    * @Assert\NotBlank(message = "app.author.name.notBlank")
    */
    public $name;
}

Vive la console !

$ php bin/console debug:translation fr AcmeDemoBundle

Gestion du pluriel

(($number % 10 == 1) && ($number % 100 != 11))
    ? 0
    : ((($number % 10 >= 2)
        && ($number % 10 <= 4)
        && (($number % 100 < 10)
        || ($number % 100 >= 20)))
            ? 1
            : 2
);
$translator->transChoice(
    'There is one apple|There are %count% apples',
    10,
    array('%count%' => 10)
);
'There is one apple|There are %count% apples'
'Il y a %count% pomme|Il y a %count% pommes'
'{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf[ There are many apples'

1. La pluralisation est un sujet sensible car les règles peuvent être complexes. Par exemple, voici la représentation mathématique des règles de pluralisation en russe.

2. Puisque les traductions dépendent du nombre, il faut le passer en paramètre.

3. En anglais, la plupart des mots ont la forme singulière lorsqu'il y a exactement un objet et un pluriel sur les autres formes (0, 2...) Donc si count vaut 1, la première chaine sera utilisée.

3. En français, les règles sont différentes. La forme singulière est utilisée lorsque l'on a 0 ou 1 objet.

4. On peut aussi spécifier les intervales que l'on souhaite lors de la traduction

Le conteneur de service

  • Une application PHP moderne est remplie d'objets.
  • Service Container !
  • Très rapide (utilise du cache)
  • Améliore l'architecture
  • Implémente l'Inversion de contrôle (Injection de dépendance)
  • Simple
  • Déclaration en YAML, PHP, XML

1. Un objet peut faciliter l'envoi de mail pendant qu'un autre permet de persister une information ou d'écrire des logs.

1. Le fait est qu'une application fait beaucoup de choses qui sont organisés dans des objets pour gérer chaque tâche.

Ce chapitre traîte d'un objet PHP spécial qui aide à instancier, organiser et récupérer plusieurs objets de l'application.

2. L'objet service container permet de standardiser et centraliser la manière dont les objets sont instanciés.

3. A apprendre et à développer

4. Permet de réutiliser et de découpler le code plus facilement

Toutes les classes symfony utilisent le service container, permettant d'étendre, configurer ou utiliser n'importe quel objet en Symfony.

C'est quoi.. ?

use AppBundle\Mailer;

$mailer = new Mailer('sendmail');
$mailer->send('ryan@example.com', ...);

La classe Mailer permet d'envoyer un mail.

Mais que fait-on si on veut utiliser cet objet autre part ? On veut pas répeter l'instanciation ni la configuration.

Et si on change le type de transport d'email ? On est obligé de changer l'instanciation sur chaque appel !

Expliquer inversion de contrôle et injection de dépendance

Création d'un service

# app/config/services.yml
services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]
class HelloController extends Controller
{
    // ...

    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('app.mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

Lors de la demande, le container construit l'objet et le retourne.

Un service n'est jamais construit jusqu'à ce qu'on l'appelle.

Une seule instance !

Passage de paramètre

# app/config/services.yml
parameters:
    app.mailer.transport:  sendmail

services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    ['%app.mailer.transport%']

Injection de service

# app/config/services.yml
services:
    app.mailer:
        # ...

    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@app.mailer']
// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;

use AppBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Injection de service

Hors constructeur

services:
    app.mailer:
        # ...
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        calls:
            - [setMailer, ['@app.mailer']]
// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;
use AppBundle\Mailer;

class NewsletterManager {
    protected $mailer;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
// ...
}

Injection de service

Récupération de l'objet Request

services:
newsletter_manager:
class:     Acme\HelloBundle\Newsletter\NewsletterManager
arguments: ["@request_stack"]
namespace Acme\HelloBundle\Newsletter;

use Symfony\Component\HttpFoundation\RequestStack;

class NewsletterManager
{
    protected $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function anyMethod()
    {
        $request = $this->requestStack->getCurrentRequest();
        // ... do something with the request
    }

    // ...
}

Service optionnel

# app/config/services.yml
services:
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@?app.mailer']
public function __construct(Mailer $mailer = null)
{
    // ...
}

Les services taggués

# app/config/services.yml
services:
    foo.twig.extension:
        class: AppBundle\Extension\FooExtension
        public: false
        tags:
            -  { name: twig.extension }
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->has('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->findDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall(
                'addTransport',
                array(new Reference($id))
            );
        }
    }
}

Autres

Bundles populaires

FOSUserBundle

  • Gestion des utilisateurs
  • Stockage Doctrine ou ODM ou Propel
  • Formulaire d'inscription
  • Mot de passe oublié
  • ...

StofDoctrineExtension‐Bundle

  • Installe et configure 11 extensions Doctrine2
  • l3pp4rd / DoctrineExtensions
/**
 * @Gedmo\Timestampable(on="create")
 * @ORM\Column(type="datetime")
 */
private $created;

/**
 * @Gedmo\Blameable(on="create")
 * @ORM\Column(type="string")
 */
private $createdBy;

Cité dans la doc officielle

♥ FOS ♥

  • FOSCommentBundle
  • FOSOAuthServerBundle
  • FOSMessageBundle
  • FOSFacebookBundle
  • FOSTwitterBundle
  • FOSJsRoutingBundle
  • FOSElastica

Bonus: Méthodologie et organisation du CSS avec SASS

Ressources