• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 02/08/2019

Gérez vos données avec Doctrine ORM

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Créez vos premières entités Doctrine

Qu'est-ce qu'un ORM ?

Doctrine ORM implémente 2 patterns objets pour mapper un objet PHP à des éléments d'un système de persistance :

Le Data Mapper est une couche qui synchronise la donnée stockée en base avec les objets PHP. En d'autres termes :

  • il peut insérer, mettre à jour des entrées en base de données à partir de données contenues dans les propriétés d'un objet ;

  • il peut supprimer des entrées en base de données si les "entités" liées sont identifiées pour être supprimées ;

  • il "hydrate" des objets en mémoire à partir d'informations contenues en base.

L'implémentation dans le projet Doctrine de ce Data Mapper s'appelle l'Entity Manager, les entités ne sont que de simples objets PHP mappés.

Pour une raison de performance et d'intégrité, l'Entity Manager ne synchronise pas directement chaque changement avec la base de données.

L'Unit of Work est lui utilisé pour gérer l'état des différents objets hydratés par l'Entity Manager. La synchronisation en base ne s'effectue que quand on exécute la méthode "flush" et est effectuée sous forme d'une transaction qui est annulée en cas d'échec.

L'Entity Manager fait donc le lien entre les "Entités", qui sont de simples objets PHP, et la base de données :

  • à l'aide de la fonction find, il retrouve et hydrate un objet à partir d'informations retrouvées en base ;

  • à l'aide de la fonction persist, il ajoute l'objet manipulé dans l'Unit of Work ;

  • à l'aide de la fonction flush, tous les objets "marqués" pour être ajoutés, mis à jour et supprimés conduiront à l'exécution d'une transaction avec la base de données.

Comment utiliser l'Entity Manager?

Dès lors que les objets sont mappés et reconnus par l'Entity Manager, nous pouvons les manipuler. Pour mapper des entités à des tables, Doctrine est capable de comprendre les formats suivants :

  • des annotations dans les commentaires (dits "DocBlocks") des objets PHP ;

  • des fichiers XML ;

  • des fichiers YAML ;

  • des fichiers PHP de configuration.

Votre première entité Doctrine

Prenons l'exemple d'une classe Article avec les propriétés id, title, content et date :

<?php

namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;

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

    /**
    * @ORM\Column(type="string")
    */
    public $title;

    /**
    * @ORM\Column(type="text")
    */
    public $content;

    /**
    * @ORM\Column(type="datetime", name="date")
    */
    public $date;
}
  • L'annotation Entity spécifie que la classe est considérée en tant qu'entité. L'attribut le plus important est repositoryClass, qui permet de spécifier un repository spécifique pour l'entité (on y reviendra plus tard).

  • L'annotation Id identifie la clé primaire de la table.

  • L'annotation GeneratedValue délègue la responsabilité de l'unicité de l'id au système de persistance choisi.

L'annotation "Column"

L'annotation @ORM\Column permet de mapper une propriété PHP à une colonne de la base de données.

Par défaut, le nom de la colonne de la base de données sera le nom de la propriété PHP, mais il est possible de le surcharger. Si une propriété n'est pas marquée avec l'annotation, elle sera complètement ignorée.

Cette annotation a de nombreux attributs, tous documentés. Voici les plus utiles :

  • unique  qui définit que la valeur doit être unique pour la colonne donnée ;

  • nullable  qui autorise/interdit la valeur nulle ;

  • length, pour les chaînes de caractères, définit la longueur.

Les types de mapping Doctrine

Les types définis dans les annotations Doctrine ne correspondent ni aux types en PHP ni à ceux de la base de données, mais sont mappés aux deux. En voici quelques-uns :

Type Doctrine

Type PHP

Type en base de données (MySQL)

string

string

VARCHAR

integer

integer

INT

boolean

boolean

BOOLEAN

date

\DateTime

DATETIME

datetime

\DateTime

TIMESTAMP

blob

stream resource

BLOB

Créez et mettez à jour votre base de données

Doctrine est capable de créer et mettre à jour une base de données à partir des informations tirées du mapping des entités.

La première étape va être de configurer Doctrine ORM dans l'application Symfony. Pour cela, il faudra compléter le fichier  config/packages/doctrine.yaml  :

# config/packages/doctrine.yaml

parameters:
    # Permet de passer cette url en tant que variable d'environnement
    env(DATABASE_URL): ''

