đŸ—„ïž Doctrine ORM

Gérer la base de données avec des objets PHP

EntitĂ©s ‱ Relations ‱ RequĂȘtes ‱ Migrations

Formation DWWM - 2026

Au programme 📋

# 🎯 Partie 1

Qu'est-ce que Doctrine ORM ?

Object-Relational Mapping

C'est quoi un ORM ? đŸ€”

ORM = Object-Relational Mapping = Faire correspondre des objets PHP avec des tables SQL.

Analogie : Le traducteur automatique 🌐

Imagine que tu parles français et ton ami parle japonais :

Tu parles PHP, Doctrine parle SQL pour toi !

Sans ORM vs Avec Doctrine 🆚

❌ Sans ORM (SQL pur en PHP)

// Créer un utilisateur
$sql = "INSERT INTO user (name, email, created_at) 
        VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
    'Marie', 
    'marie@example.com', 
    date('Y-m-d H:i:s')
]);
$userId = $pdo->lastInsertId();

// Récupérer un utilisateur
$sql = "SELECT * FROM user WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId]);
$userData = $stmt->fetch();

// Modifier
$sql = "UPDATE user SET email = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute(['new@example.com', $userId]);

// Supprimer
$sql = "DELETE FROM user WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId]);

✅ Avec Doctrine (Objets PHP)

// Créer un utilisateur
$user = new User();
$user->setName('Marie');
$user->setEmail('marie@example.com');
$user->setCreatedAt(new \DateTime());

$entityManager->persist($user);
$entityManager->flush();

// Récupérer un utilisateur
$user = $repository->find($userId);

// Modifier
$user->setEmail('new@example.com');
$entityManager->flush();

// Supprimer
$entityManager->remove($user);
$entityManager->flush();

✅ Avantages de Doctrine :

Le mapping Objet ↔ Table 🔗

Classe PHP (Entité)
User {
- id: int
- name: string
- email: string
}
↕ Doctrine fait le lien
Table SQL
user (
id INT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
)
Concept Objet Équivalent SQL
Classe (Entity) Table
Propriété Colonne
Instance d'objet Ligne (enregistrement)
Collection d'objets Plusieurs lignes
Relation (référence) Clé étrangÚre
# ⚙ Partie 2

Installation et Configuration

Préparer Doctrine

Installer Doctrine đŸ“„

Commande d'installation

composer require symfony/orm-pack
composer require --dev symfony/maker-bundle

💡 Que fait cette commande ?

Fichiers créés

config/
├── packages/
│   └── doctrine.yaml      # Configuration Doctrine
└── .env                   # Variables d'environnement

src/
├── Entity/                # Dossier des entitĂ©s (vide)
└── Repository/            # Dossier des repositories (vide)

Configurer la base de donnĂ©es 🔧

Modifier le fichier .env pour configurer la connexion :

MySQL / MariaDB

# .env
DATABASE_URL="mysql://utilisateur:motdepasse@127.0.0.1:3306/nom_base?serverVersion=8.0"

# Exemples réels :
# DATABASE_URL="mysql://root:@127.0.0.1:3306/mon_blog"
# DATABASE_URL="mysql://dwwm:secret@127.0.0.1:3306/symfony_app"

PostgreSQL

DATABASE_URL="postgresql://utilisateur:motdepasse@127.0.0.1:5432/nom_base?serverVersion=15&charset=utf8"

SQLite (développement)

DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

⚠ SĂ©curitĂ© : Ne JAMAIS commit le fichier .env avec de vrais mots de passe ! Utilisez .env.local qui est dans le .gitignore.

CrĂ©er la base de donnĂ©es đŸ—„ïž

Commande de création

php bin/console doctrine:database:create

✅ RĂ©sultat :

Created database `mon_blog` for connection named default

Autres commandes utiles

# Supprimer la base de données
php bin/console doctrine:database:drop --force

# Vérifier la connexion
php bin/console doctrine:query:sql "SELECT 1"
# 📩 Partie 3

Entités et Propriétés

Les classes qui représentent vos tables

C'est quoi une EntitĂ© ? 📩

Une Entité est une classe PHP qui représente une table en base de données.

Analogie : La fiche d'identitĂ© đŸȘȘ

Une entité = une fiche descriptive d'un objet du monde réel :

Chaque fiche a des champs (nom, email...) = propriétés de l'entité !

CrĂ©er une entitĂ© đŸ—ïž

