Symfony 4 : Persistence de données avec Doctrine

A l’aide de quelques lignes de commande nous allons créer notre modèle de données, gérer la persistence (la sauvegarde ) dans une table à l’aide d’un ORM bien connu dans le monde de Symfony : Doctrine

Savez vous ce qu’est un ORM? Object Relational Mapping, en gros c’est une librairie qui va faire la correspondance entre vos tables de base de données et des classes PHP. Quand vous travaillez avec un ORM, vous ne faites plus de SQL, mais vous utilisez des méthodes de la librairie, ainsi quelqu’un qui ne connait pas SQL peut très bien se débrouiller pour insérer ou sélectionner des données, mettre à jour ou effacer , bref faire du CRUD.

Personnellement je trouve qu’un ORM est pratique si on se limite à des requêtes pas trop élaborées, si vous constatez que vous avez un peu de mal à formuler des requêtes avec un ORM, il est peut être judicieux de passer à des requêtes SQL pures. Dans le cas du CRUD c’est à dire travailler avec des formulaire pour insertion ou édition, un ORM va très bien, et je vous conseille de vous familiariser avec.

Ce tutoriel est largement inspiré du tutoriel officiel https://symfony.com/doc/current/doctrine.html avec des ajouts de ma part.

Installer Doctrine pour Symfony 4

Si vous ne l’avez déjà fait voici les manipulations pour installer l’ORM Doctrine :

composer require symfony/orm-pack

Restricting packages listed in "symfony/symfony" to "4.3.*"
Using version ^1.0 for symfony/orm-pack
./composer.json has been updated
Restricting packages listed in "symfony/symfony" to "4.3.*"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class

Normalement ça devrait être déjà installé. Ensuite pour être sûr de disposer d’un outil en ligne de commande pratique pour créer un base de données, une table, une entité, il y a le makerBundle à installer :

composer require --dev symfony/maker-bundle

Vous retrouverez dans la partie require-dev du fichier composer.json , l’argument --dev signifie qu’en production, ce package ne sera pas installé, il sert uniquement en développement.

Utilisation de la ligne de commande Doctrine

Voici quelques commandes utiles pour rendre la création d’entité plus fluide (on dit streamline en anglais), création d’une entité, création de la table correspondante opération nommée migration, pour accepter une valeur par défaut proposée appuyer sur Entrée.

php bin/console make:entity

Class name of the entity to create or update (e.g. AgreeableJellybean):
 > Product
 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 <return> to stop adding fields):
 > name
 Field type (enter ? to see all types) [string]:
 > 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 <return> 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]:
 > no
 updated: src/Entity/Product.php
 Add another property? Enter the property name (or press <return> to stop adding fields):
 > 
           
  Success! 
          
 Next: When you're ready, create a migration with make:migration

Notez qu’il y a deux fichiers créés, un fichier pour l’entité dans le répertoire src/Entity/Product.php , et un fichier repository qui est src/Repository/ProductRepository.php , ce fichier renferme les méthodes pour faire des requêtes. Dans un ORM, les méthodes pour faire des requêtes suivent une logique particulière, elles sont du type findOne, findAll .

Migration sous Doctrine : créer la table correspondante à l’entité

Maintenant nous voulons créer la table correspondante en base de données, on fait ce qu’on appelle une migration. Cela se fait en deux temps, on crée un fichier PHP qui se crée dans src/Migrations, et on fait la migration.

php bin/console make:migration

Si vous regardez dans ce fichier, il y a une classe qui étend la classe AbstractMigration, et deux méthodes up() et down() . La méthode down() sert à défaire la création de la table, c’est une bonne pratique de faire une méthode qui défait, on dit rollback en anglais. Maintenant faisons la migration proprement dite, cette migration est possible que si dans le fichier .env, il y a la chaine de connexion bien remplie avec les identifiant et mot de passe, nom de la base de donnée et du host. La ligne générique est créée lors de l’installation du bundle Doctrine, pour que vous la renseigniez.

php bin/console doctrine:migrations:migrate
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 20190612091941 from 0

  ++ migrating 20190612091941

     -> 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 128.9ms, used 12M memory)

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

  ++ finished in 133.7ms
  ++ used 12M memory
  ++ 1 migrations executed
  ++ 1 sql queries

Faire un tour dans la base de données et vous verrez deux tables créée, la table Product et la table migration_versions , qui est très importante puisqu’elle contient l’historique des migrations. Vous devez toujours utiliser la migration pour créer une table afin d’avoir un projet bien carré. Programmer avec Symfony vous permet d’avoir des bonnes pratiques de développement, qualité très appréciées dans le monde de l’entreprise.

Cette notion de migrations et d’historisation de migration existe aussi dans Laravel, je ne saurai dire qui en premier a mis en oeuvre.