doctrine:
    dbal:
        driver: 'pdo_mysql'
        server_version: '5.7'
        charset: utf8mb4

        url: '%env(resolve:DATABASE_URL)%'
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                type: annotation
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

Très souvent, il faudra seulement définir le DATABASE_URL qui peut être de la forme suivante et qui dépend de la configuration et du type de base de données utilisé :

mysql://utilisateur:mot_de_passe@ip:un_port/nom_de_la_base_de_données

Symfony 4 est fourni avec une intégration de Doctrine ORM qui fournit de nombreuses commandes dans la console. Pour créer le schéma de la base de données, par exemple :

php bin/console doctrine:schema:create

Cette commande va générer et exécuter les instructions nécessaires à la création de la base de données configurée.

Pour mettre à jour le schéma, il faudra utiliser la commande  doctrine:schema:update .

Par exemple, voici ce que nous donne l'appel de la commande après ajout de la classe Article utilisée précédemment :

➜ php bin/console doctrine:schema:update
 !
 ! [CAUTION] This operation should not be executed in a production environment!
 !
 !
 !
 ! Use the incremental update to detect changes during development and use
 !
 ! the SQL DDL provided to manually update your database in production.
 !

 The Schema-Tool would execute "1" query to update the database.

 Please run the operation by passing one - or both - of the following options:

 doctrine:schema:update --force to execute the command
 doctrine:schema:update --dump-sql to dump the SQL statements to the screen

➜ php bin/console doctrine:schema:update --dump-sql

CREATE TABLE blog_article (id INTEGER NOT NULL, title VARCHAR(255) NOT NULL, content CLOB NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id));

Créez des relations entre vos entités

À un certain niveau de complexité, vos objets PHP vont interagir les uns avec les autres :

  • Dans une relation 1-1 : un objet A correspond à un objet B ;

  • Dans une relation 1-n : un objet A est lié à de nombreuses instances de B ;

  • Dans une relation n-n : des objets de type A ont de multiples relations avec des objets de type B.

Doctrine supporte différents types d'associations :

  • One-To-One : 1 entité est liée à 1 entité ;

  • Many-To-One : plusieurs entités liées à 1 entité (liée à une OneTo-Many) ;

  • One-To-Many : une entité liée à plusieurs ;

  • Many-To-Many : plusieurs entités liées à plusieurs.

Les relations de type 1-1

Les relations de type 1-1 sont dans la pratique assez rares. Imaginons un objet "Commande" lié à un objet "Panier" dans le cadre d'une boutique e-commerce : une commande correspond à un panier validé par le client.

La relation ne sera décrite que dans l'un des objets : il nous appartient de décider lequel selon notre propre logique business.

À mon sens, il y a peu de chances que l'on consulte un panier déjà validé dans un espace d'administration, puisque nous avons accès à la commande. Par contre, grâce au panier, nous accédons à la liste des produits achetés dans la commande. Il semble donc plus utile de décrire la relation dans la classe Commande :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Objet représentant une commande Client
 */
class Command
{
    /**
     * Il y a un seul panier possible par commande
     * @ORM\OneToOne(targetEntity="App\Entity\Cart")
     * @ORM\JoinColumn(name="cart_id", referencedColumnName="id")
     */
    private $cart;
    
    // ...
}

/**
 * Objet représentant un panier d'achats.
 * @Entity()
 */
class Cart
{
    // ...
}

Ici, nous avons utilisé deux annotations de Doctrine, OneToOne et JoinColumn:

  • OneToOne permet de définir la relation entre les deux entités en permettant de définir une entité liée à l'aide du paramètre targetEntity ;

  • JoinColumn est une annotation facultative : elle permet de définir la clé étrangère qui fait office de référence dans la table.

Voici par exemple le code SQL (simplifié) que générerait Doctrine ORM pour cette relation :

CREATE TABLE Command (
    id INT AUTO_INCREMENT NOT NULL,
    cart_id INT DEFAULT NULL,
    UNIQUE INDEX UNIQ_6FBC94267FE4B2B (cart_id),
    PRIMARY KEY(id)
) ENGINE = InnoDB;

CREATE TABLE Cart (
    id INT AUTO_INCREMENT NOT NULL,
    PRIMARY KEY(id)
) ENGINE = InnoDB;
ALTER TABLE Command ADD FOREIGN KEY (cart_id) REFERENCES Cart(id);

Et si j'ai besoin d'accéder à ma commande à partir de mon panier, je fais comment ? :o