Commande interactive

php bin/console make:entity

Exemple de création : Article

$ php bin/console make:entity Article

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

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

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

Field length [255]:
> 255

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

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

Field type [string]:
> text

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

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

Field type [string]:
> datetime

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

New property name (press <return> to stop adding fields):
> (appuyer sur Entrée pour finir)

Code de l'entitĂ© gĂ©nĂ©rĂ©e đŸ’»

src/Entity/Article.php

<?php
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $content = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
    private ?\DateTimeInterface $createdAt = null;

    // Getters et Setters générés automatiquement
    
    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;
        return $this;
    }
    
    // ... autres getters/setters
}

Comprendre les attributs Doctrine đŸ·ïž

#[ORM\Entity]

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article

Indique que cette classe est une entité Doctrine avec son repository associé.

#[ORM\Id] et #[ORM\GeneratedValue]

#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

Clé primaire auto-incrémentée (comme id INT AUTO_INCREMENT PRIMARY KEY).

#[ORM\Column]

#[ORM\Column(length: 255)]
private ?string $title = null;

Colonne VARCHAR(255) en SQL.

#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;

Colonne TEXT en SQL (pour longs textes).

Types de colonnes Doctrine 📊

Type Doctrine Type SQL Type PHP Usage
string VARCHAR string Texte court (255 max)
text TEXT string Texte long
integer INT int Nombre entier
float FLOAT float Décimal
boolean TINYINT(1) bool Vrai/Faux
datetime DATETIME DateTime Date et heure
date DATE DateTime Date seulement
time TIME DateTime Heure seulement
json JSON array Données JSON

Options de colonne đŸŽ›ïž

Options courantes

Option Description Exemple
length Longueur max (VARCHAR) #[ORM\Column(length: 100)]
nullable Peut ĂȘtre NULL #[ORM\Column(nullable: true)]
unique Valeur unique (email...) #[ORM\Column(unique: true)]
options Options SQL natives #[ORM\Column(options: ['default' => 0])]

Exemples complets

// Email unique
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;

// Prix avec 2 décimales
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private ?string $price = null;

// Description optionnelle
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;

// Booléen avec valeur par défaut
#[ORM\Column(options: ['default' => false])]
private ?bool $isPublished = false;
# 🔗 Partie 4

Relations entre Entités

Lier les tables entre elles

Les 4 types de relations 🔗

Analogie : Relations humaines đŸ‘„

Relation Exemple En SQL
OneToOne User ↔ Profile FK dans Profile
OneToMany Category → Articles FK dans Article
ManyToOne Articles → Category FK dans Article
ManyToMany Articles ↔ Tags Table intermĂ©diaire

ManyToOne - Plusieurs vers Un đŸ‘„â†’đŸ‘€

Cas d'usage : Plusieurs articles appartiennent à UNE catégorie.

Créer la relation

php bin/console make:entity Article

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

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

What class should this entity be related to?:
> Category

Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne

Is the Article.category property allowed to be null (nullable)? (yes/no) [yes]:
> no

Do you want to add a new property to Category so that you can access/update 
Article objects from it - e.g. $category->getArticles()? (yes/no) [yes]:
> yes

New field name inside Category [articles]:
> articles

Do you want to automatically delete orphaned App\Entity\Article objects 
(orphanRemoval)? (yes/no) [no]:
> no

Code gĂ©nĂ©rĂ© - ManyToOne đŸ’»

src/Entity/Article.php

#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?Category $category = null;

public function getCategory(): ?Category
{
    return $this->category;
}

public function setCategory(?Category $category): static
{
    $this->category = $category;
    return $this;
}

src/Entity/Category.php

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')]
private Collection $articles;

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

/**
 * @return Collection<int, Article>
 */
public function getArticles(): Collection
{
    return $this->articles;
}

public function addArticle(Article $article): static
{
    if (!$this->articles->contains($article)) {
        $this->articles->add($article);
        $article->setCategory($this);
    }
    return $this;
}

public function removeArticle(Article $article): static
{
    if ($this->articles->removeElement($article)) {
        if ($article->getCategory() === $this) {
            $article->setCategory(null);
        }
    }
    return $this;
}

Utiliser la relation ManyToOne 🎯

Créer un article avec catégorie

// Récupérer une catégorie
$category = $categoryRepository->find(1);

