Gérer la base de données avec des objets PHP
EntitĂ©s âą Relations âą RequĂȘtes âą Migrations
Formation DWWM - 2026
Object-Relational Mapping
ORM = Object-Relational Mapping = Faire correspondre des objets PHP avec des tables SQL.
Imagine que tu parles français et ton ami parle japonais :
$user->getName()SELECT name FROM userTu parles PHP, Doctrine parle SQL pour toi !
// 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]);
// 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 :
| 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 |
Préparer Doctrine
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle
đĄ Que fait cette commande ?
symfony/orm-pack : Doctrine ORM + composants nécessairessymfony/maker-bundle : Générateur de code (entités, migrations...)config/
âââ packages/
â âââ doctrine.yaml # Configuration Doctrine
âââ .env # Variables d'environnement
src/
âââ Entity/ # Dossier des entitĂ©s (vide)
âââ Repository/ # Dossier des repositories (vide)
Modifier le fichier .env pour configurer la connexion :
# .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"
DATABASE_URL="postgresql://utilisateur:motdepasse@127.0.0.1:5432/nom_base?serverVersion=15&charset=utf8"
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.
php bin/console doctrine:database:create
â RĂ©sultat :
Created database `mon_blog` for connection named default
# 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"
Les classes qui représentent vos tables
Une Entité est une classe PHP qui représente une table en base de données.
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é !
php bin/console make:entity
$ 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)
<?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
}
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
Indique que cette classe est une entité Doctrine avec son repository associé.
#[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(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).
| 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 |
| 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])] |
// 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;
Lier les tables entre elles
| 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 |
Cas d'usage : Plusieurs articles appartiennent à UNE catégorie.
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
#[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;
}
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;
}
// 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();
$article = $articleRepository->find(1);
// Accéder à la catégorie
echo $article->getCategory()->getName();
// Dans Twig
{{ article.category.name }}
$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 %}
Cas d'usage : Un article peut avoir plusieurs tags, un tag peut ĂȘtre sur plusieurs articles.
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
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.
$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();
$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 %}
$tag = $tagRepository->findOneBy(['name' => 'PHP']);
foreach ($tag->getArticles() as $article) {
echo $article->getTitle();
}
Cas d'usage : Un utilisateur a UN profil, un profil appartient Ă UN utilisateur.
// 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;
$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();
Sauvegarder et manipuler les données
EntityManager = Le chef d'orchestre de Doctrine. Il gÚre TOUTES les opérations sur la base de données.
Tu prépares plusieurs opérations, puis tu les exécutes TOUTES EN UNE FOIS avec flush() !
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 !
$article = new Article();
$article->setTitle('Titre');
$entityManager->persist($article);
// â L'objet est "trackĂ©" mais PAS ENCORE en base !
$entityManager->flush();
// â MAINTENANT les requĂȘtes SQL sont exĂ©cutĂ©es
On peut préparer 100 objets avec persist(), puis faire UN SEUL flush() = UNE SEULE transaction SQL.
Si une erreur survient, RIEN n'est sauvegardé (tout ou rien).
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();
$article = new Article();
$article->setTitle('Nouveau');
$article->setContent('Contenu');
$entityManager->persist($article);
$entityManager->flush();
// L'ID est maintenant disponible
echo $article->getId();
// Via repository (voir partie suivante)
$article = $articleRepository->find(1);
$article = $articleRepository->find(1);
$article->setTitle('Titre modifié');
// PAS BESOIN de persist() pour un objet déjà en base !
$entityManager->flush();
$article = $articleRepository->find(1);
$entityManager->remove($article);
$entityManager->flush();
Chercher et filtrer les données
Repository = BibliothÚque de méthodes pour chercher des entités.
Le repository = un bibliothécaire qui connaßt parfaitement sa bibliothÚque :
$repository = $entityManager->getRepository(Article::class);
$article = $repository->find(1);
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é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 |
// 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]);
Vous pouvez ajouter vos propres méthodes dans le Repository !
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();
}
}
Construire des requĂȘtes complexes
QueryBuilder = Constructeur de requĂȘtes SQL en PHP orientĂ© objet.
Le QueryBuilder = assembler des briques LEGO pour construire une requĂȘ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
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é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') |
// NE JAMAIS FAIRE ĂA !
$title = $_GET['title'];
->where("a.title = '$title'") // â FAILLE DE SĂCURITĂ !
->where('a.title = :title')
->setParameter('title', $title) // â Protection automatique
->andWhere('a.category = :category')
->andWhere('a.published = :published')
->setParameter('category', $category)
->setParameter('published', true)
// OU avec tableau
->setParameters([
'category' => $category,
'published' => true
])
// 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();
// Articles mĂȘme sans catĂ©gorie
$qb = $repository->createQueryBuilder('a')
->leftJoin('a.category', 'c')
->addSelect('c')
->getQuery()
->getResult();
// Articles avec catégorie ET tags
$qb = $repository->createQueryBuilder('a')
->leftJoin('a.category', 'c')
->leftJoin('a.tags', 't')
->addSelect('c', 't')
->getQuery()
->getResult();
// Nombre d'articles publiés
$count = $repository->createQueryBuilder('a')
->select('COUNT(a.id)')
->where('a.published = true')
->getQuery()
->getSingleScalarResult(); // Retourne un nombre
// Total des vues
$totalViews = $repository->createQueryBuilder('a')
->select('SUM(a.views)')
->getQuery()
->getSingleScalarResult();
// Note moyenne
$avgRating = $repository->createQueryBuilder('a')
->select('AVG(a.rating)')
->getQuery()
->getSingleScalarResult();
// 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();
SQL orienté objet
DQL = Doctrine Query Language = SQL mais avec des noms d'objets au lieu de tables.
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
SELECT a, c
FROM App\Entity\Article a
JOIN a.category c
WHERE a.published = true
ORDER BY a.createdAt DESC
đĄ DiffĂ©rences :
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();
}
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 !
Synchroniser le schéma SQL
Migration = Fichier PHP qui contient les modifications SQL à appliquer à la base de données.
Les migrations = un carnet qui garde l'historique de tous les changements :
On peut revenir en arriĂšre (rollback) ou avancer pas Ă pas !
php bin/console make:entity Article
php bin/console make:migration
â
Résultat : Fichier créé dans migrations/
migrations/Version20250128143000.php
php bin/console doctrine:migrations:migrate
<?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');
}
}
| 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 !
Remplir la base avec des données de test
Fixtures = Données de test qu'on charge dans la base de données pour développer.
Les fixtures = planter des graines dans un jardin vide :
đĄ Pourquoi utiliser des fixtures ?
composer require --dev orm-fixtures
â
Résultat : Dossier src/DataFixtures/ créé avec un exemple.
<?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();
}
}
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
php bin/console doctrine:fixtures:load --no-interaction
php bin/console doctrine:fixtures:load --append
composer require --dev fakerphp/faker
<?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éthode | Description | Exemple |
|---|---|---|
name() |
Nom complet | Jean Dupont |
firstName() |
Prénom | Marie |
lastName() |
Nom | Martin |
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 |
.env avec DATABASE_URLphp bin/console doctrine:database:createphp bin/console make:entityphp bin/console make:migrationphp bin/console doctrine:migrations:migratesrc/DataFixtures/AppFixtures.phpphp bin/console doctrine:fixtures:load| 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 |
| 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 |
| 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 |
// 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);
setParameter() protĂšge contre les injections SQL$article = new Article();
$article->setTitle('Test');
$entityManager->persist($article);
// â OubliĂ© flush() â Rien en base !
// â
BON
$entityManager->flush();
$article = $repository->find(1);
$article->setTitle('Modifié');
$entityManager->persist($article); // â Inutile !
$entityManager->flush();
// â
BON - pas besoin de persist()
$article->setTitle('Modifié');
$entityManager->flush();
// â DANGEREUX
->where("a.title = '$title'")
// â
SĂR
->where('a.title = :title')
->setParameter('title', $title)
đ https://www.doctrine-project.org/
Documentation complĂšte de Doctrine ORM
đ https://symfony.com/doc/current/doctrine.html
Intégration Doctrine dans Symfony
đ https://symfony.com/bundles/DoctrineFixturesBundle
Documentation des Fixtures
đ https://fakerphp.github.io/
Générateur de fausses données
đĄ Prochaine Ă©tape : Les formulaires Symfony !
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