Il est possible de déclarer la relation OneToOne de façon "bidirectionnelle". Nous ne détaillerons pas cela ici, mais la documentation officielle est très claire sur le sujet (surtout quand vous aurez compris comment fonctionnent les relations de type 1-n).

Les relations de type 1-n

Les relations 1-n (ou encore 1 à n) sont très communes ! Reprenons notre exemple précédent : un client a eu un problème avec sa commande, il ne l'a jamais reçue. Il doit y avoir un problème avec son adresse de livraison, ou de facturation.

Il y a donc pour 1 client n adresses, n'est-ce pas ?

Voici comment nous pourrions représenter cette relation de façon "bidirectionnelle" (c'est-à-dire que chaque entité a connaissance de la relation) :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Objet qui définit un client
 */
class Customer
{
    /**
     * Un client a potentiellement plusieurs adresses
     * @ORM\OneToMany(targetEntity="App\Entity\Address", mappedBy="customer")
     */
    private $addresses;
    
    // ...
    
    public function __construct() {
        $this->addresses = new ArrayCollection();
    }
}

/**
 * Objet qui définit une adresse
 */
class Address
{
    // ...
    
    /**
     * Les adresses sont liées à un client
     * @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="adresses")
     * @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
     */
    private $customer;
}

C'est l'occasion pour vous de découvrir les deux attributs mappedBy et inversedBy. En fait, il y a toujours une entité qui est "propriétaire" de la relation. Dans ce cas métier, on peut considérer que c'est le client qui est propriétaire de la relation. En effet, une adresse "client" ne peut exister sans client !

  • L'entité propriétaire doit définir l'attribut "mappedBy" : il correspond à la propriété de l'objet "possédé" qui fait le lien entre les deux entités.

  • L'entité possédée doit définir l'attribut "inversedBy" : il correspond à la propriété de l'objet "propriétaire" qui fait le lien entre les deux entités.

Ce n'est vraiment pas clair tout ça... une solution pour retenir cela ?

Oui, disons-le autrement :

  • l'annotation ManyToOne a un attribut inversedBy ;

  • l'annotation OneToMany a un attribut mappedBy.

Enfin, nous avons dû adapter le code de notre classe pour initialiser la propriété  $adresses  en tant qu'un  ArrayCollection  vide. Doctrine ORM va cette fois non pas retrouver un objet  Address, mais bien une collection d'objets de type  Address ! Les créateurs du projet recommandent donc d'instancier la propriété en tant qu'ArrayCollection vide par défaut.

Il existe des cas particuliers pour lesquels une relation ManyToOne ou OneToMany peut être unidirectionnelle ; n'hésitez pas à parcourir la documentation de Doctrine ORM dans ce cas. 

Les relations de type n-n

Reprenons notre exemple du panier d'un client. Ce panier contient des produits, et ces produits peuvent faire partie de plusieurs paniers (dans la limite des stocks disponibles, mais laissons cela de côté :lol: ). On peut donc définir une relation de type ManyToMany entre notre entité Product et notre entité Cart, n'est-ce pas ?

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Objet qui définit un produit
 */
class Product
{
    /**
     * Un produit peut être mis dans plusieurs paniers
     * @ORM\ManyToMany(targetEntity="App\Entity\Cart", inversedBy="products")
     * @JoinTable(name="products_carts")
     */
    private $carts;
    
    // ...
    
    public function __construct() {
        $this->carts = new ArrayCollection();
    }
}

/**
 * Objet qui définit un panier
 */
class Cart
{
    // ...
    
    /**
     * Les produits sont liés à un panier
     * @ORM\ManyToMany(targetEntity="App\Entity\Products", mappedBy="carts")
     */
    private $products;

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

Rien de nouveau ici, mais une question que nous pourrions nous poser :

Comment décider quelle annotation ManyToMany doit avoir l'attribut mappedBy et l'autre inversedBy ? :o

Eh bien... c'est comme pour les relations de type 1-n : quelle entité, quel objet est "propriétaire" de cette relation ? Ici, c'est le panier qui possède des produits, nous utilisons donc "mappedBy" pour l'entité Cart.

Manipulez les entités dans vos contrôleurs

Maintenant que nos relations sont bien définies, il serait temps de manipuler nos entités dans nos contrôleurs Symfony, n'est-ce pas ?

Ensemble, voyons comment vous pouvez créer, éditer, rechercher et supprimer vos entités.

Manipulation d'objets en base : création, mise à jour et suppression

Nous l'avons vu, c'est l'Entity Manager qui est responsable de la gestion de nos entités. C'est pourquoi nous avons besoin de l'injecter dans nos contrôleurs pour "persister" et "flusher" nos entités. Mettons à jour notre formulaire de création d'articles que nous avions réalisé dans le chapitre sur les formulaires :

<?php
// src/Controller/FormController.php

/**
 * @Route("/form/new")
 */
public function new(Request $request)
{
    $article = new Article();
    $article->setTitle('Hello World');
    $article->setContent('Un très court article.');
    $article->setAuthor('Zozor');

    $form = $this->createForm(ArticleType::class, $article);

    $form->handleRequest($request);

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

    return $this->render('default/new.html.twig', array(
        'form' => $form->createView(),
    ));
}

Pour la création, c'est aussi simple que cela ! Au "flush", l'objet sera converti en une requête SQL de type INSERT... mais vous n'avez pas besoin de vous en préoccuper, c'est le rôle de l'ORM de le faire pour vous ! :magicien:

Et pour la mise à jour, c'est tout aussi simple :

<?php
// src/Controller/FormController.php

/**
 * @Route("/form/edit/{id<\d+>}")
 */
public function edit(Request $request, Article $article)
{
    $form = $this->createForm(ArticleType::class, $article);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // va effectuer la requête d'UPDATE en base de données
        $this->getDoctrine()->getManager()->flush();
    }

    return $this->render('default/new.html.twig', array(
        'form' => $form->createView(),
    ));
}

Que s'est-il passé ici ? Deux choses :

  • Tout d'abord, comme vous pouvez le voir, Symfony 4 est capable de retrouver l'Article à l'aide de Doctrine ORM directement en utilisant l'id passé dans la route, génial !

  • Ensuite, cette fois, pas besoin de "persister" l'entité : en effet, l'objet a déjà été retrouvé à partir de Doctrine ORM.

Et la suppression, alors ? Encore plus simple !

<?php
// src/Controller/FormController.php

/**
 * @Route("/form/delete/{id<\d+>}", methods={"POST"})
 */
public function delete(Request $request, Article $article)
{
    $em = $this->getDoctrine()->getManager();
    
    $em->remove($article);
    $em->flush();

    // redirige la page
    return $this->redirectToRoute('admin_article_index');
}
Recherche d'objets en base : place aux repositories !

 Pour ce qui est de la recherche d'objets en base, Doctrine ORM recommande d'utiliser des repositories (rappelez-vous la classe que l'on peut définir dans l'annotation Entity à l'aide de l'attribut repositoryClass).

D'accord, mais c'est quoi un repository ? :euh:

C'est une question qui a de multiples réponses, et vous trouverez de nombreux articles et conférences sur ce sujet. Disons simplement qu'un repository est un objet dont la responsabilité est de récupérer une collection d'objets. Les repositories Doctrine ORM ont accès à deux objets principalement :

  • l'EntityManager, que vous connaissez déjà ;

  • et un QueryBuilder (un constructeur de requêtes) pour vous aider à faire des recherches plus fines dans la collection d'entités disponibles.

Chaque entité pour laquelle nous n'avons pas déclaré de repository particulier dispose d'un repository par défaut accessible dans tous les contrôleurs de nos applications.

<?php

$repository = $this->getDoctrine()->getRepository(Article::class);

Ce repository dispose de plusieurs fonctions pour retrouver vos objets parmi la collection disponible (ou concrètement, parmi ceux disponibles en base de données) :

<?php

$repository = $this->getDoctrine()->getRepository(Article::class);

// Récupère l'objet en fonction de l'@Id (généralement appelé $id)
$article = $repository->find($id);

// Recherche d'un seul article par son titre
$article = $repository->findOneBy(['title' => 'Amazing article']);

// Ou par titre et nom d'auteur
$article = $repository->findOneBy([
    'title' => 'Amazing Article',
    'price' => 'Zozor',
]);

// Recherche de tous les articles en fonction de multiples conditions
$articles = $repository->findBy(
    ['author' => 'Zozor'],
    ['title' => 'ASC'], // le deuxième paramètre permet de définir l'ordre
    10, // le troisième la limite
    2 // et à partir duquel on récupère ("OFFSET" au sens MySQL)
);

// Retrouver tous les articles
$articles = $repository->findAll();

Avec toutes ses fonctions, 99 % des besoins de vos applications devraient être couverts. Si vous souhaitez créer des fonctions spécifiques, il faudra déclarer et créer vos propres repositories :

<?php

namespace App\Repository;

use App\Entity\Article;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;

class ArticleRepository extends ServiceRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Article::class);
    }

    public function findLastArticles()
    {
        return $this->findBy([], ['publicationDate' => 'DESC']);
    }
}