// Créer un article
$article = new Article();
$article->setTitle('Mon article');
$article->setContent('Contenu...');
$article->setCategory($category);  // ← Associer la catĂ©gorie

$entityManager->persist($article);
$entityManager->flush();

Afficher la catégorie d'un article

$article = $articleRepository->find(1);

// Accéder à la catégorie
echo $article->getCategory()->getName();

// Dans Twig
{{ article.category.name }}

Afficher tous les articles d'une catégorie

$category = $categoryRepository->find(1);

// Parcourir les articles
foreach ($category->getArticles() as $article) {
    echo $article->getTitle();
}

// Dans Twig
{% for article in category.articles %}
    <h3>{{ article.title }}</h3>
{% endfor %}

ManyToMany - Plusieurs vers Plusieurs đŸ‘„â†”đŸ‘„

Cas d'usage : Un article peut avoir plusieurs tags, un tag peut ĂȘtre sur plusieurs articles.

Créer la relation

php bin/console make:entity Article

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

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

What class should this entity be related to?:
> Tag

Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToMany

Do you want to add a new property to Tag so that you can access/update 
Article objects from it - e.g. $tag->getArticles()? (yes/no) [yes]:
> yes

Code gĂ©nĂ©rĂ© - ManyToMany đŸ’»

src/Entity/Article.php

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
private Collection $tags;

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

/**
 * @return Collection<int, Tag>
 */
public function getTags(): Collection
{
    return $this->tags;
}

public function addTag(Tag $tag): static
{
    if (!$this->tags->contains($tag)) {
        $this->tags->add($tag);
    }
    return $this;
}

public function removeTag(Tag $tag): static
{
    $this->tags->removeElement($tag);
    return $this;
}

💡 En SQL : Doctrine crĂ©e automatiquement une table article_tag avec deux colonnes article_id et tag_id.

Utiliser la relation ManyToMany 🎯

Ajouter des tags Ă  un article

$article = new Article();
$article->setTitle('Mon article');

$tag1 = $tagRepository->findOneBy(['name' => 'PHP']);
$tag2 = $tagRepository->findOneBy(['name' => 'Symfony']);

$article->addTag($tag1);
$article->addTag($tag2);

$entityManager->persist($article);
$entityManager->flush();

Afficher les tags d'un article

$article = $articleRepository->find(1);

foreach ($article->getTags() as $tag) {
    echo $tag->getName();
}

// Dans Twig
{% for tag in article.tags %}
    <span class="badge">{{ tag.name }}</span>
{% endfor %}

Afficher les articles d'un tag

$tag = $tagRepository->findOneBy(['name' => 'PHP']);

foreach ($tag->getArticles() as $article) {
    echo $article->getTitle();
}

OneToOne - Un vers Un đŸ‘€â†”đŸ‘€

Cas d'usage : Un utilisateur a UN profil, un profil appartient Ă  UN utilisateur.

Code exemple

// Dans User.php
#[ORM\OneToOne(targetEntity: Profile::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
private ?Profile $profile = null;

// Dans Profile.php
#[ORM\OneToOne(targetEntity: User::class, inversedBy: 'profile')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;

Utilisation

$user = new User();
$user->setEmail('user@example.com');

$profile = new Profile();
$profile->setBio('Développeur passionné');
$profile->setUser($user);

$entityManager->persist($user);
$entityManager->persist($profile);
$entityManager->flush();

// AccĂšs
echo $user->getProfile()->getBio();
echo $profile->getUser()->getEmail();
# đŸ’Ÿ Partie 5

EntityManager et Persistance

Sauvegarder et manipuler les données

C'est quoi l'EntityManager ? đŸŽ›ïž

EntityManager = Le chef d'orchestre de Doctrine. Il gÚre TOUTES les opérations sur la base de données.

Analogie : Le gestionnaire de transactions bancaires 🏩

Tu prépares plusieurs opérations, puis tu les exécutes TOUTES EN UNE FOIS avec flush() !

Injecter l'EntityManager 💉

Dans un contrĂŽleur

use Doctrine\ORM\EntityManagerInterface;

class ArticleController extends AbstractController
{
    #[Route('/article/create', name: 'article_create')]
    public function create(EntityManagerInterface $entityManager): Response
    {
        // $entityManager est automatiquement injecté
        
        $article = new Article();
        $article->setTitle('Mon article');
        $article->setContent('Contenu...');
        
        $entityManager->persist($article);
        $entityManager->flush();
        
        return new Response('Article créé avec ID : ' . $article->getId());
    }
}

