Site non compatible avec cette vieillerie d'Internet Explorer. Pour une expérience complète, voir la liste des navigateurs supportés.
Thumbnail

 Memo Le CRUD sous Symfony 4

Ici nous verrons comment gérer un produit en base de données de A à Z à l'aide du framework Symfony dans sa version 4.


Tuto | Symfony

Auteur : Guillaume M.
Créé le : 02 Nov 2019
Dernière mise à jour : 25 Dec 2019

Basé sur la version 4.3 de Symfony.

Dans ce tuto, nous verrons comment créer une entité et comment manipuler cette entité en base de données. À noter que pour ce tuto, je me base sur une installation fraîche (full) et donc nous passerons aussi par l'étape de configuration d'une base données, de création de formulaire etc.

Quelques notions à connaître pour suivre le tuto :


  • Création d'un projet Symfony (full)
  • Les lignes de commandes sous Symfony
  • Le routing (annotations)
  • Entité, controller et template

 

Création d'un fichier .env.local


Cette étape n'est pas forcément nécessaire, on peut sans problème éditer le fichier .env qui est déjà présent. Mais le fait d'utiliser un fichier .env.local présente un avantage lorsque l'on utilise github : le fichier est tout simplement ignoré lors des commits.

Étant donné que les variables d'environnement contiennent des données sensibles (comme le mot de passe de la base par exemple) et bien cela nous assure qu'aucune de ces données sensibles ne se trouvera dans le repository en ligne.

Autre avantage, on conserve l'intégrité du fichier .env tant qu'il n'y a pas de mise en production (lors d'une mise en production, c'est le fichier .env qui doit être utilisé) et symfony utilise en priorité le fichier .env.local tant qu'il existe.

Dans un premier temps, créons un nouveau fichier .env.local à la racine du projet (là où se situe le fichier .env).

Copier-coller tout le contenu du fichier .env dans le fichier .env.local. et regardons ensuite cette ligne :

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name

Maintenant, éditons-là :

DATABASE_URL=mysql://root:@127.0.0.1:3306/tuto_symfo

Ici j'ai donc comme utilisateur root sans mot de passe, l'adresse ip de la BDD (ici local) et une base de données portant le nom tuto_symfo (créée au préalable avec phpmyadmin).

 

Création d'une entité


Dans un premier temps, créons une entité Product à l'aide de la commande :

php bin/console make:entity Product

Un fichier Product.php et un fichier ProductRepository.php ont été créés, la commande nous propose ensuite d'ajouter quelques attributs à l'entité :

Astuce : Une donnée entre crochet représente un type ou une valeur par défaut pour le champ, si on appuie directement sur la touche "Entrée", c'est ce type ou cette valeur qui seront choisis.
 created: src/Entity/Product.php
 created: src/Repository/ProductRepository.php
 
 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press  to stop adding fields):
 > name

 Field type (enter ? to see all types) [string]:
 > 

 Field length [255]:
 > 

 Can this field be null in the database (nullable) (yes/no) [no]:
 > 

 updated: src/Entity/Product.php

 Add another property? Enter the property name (or press  to stop adding fields):
 > price

 Field type (enter ? to see all types) [string]:
 > integer

 Can this field be null in the database (nullable) (yes/no) [no]:
 > 

 updated: src/Entity/Product.php

 Add another property? Enter the property name (or press  to stop adding fields):
 > 


  Success! 
 

 Next: When you're ready, create a migration with make:migration

Ici j'y ai juste ajouté un champ name et price à l'entité ce qui devrait ressembler à ceci au niveau de la class Product :

<?php 

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @ORM\Column(type="integer")
     */
    private $price;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getPrice(): ?int
    {
        return $this->price;
    }

    public function setPrice(int $price): self
    {
        $this->price = $price;

        return $this;
    }
}

Passons maintenant à la génération du fichier de migration :

php bin/console make:migration

Puis, effectuons la migration vers la base de données :

php bin/console doctrine:migrations:migrate

Ici on peut voir qu'une table product avec les champs name et price a été créée dans notre base de données :