// Dans un contrôleur

public function getLastArticles(ArticleRepository $repository)
{
    $articles = $repository->findLastArticles(); // ArrayCollection
    
    // ...
}

Gérez les migrations de votre base de données

Parlons maintenant d'un problème difficile qui survient lorsque nous travaillons sur une application qui doit évoluer et qui est déjà disponible en production. Votre client vous a demandé d'effectuer des modifications qui ont eu un impact sur vos entités et donc sur votre base de données. Il falloir mettre la base de données à jour et cela semble risqué... :'( Mais ça, c'était avant que vous n'entendiez parler du bundle DoctrineMigration !

Introduction au bundle DoctrineMigration

Les fichiers de migration sont une méthode de mise à jour de base de données en toute sécurité, que ce soit en local ou sur un serveur de production.

Plutôt que d'exécuter la commande  doctrine:schema:update  ou d'appliquer les modifications en base de données à l'aide de scripts SQL, les fichiers de migration permettent d'appliquer les changements et de les annuler sans risques de corruption de la base de données.

Installation et configuration

Pour installer cette extension dans votre projet, vous aurez besoin de Composer :

$ composer require doctrine/doctrine-migrations-bundle "^2.0"

Et c'est tout ! Grâce à Symfony Flex fourni avec Symfony 4, l'extension a été entièrement configurée.