💡 Injection de dĂ©pendances : Symfony injecte automatiquement l'EntityManager quand vous le typez en paramĂštre !

persist() et flush() đŸ’Ÿ

persist() - Préparer

$article = new Article();
$article->setTitle('Titre');

$entityManager->persist($article);
// ← L'objet est "trackĂ©" mais PAS ENCORE en base !

flush() - Exécuter

$entityManager->flush();
// ← MAINTENANT les requĂȘtes SQL sont exĂ©cutĂ©es

Pourquoi séparer persist() et flush() ?

Performance

On peut préparer 100 objets avec persist(), puis faire UN SEUL flush() = UNE SEULE transaction SQL.

Atomicité

Si une erreur survient, RIEN n'est sauvegardé (tout ou rien).

Exemple : Créer plusieurs objets

for ($i = 1; $i <= 100; $i++) {
    $article = new Article();
    $article->setTitle("Article $i");
    $entityManager->persist($article);
}

// UNE SEULE requĂȘte SQL pour tout insĂ©rer
$entityManager->flush();

OpĂ©rations CRUD complĂštes 🔄

CREATE - Créer

$article = new Article();
$article->setTitle('Nouveau');
$article->setContent('Contenu');

$entityManager->persist($article);
$entityManager->flush();

// L'ID est maintenant disponible
echo $article->getId();

READ - Lire

// Via repository (voir partie suivante)
$article = $articleRepository->find(1);

UPDATE - Modifier

$article = $articleRepository->find(1);
$article->setTitle('Titre modifié');

// PAS BESOIN de persist() pour un objet déjà en base !
$entityManager->flush();

DELETE - Supprimer

$article = $articleRepository->find(1);

$entityManager->remove($article);
$entityManager->flush();
# 🔍 Partie 6

Repository et RequĂȘtes

Chercher et filtrer les données

C'est quoi un Repository ? 📚

Repository = BibliothÚque de méthodes pour chercher des entités.

Analogie : Le bibliothĂ©caire 📖

Le repository = un bibliothécaire qui connaßt parfaitement sa bibliothÚque :

Obtenir un Repository 🎯

Méthode 1 : Via EntityManager

$repository = $entityManager->getRepository(Article::class);
$article = $repository->find(1);

Méthode 2 : Injection directe (recommandée)

use App\Repository\ArticleRepository;

class ArticleController extends AbstractController
{
    #[Route('/article/{id}', name: 'article_show')]
    public function show(int $id, ArticleRepository $repository): Response
    {
        $article = $repository->find($id);
        
        if (!$article) {
            throw $this->createNotFoundException('Article introuvable');
        }
        
        return $this->render('article/show.html.twig', [
            'article' => $article
        ]);
    }
}

MĂ©thodes de base du Repository 🔧

Méthode Description Retour
find($id) Trouver par ID Object | null
findAll() Tous les enregistrements Array
findBy($criteria, $orderBy, $limit, $offset) Chercher avec critĂšres Array
findOneBy($criteria) Premier résultat correspondant Object | null
count($criteria) Compter les résultats int

Exemples

// Par ID
$article = $repository->find(5);

// Tous
$articles = $repository->findAll();

// Par critĂšres
$articles = $repository->findBy(['category' => $category]);

// Avec tri et limite
$articles = $repository->findBy(
    ['published' => true],      // CritĂšres
    ['createdAt' => 'DESC'],    // Tri
    10,                          // Limite
    0                            // Offset
);

// Un seul résultat
$article = $repository->findOneBy(['slug' => 'mon-article']);

// Compter
$count = $repository->count(['published' => true]);

CrĂ©er des mĂ©thodes personnalisĂ©es 🎹

Vous pouvez ajouter vos propres méthodes dans le Repository !

src/Repository/ArticleRepository.php

