
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.


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é :
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
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,
]);
}
}
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 typeResponse
(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 typeProductFormType
- La vue du formulaire est générée avec la fonction
$form->createView()
et directement passée dans la variableform_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
.
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
.
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 tableproduct
$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).
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 %}
templates/product/...
et j'en ai profité pour ajouter un lien vers notre formulaire d'ajoutBien, 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.
null
) mais il va de soi que cette vérification doit être faite en pratiqueCré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()
.
find(...)
du managerIl 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 !