S'il est correctement installé, vous devriez avoir accès de nouvelles commandes dans la console de Symfony :

doctrine:migrations
    :diff [diff] Génères une migration en comparant la base de données avec les informations de mapping.
    :execute [execute] Exécute une migration manuellement.
    :generate [generate] Crées une classe de Migration.
    :migrate [migrate] Effectues une migration vers le fichier de migration le plus récent ou celui spécifié.
    :status [status] Affiche le status des migrations.
    :version [version] Ajoute et supprime manuellement des versions à partir de la version en en base.

 Commençons par regarder le statut des migrations de notre application à partir de la commande  doctrine:migrations:status  :

➜ bin/console doctrine:migrations:status

 == Configuration

 >> Name: Application Migrations
 >> Database Driver: pdo_sqlite
 >> Database Name: /var/www/html/var/data/blog.sqlite
 >> Configuration Source: manually configured
 >> Version Table Name: migration_versions
 >> Version Column Name: version
 >> Migrations Namespace: App\Migrations
 >> Migrations Directory: /var/www/html/src/Migrations
 >> Previous Version: Already at first version
 >> Current Version: 0
 >> Next Version: Already at latest version
 >> Latest Version: 0
 >> Executed Migrations: 0
 >> Executed Unavailable Migrations: 0
 >> Available Migrations: 0
 >> New Migrations: 0

Nous venons d'installer l'extension et donc actuellement, nous n'avons aucun fichier de migration ni aucune version de migration actuellement appliquée sur notre base de données. Puisque nous avons manipulé un nouvel objet Article tout au long du chapitre, notre base de données n'est pas synchronisée avec ce nouvel objet : et si l'on créait notre premier fichier de migration ? ;)

Créez votre premier fichier de migration

Pour créer notre premier fichier de configuration, nous pouvons utiliser la commande   doctrine:migrations:generate :

➜ bin/console doctrine:migrations:generate
Generated new migration class to "/var/www/html/src/Migrations/Version20181028033516.php"

En ouvrant le fichier correspondant, nous voyons qu'il n'est pas très utile...

<?php

namespace App\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

class Version20181028033516 extends AbstractMigration
{
    public function up(Schema $schema)
    {

    }

    public function down(Schema $schema)
    {

    }
}

Et pourtant, si l'on réutilise la commande  doctrine:migrations:status , il y a eu du changement !

➜ bin/console doctrine:migrations:status
 
== Configuration

 >> Name: Application Migrations
 >> Database Driver: pdo_sqlite
 >> Database Name: /var/www/html/var/data/blog.sqlite
 >> Configuration Source: manually configured
 >> Version Table Name: migration_versions
 >> Version Column Name: version
 >> Migrations Namespace: App\Migrations
 >> Migrations Directory: /var/www/html/src/Migrations
 >> Previous Version: Already at first version
 >> Current Version: 0
 >> Next Version: 2018-10-28 03:35:16 (20181028033516)
 >> Latest Version: 2018-10-28 03:35:16 (20181028033516)
 >> Executed Migrations: 0
 >> Executed Unavailable Migrations: 0
 >> Available Migrations: 1
 >> New Migrations: 1