namespace App\Repository;

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

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

    /**
     * Trouver les articles publiés récents
     */
    public function findRecentPublished(int $limit = 10): array
    {
        return $this->createQueryBuilder('a')
            ->andWhere('a.published = :published')
            ->setParameter('published', true)
            ->orderBy('a.createdAt', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    /**
     * Chercher par titre
     */
    public function searchByTitle(string $query): array
    {
        return $this->createQueryBuilder('a')
            ->andWhere('a.title LIKE :query')
            ->setParameter('query', '%' . $query . '%')
            ->getQuery()
            ->getResult();
    }

    /**
     * Articles d'une catégorie avec nombre de commentaires
     */
    public function findByCategory(int $categoryId): array
    {
        return $this->createQueryBuilder('a')
            ->leftJoin('a.comments', 'c')
            ->addSelect('c')
            ->andWhere('a.category = :categoryId')
            ->setParameter('categoryId', $categoryId)
            ->orderBy('a.createdAt', 'DESC')
            ->getQuery()
            ->getResult();
    }
}
# đŸ—ïž Partie 7

QueryBuilder Avancé

Construire des requĂȘtes complexes

C'est quoi le QueryBuilder ? đŸ—ïž

QueryBuilder = Constructeur de requĂȘtes SQL en PHP orientĂ© objet.

Analogie : Les LEGO đŸ§±

Le QueryBuilder = assembler des briques LEGO pour construire une requĂȘte :

Syntaxe de base du QueryBuilder 📝

Structure complĂšte

$qb = $repository->createQueryBuilder('a')  // Alias 'a' pour Article
    ->andWhere('a.published = :published')   // Condition
    ->setParameter('published', true)        // ParamÚtre sécurisé
    ->orderBy('a.createdAt', 'DESC')        // Tri
    ->setMaxResults(10)                      // Limite
    ->getQuery()                             // Construire la requĂȘte
    ->getResult();                           // Exécuter et récupérer

SQL équivalent

SELECT * FROM article a
WHERE a.published = true
ORDER BY a.created_at DESC
LIMIT 10

💡 Alias : 'a' est un alias pour Article. Permet d'Ă©crire a.title au lieu de article.title.

MĂ©thodes du QueryBuilder 🔧

Méthode Description Exemple
select() Colonnes à sélectionner ->select('a.id, a.title')
where() Condition (remplace tout) ->where('a.id = :id')
andWhere() Condition ET ->andWhere('a.published = true')
orWhere() Condition OU ->orWhere('a.featured = true')
orderBy() Tri principal ->orderBy('a.createdAt', 'DESC')
addOrderBy() Tri secondaire ->addOrderBy('a.title', 'ASC')
setMaxResults() LIMIT ->setMaxResults(10)
setFirstResult() OFFSET ->setFirstResult(20)
join() Jointure ->join('a.category', 'c')
leftJoin() Jointure gauche ->leftJoin('a.comments', 'com')

ParamĂštres sĂ©curisĂ©s 🔒

❌ DANGEREUX - Injection SQL

// NE JAMAIS FAIRE ÇA !
$title = $_GET['title'];
->where("a.title = '$title'")  // ← FAILLE DE SÉCURITÉ !

✅ SÛR - ParamĂštres prĂ©parĂ©s

->where('a.title = :title')
->setParameter('title', $title)  // ← Protection automatique

Plusieurs paramĂštres

->andWhere('a.category = :category')
->andWhere('a.published = :published')
->setParameter('category', $category)
->setParameter('published', true)

// OU avec tableau
->setParameters([
    'category' => $category,
    'published' => true
])

Jointures avec QueryBuilder 🔗

INNER JOIN (join)

// Articles avec leur catégorie
$qb = $repository->createQueryBuilder('a')
    ->join('a.category', 'c')        // Jointure
    ->addSelect('c')                  // Charger aussi la catégorie
    ->where('c.name = :categoryName')
    ->setParameter('categoryName', 'Tech')
    ->getQuery()
    ->getResult();

LEFT JOIN (leftJoin)

// Articles mĂȘme sans catĂ©gorie
$qb = $repository->createQueryBuilder('a')
    ->leftJoin('a.category', 'c')
    ->addSelect('c')
    ->getQuery()
    ->getResult();

Jointures multiples

// Articles avec catégorie ET tags
$qb = $repository->createQueryBuilder('a')
    ->leftJoin('a.category', 'c')
    ->leftJoin('a.tags', 't')
    ->addSelect('c', 't')
    ->getQuery()
    ->getResult();

AgrĂ©gations (COUNT, SUM, AVG...) 📊

Compter

// Nombre d'articles publiés
$count = $repository->createQueryBuilder('a')
    ->select('COUNT(a.id)')
    ->where('a.published = true')
    ->getQuery()
    ->getSingleScalarResult();  // Retourne un nombre

Somme

// Total des vues
$totalViews = $repository->createQueryBuilder('a')
    ->select('SUM(a.views)')
    ->getQuery()
    ->getSingleScalarResult();

Moyenne

// Note moyenne
$avgRating = $repository->createQueryBuilder('a')
    ->select('AVG(a.rating)')
    ->getQuery()
    ->getSingleScalarResult();

GROUP BY

// Nombre d'articles par catégorie
$stats = $repository->createQueryBuilder('a')
    ->select('c.name, COUNT(a.id) as nb_articles')
    ->join('a.category', 'c')
    ->groupBy('c.id')
    ->getQuery()
    ->getResult();
# 📝 Partie 8

DQL - Doctrine Query Language

SQL orienté objet

C'est quoi DQL ? 📝

DQL = Doctrine Query Language = SQL mais avec des noms d'objets au lieu de tables.

SQL classique

SELECT * 
FROM article a
JOIN category c ON a.category_id = c.id
WHERE a.published = 1
ORDER BY a.created_at DESC
LIMIT 10

DQL Doctrine

SELECT a, c 
FROM App\Entity\Article a
JOIN a.category c
WHERE a.published = true
ORDER BY a.createdAt DESC

💡 DiffĂ©rences :

Utiliser DQL dans un Repository đŸ’»

Méthode 1 : DQL pur

public function findRecentPublished(): array
{
    $dql = 'SELECT a 
            FROM App\Entity\Article a
            WHERE a.published = :published
            ORDER BY a.createdAt DESC';
    
    return $this->getEntityManager()
        ->createQuery($dql)
        ->setParameter('published', true)
        ->setMaxResults(10)
        ->getResult();
}

Méthode 2 : QueryBuilder (recommandé)

public function findRecentPublished(): array
{
    return $this->createQueryBuilder('a')
        ->where('a.published = :published')
        ->setParameter('published', true)
        ->orderBy('a.createdAt', 'DESC')
        ->setMaxResults(10)
        ->getQuery()
        ->getResult();
}

✅ Recommandation : Utilisez QueryBuilder plutît que DQL pur. Plus lisible et moins d'erreurs !

# 🔄 Partie 9

Migrations de Base de Données

Synchroniser le schéma SQL

C'est quoi une migration ? 🔄

Migration = Fichier PHP qui contient les modifications SQL à appliquer à la base de données.

Analogie : Le carnet de vaccinations 💉

Les migrations = un carnet qui garde l'historique de tous les changements :

On peut revenir en arriĂšre (rollback) ou avancer pas Ă  pas !

CrĂ©er et exĂ©cuter une migration đŸ—ïž

Étape 1 : CrĂ©er les entitĂ©s

php bin/console make:entity Article

Étape 2 : GĂ©nĂ©rer la migration

php bin/console make:migration

✅ RĂ©sultat : Fichier créé dans migrations/

migrations/Version20250128143000.php

Étape 3 : ExĂ©cuter la migration

php bin/console doctrine:migrations:migrate
EntitĂ©s PHP → make:migration → Fichier SQL → migrate → Base de donnĂ©es

Contenu d'une migration 📄

migrations/Version20250128143000.php

<?php
namespace DoctrineMigrations;

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

final class Version20250128143000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Create article table';
    }

    public function up(Schema $schema): void
    {
        // Migration "forward" - créer/modifier
        $this->addSql('CREATE TABLE article (
            id INT AUTO_INCREMENT NOT NULL, 
            title VARCHAR(255) NOT NULL, 
            content LONGTEXT NOT NULL, 
            created_at DATETIME NOT NULL, 
            PRIMARY KEY(id)
        ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`');
    }

    public function down(Schema $schema): void
    {
        // Migration "backward" - annuler
        $this->addSql('DROP TABLE article');
    }
}

Commandes de migration đŸ› ïž

Commande Description
make:migration Créer une nouvelle migration
doctrine:migrations:migrate Exécuter toutes les migrations
doctrine:migrations:status Voir l'état des migrations
doctrine:migrations:diff Générer automatiquement les différences
doctrine:schema:update --dump-sql Voir le SQL sans l'exécuter
doctrine:schema:update --force Appliquer directement (dev only)

⚠ Production : En production, utilisez TOUJOURS les migrations ! N'utilisez JAMAIS doctrine:schema:update --force !

# đŸŒ± Partie 10

Fixtures - Jeux de Données

Remplir la base avec des données de test

C'est quoi des Fixtures ? đŸŒ±

Fixtures = Données de test qu'on charge dans la base de données pour développer.

Analogie : Le jardinier qui plante des graines đŸŒ±

Les fixtures = planter des graines dans un jardin vide :

💡 Pourquoi utiliser des fixtures ?

Installer DoctrineFixturesBundle đŸ“„

Commande d'installation

composer require --dev orm-fixtures

✅ RĂ©sultat : Dossier src/DataFixtures/ créé avec un exemple.

CrĂ©er des Fixtures đŸ—ïž

src/DataFixtures/AppFixtures.php

<?php
namespace App\DataFixtures;

use App\Entity\Article;
use App\Entity\Category;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        // Créer des catégories
        $categories = [];
        $categoryNames = ['Tech', 'Sport', 'Culture', 'Politique'];
        
        foreach ($categoryNames as $name) {
            $category = new Category();
            $category->setName($name);
            $manager->persist($category);
            $categories[] = $category;
        }

        // Créer 20 articles
        for ($i = 1; $i <= 20; $i++) {
            $article = new Article();
            $article->setTitle("Article n°$i");
            $article->setContent("Contenu de l'article n°$i...");
            $article->setCreatedAt(new \DateTime());
            
            // Assigner une catégorie aléatoire
            $randomCategory = $categories[array_rand($categories)];
            $article->setCategory($randomCategory);
            
            $manager->persist($article);
        }

        // Tout sauvegarder en base
        $manager->flush();
    }
}

Charger les Fixtures đŸŒ±

Commande

php bin/console doctrine:fixtures:load

⚠ ATTENTION : Cette commande SUPPRIME toutes les donnĂ©es existantes puis charge les fixtures !

Careful, database "mon_blog" will be purged. Do you want to continue? (yes/no) [no]:
> yes

Charger sans confirmation (scripts automatiques)

php bin/console doctrine:fixtures:load --no-interaction

Ajouter sans supprimer (append)

php bin/console doctrine:fixtures:load --append

Fixtures rĂ©alistes avec Faker 🎭

Installer Faker

composer require --dev fakerphp/faker

Utiliser Faker dans les fixtures

<?php
namespace App\DataFixtures;

use App\Entity\Article;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $faker = Factory::create('fr_FR');  // Données en français

        // Créer 50 utilisateurs
        $users = [];
        for ($i = 0; $i < 50; $i++) {
            $user = new User();
            $user->setName($faker->name());
            $user->setEmail($faker->email());
            $user->setCreatedAt($faker->dateTimeBetween('-2 years'));
            
            $manager->persist($user);
            $users[] = $user;
        }

        // Créer 100 articles
        for ($i = 0; $i < 100; $i++) {
            $article = new Article();
            $article->setTitle($faker->sentence());
            $article->setContent($faker->paragraphs(5, true));
            $article->setCreatedAt($faker->dateTimeBetween('-1 year'));
            $article->setViews($faker->numberBetween(0, 10000));
            $article->setPublished($faker->boolean(80));  // 80% publiés
            $article->setAuthor($faker->randomElement($users));
            
            $manager->persist($article);
        }

        $manager->flush();
    }
}

MĂ©thodes Faker utiles 🎹

Méthode Description Exemple
name() Nom complet Jean Dupont
firstName() Prénom Marie
lastName() Nom Martin
email() Email jean@example.com
sentence() Phrase Ceci est une phrase.
paragraph() Paragraphe Lorem ipsum...
text(200) Texte de N caractĂšres Lorem ipsum dolor...
numberBetween(1, 100) Nombre aléatoire 42
boolean(70) Booléen (70% true) true
dateTimeBetween('-1 year') Date aléatoire 2024-06-15
randomElement($array) ÉlĂ©ment alĂ©atoire Sport
# 🎓 RĂ©capitulatif

Ce qu'on a appris

Workflow complet Doctrine 🔄

  1. Configurer la connexion BDD
    Modifier .env avec DATABASE_URL
  2. Créer la base de données
    php bin/console doctrine:database:create
  3. Créer les entités
    php bin/console make:entity
  4. Générer la migration
    php bin/console make:migration
  5. Exécuter la migration
    php bin/console doctrine:migrations:migrate
  6. Créer les fixtures (optionnel)
    Éditer src/DataFixtures/AppFixtures.php
  7. Charger les fixtures
    php bin/console doctrine:fixtures:load

Commandes essentielles đŸ› ïž

Commande Description
doctrine:database:create Créer la base de données
doctrine:database:drop --force Supprimer la base
make:entity Créer/modifier une entité
make:migration Générer une migration
doctrine:migrations:migrate Exécuter les migrations
doctrine:migrations:status État des migrations
doctrine:query:sql "SELECT 1" ExĂ©cuter une requĂȘte SQL
doctrine:fixtures:load Charger les fixtures

Concepts clĂ©s Doctrine 📚

Concept Description Utilisation
Entité Classe PHP = Table SQL src/Entity/Article.php
Repository Méthodes de recherche $repo->find(5)
EntityManager Gestionnaire de persistance $em->persist() / flush()
QueryBuilder Constructeur de requĂȘtes ->where()->orderBy()
DQL SQL orienté objet SELECT a FROM Article a
Migration Historique des changements SQL make:migration
Fixtures Données de test doctrine:fixtures:load

Types de relations rĂ©sumĂ©s 🔗

Relation Attribut Exemple
ManyToOne #[ORM\ManyToOne] Articles → CatĂ©gorie
OneToMany #[ORM\OneToMany] CatĂ©gorie → Articles
ManyToMany #[ORM\ManyToMany] Articles ↔ Tags
OneToOne #[ORM\OneToOne] User ↔ Profile

Utilisation

// Accéder à la relation
$article->getCategory()->getName();
$category->getArticles();  // Collection

// Ajouter Ă  une relation ManyToMany
$article->addTag($tag);

// Définir une relation ManyToOne
$article->setCategory($category);

Bonnes pratiques Doctrine 💡

  1. Toujours utiliser les migrations
    Historique traçable, déploiement sûr
  2. Méthodes personnalisées dans le Repository
    Ne pas mettre de requĂȘtes dans les contrĂŽleurs
  3. Utiliser QueryBuilder plutĂŽt que DQL pur
    Plus lisible, moins d'erreurs de typo
  4. Toujours utiliser des paramÚtres préparés
    setParameter() protĂšge contre les injections SQL
  5. Lazy loading des relations
    Doctrine charge les relations seulement quand nécessaire
  6. Un seul flush() par requĂȘte HTTP
    Grouper les persist(), puis un seul flush()
  7. Fixtures pour le développement
    Données réalistes avec Faker

Erreurs courantes Ă  Ă©viter ⚠

❌ Erreur 1 : Oublier flush()

$article = new Article();
$article->setTitle('Test');
$entityManager->persist($article);
// ❌ OubliĂ© flush() → Rien en base !

// ✅ BON
$entityManager->flush();

❌ Erreur 2 : persist() un objet dĂ©jĂ  en base

$article = $repository->find(1);
$article->setTitle('Modifié');
$entityManager->persist($article);  // ❌ Inutile !
$entityManager->flush();

// ✅ BON - pas besoin de persist()
$article->setTitle('Modifié');
$entityManager->flush();

❌ Erreur 3 : Injection SQL

// ❌ DANGEREUX
->where("a.title = '$title'")

// ✅ SÛR
->where('a.title = :title')
->setParameter('title', $title)

Ressources pour aller plus loin 📚

Documentation Doctrine

🌐 https://www.doctrine-project.org/

Documentation complĂšte de Doctrine ORM

Symfony - Doctrine

🌐 https://symfony.com/doc/current/doctrine.html

Intégration Doctrine dans Symfony

DoctrineFixturesBundle

🌐 https://symfony.com/bundles/DoctrineFixturesBundle

Documentation des Fixtures

Faker PHP

🌐 https://fakerphp.github.io/

Générateur de fausses données

Ce que vous maĂźtrisez maintenant ! đŸ’Ș

Compétences acquises :

💡 Prochaine Ă©tape : Les formulaires Symfony !

🎓 FĂ©licitations !

Vous maĂźtrisez Doctrine ORM

Vous savez maintenant :

✅ Mapper des objets PHP vers SQL

✅ CrĂ©er des relations complexes

✅ Construire des requĂȘtes puissantes

✅ GĂ©rer l'Ă©volution de votre schĂ©ma

💡 Conseil final :

Doctrine est puissant mais peut ĂȘtre dĂ©routant au dĂ©but. Pratiquez en crĂ©ant de vraies applications et tout deviendra naturel !

Prochaine Ă©tape : Les Formulaires ! 📝

Formation DWWM - 2026