WARNING! You are about to execute a database migration that could result in schema changes and data loss. Are you sure you wish to continue? (y/n)y
Migrating up to 20191103104447 from 0

  ++ migrating 20191103104447

     -> CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, price INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB

  ++ migrated (took 74.3ms, used 14M memory)

  ------------------------

  ++ finished in 86.3ms
  ++ used 14M memory
  ++ 1 migrations executed
  ++ 1 sql queries

On peut maintenant passer à la création d'un controller.

 

Création d'un controller


Créons un controller ProductController avec la commande qui va bien :

php bin/console make:controller ProductController

Deux fichiers ont été créés :

src/Controller/ProductController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="product")
     */
    public function index()
    {
        return $this->render('product/index.html.twig', [
            'controller_name' => 'ProductController',
        ]);
    }
}

et

 templates/product/index.html.twig
{# templates/product/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Hello ProductController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ 'C:/wamp64/www/tuto_memo/src/Controller/ProductController.php'|file_link(0) }}">src/Controller/ProductController.php</a></code></li>
        <li>Your template at <code><a href="{{ 'C:/wamp64/www/tuto_memo/templates/product/index.html.twig'|file_link(0) }}">templates/product/index.html.twig</a></code></li>
    </ul>
</div>
{% endblock %}

Maintenant que les bases sont posées, nous pouvons passer à la création d'un formulaire pour le produit.

 

Création d'un formulaire


Encore une fois nous allons utiliser une commande tout prête qui va nous permettre de générer un formulaire tout en le liant à notre entité Product, et nous l'appellerons ProductFormType :

php bin/console make:form ProductFormType

La commande nous demande ensuite le nom de l'entité pour que le formulaire puisse se contruire autour, ce sera donc pour l'entité Product :

 The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > Product
Note : Si par hasard on souhaitait construire un formulaire sans le relier à une entité, il suffirait de laisser le nom de l'entité vide

Une classe permettant la construction de ce formulaire a été créée :

src/Form/ProductFormType.php
<?php

namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProductFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('price')
            ->add('submit', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}
Note : Ici j'ai ajouté un bouton submit au builder.

Dernière étape : créer un template pour notre formulaire, nous l'appellerons product-form.html.twig

Malheureusement pas de commande magique ce coup-ci, il nous faut le créer à la main dans le dossier product des templates :

templates/product/product-form.html.twig
{% extends "base.html.twig" %}

{% block body %}
<h1>{{ form_title }}</h1>
{{ form(form_product) }}
{% endblock %}

Ce template pourra être utilisé aussi bien pour l'ajout que pour la modification, c'est pourquoi j'y ai ajouté une variable form_title qui sera initialisée dans le controller d'ajout ou de modification. Quand à la fonction form(...), elle se contente d'afficher le formulaire form_product qui sera aussi transmis par le controller.

Bien, maintenant que nous avons un formulaire prêt à l'emploi, nous allons pouvoir passer au chose sérieuse en ajoutant un produit dans notre base de données via un formulaire.

 

Ajout d'une entité en base (Create)


Ok c'est maintenant l'heure de retrourner dans notre controller ProductController et d'y ajouter une route pour afficher notre formulaire d'ajout :

<?php

namespace App\Controller;

// ...
use App\Form\ProductFormType; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; class ProductController extends AbstractController { // ... /** * @Route("/add-product", name="add_product") */ public function addProduct(Request $request): Response { $form = $this->createForm(ProductFormType::class); return $this->render("product/product-form.html.twig", [ "form_title" => "Ajouter un produit", "form_product" => $form->createView(), ]); } }

Pour l'instant rien n'est fonctionnel, nous nous contentons simplement d'afficher un formulaire de produit à l'adresse /add-product.

Quelques précisions :

  • 3 use ont été ajoutés
  • L'objet Request est la clé dans le processus d'envoi d'un formulaire, nous le verrons par la suite
  • La fonction addProduct retourne un objet de type Response (dans notre cas le rendu du template)
  • La fonction $this->createForm(...) prend en paramètre le type de formulaire que l'on souhaite généré, ici donc c'est notre formulaire de type ProductFormType
  • La vue du formulaire est générée avec la fonction $form->createView() et directement passée dans la variable form_product du template
  • Sans oublier notre variable form_title que l'on passe également à notre template

Bien, maintenant que notre formulaire s'affiche correctement, il est temps de passer au processus d'ajout dans notre controller.

Pour se faire, ajoutons ces quelques lignes dans notre fonction :

<?php

namespace App\Controller;

// ...
use App\Entity\Product;

class ProductController extends AbstractController
{
    // ...

    /**
     * @Route("/add-product", name="add_product")
     */
    public function addProduct(Request $request): Response
    {
        $product = new Product();
        $form = $this->createForm(ProductFormType::class, $product);
        $form->handleRequest($request);

        if($form->isSubmitted() && $form->isValid())
        {
            // ... do something
        }

        return $this->render("product/product-form.html.twig", [
            "form_title" => "Ajouter un produit",
            "form_product" => $form->createView(),
        ]);
    }
}

Dans un premier temps parlons de la fonction $this->createForm(...), elle renvoie un objet de type FormInterface   et prend d'abord comme premier argument le type de formulaire que l'on souhaite créer, ici donc c'est la classe ProductFormType que l'on a généré au préalable puis, pour le second argument, nous lui passons un objet vide de type Product.

Note : Ne pas oublier la directive use App\Entity\Product;

Mais pourquoi donc lui passer une entité ? Et bien tout simplement car le formulaire est capable d'hydrater l'objet lui-même, ce qui sous-entend que nous n'aurons pas à nous soucier de cela lorsque le formulaire sera soumis, nous n'aurons qu'à envoyer l'objet en base !

Expliquons maintenant la fonction $form->handleRequest(...), celle-ci prend comme unique argument un objet de type Request. Cet objet contient toutes les données possibles avec une requête http ainsi que quelques données supplémentaires liées au framework Symfony. On peut par exemple y trouver les POST, les GET, si un formulaire a été soumis ou non, si il est valide etc.

C'est donc grâce à l'objet Request que l'on peut récupérer les données du formulaire mais pas d'inquiétude... l'objet $form s'en occupe pour nous grâce à sa fonction handleRequest(...) ! En effet, ce n'est pas pour rien si nous avons passé un objet de type Product à notre formulaire, grâce à la combinaison de l'entité et de l'objet Request, le formulaire hydratera automatiquement l'objet Product avec les données contenues dans l'objet Request.

Note : En interne, la fonction handleRequest(...) hydrate l'objet $form grâce à l'objet Request, ce qui change donc son état et c'est donc ce pourquoi nous pouvons utiliser les fonction $form->isSubmitted() et $form->isValid() pour savoir si un formulaire a été envoyé.

Passons maintenant à la dernière étape : envoyer le produit en base.

Pour se faire ajoutons quelques lignes dans la condition de soumission et de validation du formulaire :

/**
 * @Route("/add-product", name="add_product")
 */
public function addProduct(Request $request): Response
{
    $product = new Product();
    $form = $this->createForm(ProductFormType::class, $product);
    $form->handleRequest($request);

    if($form->isSubmitted() && $form->isValid())
    {
        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($product);
        $entityManager->flush();
    }

    return $this->render("product/product-form.html.twig", [
        "form_title" => "Ajouter un produit",
        "form_product" => $form->createView(),
    ]);
}

Et c'est tout ! On peut maintenant ajouter des nouveaux produits dans notre base de données grâce au formulaire.

Quelques explications :

  • $entityManager contient le manager de Doctrine (Doctrine est le bundle qui s'occupe de la liaison avec la base de données), c'est grâce à lui que nous pouvons effectuer des requêtes
  • La fonction $entityManager->persist(...) signale au manager de préparer une requête pour insérer l'objet $product dans la table product
  • $entityManager->flush() exécute la requête

Ici il y a une question que l'on pourrait se poser : Comment le manager fait-il pour savoir dans quelle table ajouter l'objet puisque à aucun moment on indique le nom de la table en question ? Et bien c'est très simple, l'information se trouve dans la classe Product, juste au-dessus du nom de la classe on peut y voir ceci :

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Product
{
    // ...
}

Cela indique à Doctrine le nom de la classe contenant les informations nécessaires pour effectuer des requêtes avec un type d'objet particulier. Le repository ProductRepository est donc une classe contenant toutes les informations requises pour effectuer des requêtes avec un type d'entité particulier (ici Product donc).

Souvenez-vous : ProductRepository est une classe qui a été créée en même temps que la génération de notre entité Product.

 

Récupération d'une entité en base (Read)


Maintenant que nous pouvons ajouter des entités en base de données, passons à la récupération et l'affichage des entités.

Pour ce faire modifions notre ProductController en y ajoutant une route products :

/**
 * @Route("/products", name="products")
 */
public function products()
{
    $products = $this->getDoctrine()->getRepository(Product::class)->findAll();

    return $this->render('product/products.html.twig', [
        "products" => $products,
    ]);
}

Ici rien de bien particulier, nous récupérons nos entités en base grâce au manager de doctrine puis nous passons le tableau $products contenant toutes nos entités au template products.html.twig que voici :

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

{% block title %}Hello ProductController!{% endblock %}

{% block body %}
<h1>Lise des produits</h1>
<p><a href="{{ path('add_product') }}">Ajouter un produit</a></p>
{% for product in products %}
    <table>
        <thead>
            <th>ID</th>
            <th>Name</th>
            <th>Price</th>
        </thead>
        <tbody>
            <td>{{ product.id }}</td>
            <td>{{ product.name }}</td>
            <td>{{ product.price }}</td>
        </tbody>
    </table>
{% endfor %}
{% endblock %}
Note : Je ne l'ai pas précisé mais le template doit être crée dans le dossier templates/product/...  et j'en ai profité pour ajouter un lien vers notre formulaire d'ajout

Bien, on a maintenant un visuel complet sur nos entités mais allons un peu plus loin en créant une nouvelle route dans ProductController pour afficher un produit particulier :

/**
 * @Route("/product/{id}", name="product")
 */
public function product(int $id): Response
{
    $product = $this->getDoctrine()->getRepository(Product::class)->find($id);

    return $this->render("product/product.html.twig", [
        "product" => $product,
    ]);
}

Dans un premier temps, la route prend un argument {id} que nous passons directement à la fonction product(int $id) du controller. Cela permet ensuite de récupérer le bon produit en base toujours avec l'aide de doctrine puis nous passons ensuite l'entité au template.

Note : Ici je n'effectue pas de vérification pour savoir si l'entité a bien été trouvé (donc non null) mais il va de soi que cette vérification doit être faite en pratique

Créons le template au passage :

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

{% block title %}Hello ProductController!{% endblock %}

{% block body %}
<h1>Page produit</h1>
<p>Name : {{ product.name }}</p>
<p>Price : {{ product.price }}</p>
{% endblock %}

Ici rien de compliqué, nous affichons simplement les valeur du produit.

Profitons-en aussi pour ajouter les liens dans le template products.html.twig qui permettront d'accéder à une page produit, pour ce faire, modifions simplement la ligne contenant le nom du produit dans le tableau html :

<td><a href="{{ path('product', {'id': product.id}) }}">{{ product.name }}</a></td>

Ici nous passons en 1er argument de la fonction path(...) le nom de la route product puis en 2ème argument l'id du produit qui sera donc transmis via l'url. On peut donc maintenant accéder à une page produit en passant par la liste des produits.

Bien, passons maintenant à la mise à jour d'une entité.

 

Mise à jour d'une entité (Update)


Avant de créer l'action de modification dans le controller, ajoutons tout de suite le lien dans le tableau du template products.html.twig qui permettra d'accéder au formulaire de modification :

<table>
    <thead>
        {# ... #}
        <th>Modifier</th>
    </thead>
    <tbody>
        {# ... #}
        <td><a href="{{ path('modify_product', {'id': product.id}) }}">Modifier</a></td>
    </tbody>
</table>

Ok, passons maintenant à l'ajout de la route de modification dans ProductController :

/**
 * @Route("/modify-product/{id}", name="modify_product")
 */
public function modifyProduct(Request $request, int $id): Response
{
    $entityManager = $this->getDoctrine()->getManager();

    $product = $entityManager->getRepository(Product::class)->find($id);
    $form = $this->createForm(ProductFormType::class, $product);
    $form->handleRequest($request);

    if($form->isSubmitted() && $form->isValid())
    {
        $entityManager->flush();
    }

    return $this->render("product/product-form.html.twig", [
        "form_title" => "Modifier un produit",
        "form_product" => $form->createView(),
    ]);
}

Plusieurs choses ici :

  • Nous récupérons l'entity manager de doctrine
  • Nous nous servons de l'entity manager pour récupérer le product en base relatif à l'id passé via l'url
  • Puis, comme lors de la création du formulaire d'ajout, nous passons l'entité product à la fonction createForm(...), ce qui va permettre au formulaire d'être pré-rempli avec les valeurs de l'entité associée
  • On oublie pas la fonction $form->handleRequest(...) qui, pour rappel, se charge à la fois de changer l'état du formulaire et de fournir les données saisies dans celui-ci pour l'hydratation de l'objet
  • Pour le rendu, nous réutilisons le même formulaire que celui utilisé pour l'ajout
  • Enfin, si le formulaire a été envoyé et validé alors nous n'avons plus qu'à utiliser la fonction flush() de l'entity manager pour faire un update du produit en base !

Ici la dernière partie peut sembler "magique" car il n'y pas besoin de préciser que nous voulons une requête de mise à jour, l'entity manager sait qu'il y a eu une modification d'une entité déjà existante et donc sait qu'il doit utiliser une requête de mise à jour.

Et voilà, on peut maintenant modifier nos produits à notre guise !

Mais il reste encore une chose à faire... la suppression ! C'est parti.

 

Suppression d'une entité en base (Delete)


Cette dernière partie et sans doute la plus simple, pas besoin de templates ni de formulaire, juste une simple route avec une requête dans notre controller. Voyons ça tout de suite :

/**
 * @Route("/delete-product/{id}", name="delete_product")
 */
public function deleteProduct(int $id): Response
{
    $entityManager = $this->getDoctrine()->getManager();
    $product = $entityManager->getRepository(Product::class)->find($id);
    $entityManager->remove($product);
    $entityManager->flush();

    return $this->redirectToRoute("products");
}

Ici rien de bien compliqué, comme précédemment, on passe l'id de l'objet via l'url qui est passé à la fonction et ensuite utilisé pour trouver l'entité en base.

Une fois l'entité trouvée, on passe celle-ci à la fonction remove(...) du manager pour préparer la requête de suppression puis on exécute avec la fonction flush().

Note : N'oubliez pas de vérifier qu'une entité soit bien retourné par la fonction find(...) du manager

Il reste maintenant juste une dernière petite étape pour pouvoir supprimer un produit : l'ajout d'un lien dans notre liste de produits.

Reprenons donc le templates products.html.twig et ajoutons cette ligne au tableau :

<table>
    <thead>
        {# ... #}
        <th>Supprimer</th>
    </thead>
    <tbody>
        {# ... #}
        <td><a href="{{ path('delete_product', {'id': product.id}) }}">Supprimer</a></td>
    </tbody>
</table>

Et le tour est joué ! On peut maintenant supprimer une entité en base.

Voilà le tuto s'achève, ici pas mal de points ont été ignorés pour aller à l'essentiel, je pense notamment aux divers vérifications, aux contraintes dans les formulaires pour les validations côté back ou encore la customisation du formulaire dans le template donc n'hésitez pas à faire un tour dans la documentation de symfony pour en savoir plus !