Un fichier de migration (vide !) a été créé ; il est donc disponible. Pour preuve, utilisons la commande de migration pour appliquer nos... modifications : :p

➜ bin/console doctrine:migrations:migrate 20181028033516

 Application Migrations


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 20181028033516 from 0

 ++ migrating 20181028033516

Migration 20181028033516 was executed but did not result in any SQL statements.

 ++ migrated (0.03s)

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

 ++ finished in 0.03s
 ++ 1 migrations executed
 ++ 0 sql queries

Si la création de fichiers de migration personnalisés n'est pas un des objectifs de ce cours, voici ce que nous aurions pu écrire dans la classe de migration pour qu'elle crée la table Article :

<?php

namespace App\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

class Version20181028033516 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        $this->addSql('CREATE TABLE article (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id))');
    }

    public function down(Schema $schema)
    {
        $this->addSql('DROP TABLE article');
    }
}

Et le résultat d'exécution correspond :

➜ bin/console doctrine:migrations:migrate 20181028035145

 Application Migrations


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 20181028035145 from 20181028033516

 ++ migrating 20181028035145

 -> CREATE TABLE article (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id))

 ++ migrated (0.05s)

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

 ++ finished in 0.05s
 ++ 1 migrations executed
 ++ 1 sql queries

Attends... tu nous avais dit que l'on n'aurait pas besoin d'écrire de SQL par nous-mêmes. Comment fait-on, alors ? :-°

Doctrine ORM est capable de les générer automatiquement ! C'est quand même bien d'utiliser un ORM plutôt que de tout gérer à main : regardons ensemble comment faire.

Générez automatiquement vos fichiers de migration

Dans ce chapitre, nous avons donc créé une nouvelle entité appelée Article qui a été "mappée" et reconnue par Doctrine ORM. Utilisons la commande  doctrine:migrations:diff, cette fois :

➜ bin/console doctrine:migrations:migrate 20181028033516

Generated new migration class to "/var/www/html/src/Migrations/Version20181028040934.php" from schema differences.

D'accord... cela ne nous aide pas beaucoup. Regardons donc le fichier généré par le bundle Doctrine Migrations, cette fois :

<?php

namespace App\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

class Version20181028033516 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');

        $this->addSql('CREATE TABLE article (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id))');
    }

    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');

        $this->addSql('DROP TABLE article');
    }
}

Et voilà ! L'extension a automatiquement créé le fichier de migration que vous pourrez appliquer en toute sécurité en local, comme sur votre serveur de production. En cas de problèmes, pas de panique ! Réappliquez la migration précédente. :ange:

En résumé

Dans la plupart des projets Symfony, c'est l'ORM Doctrine qui est utilisé pour la gestion de vos objets en base de données. L'ORM est composée de deux objets principaux :

  • l'Entity Manager qui fournit des fonctions que l'on manipule pour créer, éditer, rechercher et supprimer nos objets métiers ;

  • le Data Mapper qui s'occupe d'effectuer les requêtes auprès du système de gestion de base de données sélectionné (généralement MySQL).

Pour cela, à l'aide d'annotations, on indique à Doctrine ORM quels sont les objets que nous souhaitons manipuler en base et quelles relations existent entre eux. Elles sont de trois types principalement :

  • les relations 1 à 1 ;

  • les relations 1 à n ;

  • les relations n à n.

Ensuite, le repository accessible pour toutes les entités par défaut fournit de nombreuses fonctions pour parcourir la liste d'objets stockés en base : il retourne soit l'objet métier, soit un objet de type ArrayCollection qui contient une liste de nos objets métiers.

Enfin, il existe un plugin open source pour nous aider à gérer les mises à jour de base de données nécessaires à l'évolution de nos projets Symfony 4. L'extension DoctrineMigrationBundle permet, en quelques commandes, de générer des fichiers de migration et de les exécuter sans risques en production.

Avec ce que vous venez d'apprendre, vous êtes capable de gérer une base de données sans même connaître MySQL : félicitations !

Passons maintenant à un autre sujet essentiel de vos applications : la sécurité.

À tout de suite !

Exemple de certificat de réussite
Exemple de certificat de réussite