Les migrations que vous faites en développement doivent être répétées en environnement de production. Vous pouvez exécuter les commande manuellement, mais les outils de déploiement sans nul doute doivent intégrer ces actions dans leur pipeline.

Si vous modifiez votre entité, vous devez faire une migration de nouveau, il est important d’avoir la table en synchronisation avec l’entité. Pour ajouter un nouveau champ dans l’entité, rejouez la commande : php bin/console make:entity , php bin/console make:migration , php bin/console doctrine:migrations:migrate dans cet ordre. Pour l’ajout de propriété manuellement, jouez plutôt cette commande : php bin/console make:entity --regenerate , en y passant --overwrite , on refait tous les getter et setter.

Notez aussi que l’entité que l’on a créé Task n’est pas créé en migration, il n’en a même pas tenu compte. Ceci s’explique que Doctrine a besoin de méta données non disponible dans la classe Task.

Doctrine : persistence des données en base

Passons maintenant aux choses sérieuses ! Créons un controller pour faire l’insertion en BDD. Créer le controller avec le code suivant :

php bin/console make:controller ProductController
# src/Controller/ProductController.php
namespace App\Controller;

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

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="create_product")
     */
    public function createProduct(): Response
    {
        // you can fetch the EntityManager via $this->getDoctrine()
        // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
        $entityManager = $this->getDoctrine()->getManager();

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

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

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

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

Notez tout de suite une nouveauté PHP7, après le nom de la fonction il y a deux points. Ce qui se situe après les deux points, c’est ce qui est attendu comme type de retour de la fonction, c’est la signature de la méthode. Donc n’oubliez pas d’ajouter un use de la classe Response avant la déclaration de la classe.

use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController {
    ...

Maintenant allons sur la page /product pour faire marche ce bout de code, vous allez recevoir l’erreur suivante :

C’est parce qu’il y a une propriété supplémentaire qu’on n’a pas ajouté qui est Description. Faisons le de suite (committez vos fichiers pour voir la différence avant et après).

php bin/console make:entity

 Class name of the entity to create or update (e.g. TinyChef):
 > Product

 Your entity already exists! So let's add some new fields!

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

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

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

 updated: src/Entity/Product.php

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

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

Créez et appliquez la migration. Puis revenez sur l’url. Pour vérifier que votre requête s’est bien passée, vous pouvez requêter la base de donnée en ligne de commande avec Doctrine !

php bin/console doctrine:query:sql 'SELECT * FROM product'
/Users/poste/sites/newsymfony/vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Dumper.php:68:
array (size=2)
  0 => 
    array (size=4)
      'id' => string '1' (length=1)
      'name' => string 'Keyboard' (length=8)
      'price' => string '1999' (length=4)
      'description' => string 'Ergonomic and stylish!' (length=22)
  1 => 
    array (size=4)
      'id' => string '2' (length=1)
      'name' => string 'Keyboard' (length=8)
      'price' => string '1999' (length=4)
      'description' => string 'Ergonomic and stylish!' (length=22)

Je sais pas vous mais je trouve plutôt classe de pouvoir requêter aussi facilement en ligne de commande ! Sachez que Doctrine a une intelligence, il sait s’il faut updater ou insérer un objet en base.

Maintenant soyez curieux, et faites varier les valeurs de l’entité, en mettant une chaine ‘1999' au lieu de la valeur sans les single quote, remplacez '1999' par 'ABC'. La validation automatique a été ajoutée sur Symfony 4.3 mais forcez vous à mettre des contraintes par vous-même.

Félicitations ! Vous venez de persister des données avec succès avec Doctrine ! Doctrine est un gros chapitre en soi. Nous aborderons les différentes façons de faire des requêtes avec Doctrine dans un autre chapitre.

Pourquoi décomposer en persist() et flush()?

Reprenon le code du controller, nous avons créé un objet $product, mais plutôt que d’avoir une syntaxe $entityManager->flush($product);, nous avons précédé de persist(), pourquoi cela?

Le fait de décomposer en deux étape, permet de mettre à la file d’attente l’insertion de plusieur objets :

 $product1 = new Product();
        $product1->setName('Keyboard');
        $product1->setPrice(1999);
        $product1->setDescription('Ergonomic and stylish!');
        $entityManager->persist($product1);
 $product2 = new Product();
        $product2->setName('Mouse');
        $product2->setPrice(200);
        $product2->setDescription('Wireless mouse');
        $entityManager->persist($product2);

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

en une seule requête, l’insertion des deux objets se fait. Mais si vous ne voulez pas ce comportement, et que vous vouliez insérer rapidement, il existe la méthode persistFlush() qui va immédiatement insérer l’objet.

Retour en haut