1 / 10
🗄️

Doctrine ORM

Gestion de base de données avec Symfony 6.4

Formation DWWM - Développement Web et Web Mobile

🗄️ Introduction à Doctrine ORM :
Doctrine ORM est un outil de mapping objet-relationnel qui permet de manipuler la base de données via des objets PHP. Il fait le pont entre votre code orienté objet et votre base de données relationnelle.

Plan du Cours

🔧 Fondamentaux

  • Configuration Doctrine
  • Entités et annotations
  • Types de données
  • Migrations

🏗️ Manipulation

  • Entity Manager
  • Repository pattern
  • QueryBuilder
  • Relations entre entités
📚 Objectifs :
Maîtriser Doctrine ORM pour créer des applications robustes avec une base de données bien structurée et des requêtes optimisées.

Configuration de Doctrine

Installation

# Installation de Doctrine ORM
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle

Configuration de la base de données

# .env
DATABASE_URL="mysql://user:password@127.0.0.1:3306/myapp_db?serverVersion=8.0"

# Pour PostgreSQL
DATABASE_URL="postgresql://user:password@127.0.0.1:5432/myapp_db?serverVersion=13"

# Pour SQLite
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
🔧 Configuration expliquée :
DATABASE_URL : Chaîne de connexion à la base de données
auto_mapping : Détection automatique des entités
naming_strategy : Convention de nommage (camelCase → snake_case)
💡 Commandes utiles :
• php bin/console doctrine:database:create
• php bin/console doctrine:database:drop --force
• php bin/console doctrine:query:sql "SELECT * FROM user"

Création d'une Entité

Génération avec Maker Bundle

# Créer une entité User
php bin/console make:entity User

Entité User avec attributs PHP 8

createdAt = new \DateTimeImmutable();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): static
    {
        $this->email = $email;
        return $this;
    }
}
📝 Attributs expliqués :
#[ORM\Entity] : Déclare la classe comme entité Doctrine
#[ORM\Id] : Clé primaire
#[ORM\GeneratedValue] : Auto-increment
#[ORM\Column] : Définit les contraintes de colonne

Types de Données Doctrine

Types courants

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

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

# Nombres
#[ORM\Column]
private ?int $age = null;

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

Types avancés

# Dates
#[ORM\Column]
private ?\DateTime $updatedAt = null;

#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;

# JSON
#[ORM\Column]
private array $roles = [];

# Booléen
#[ORM\Column]
private bool $isActive = true;
🔄 Mapping des types :
string → VARCHAR • text → TEXT
integer → INT • decimal → DECIMAL
datetime → DATETIME • boolean → TINYINT
array/json → JSON (MySQL) ou TEXT (autres)
💡 Bonnes pratiques :
• Utilisez DateTimeImmutable pour les dates de création
• JSON pour les données non-relationnelles
• Définissez la longueur des chaînes
• nullable: true pour les champs optionnels

Migrations avec Doctrine

Génération et exécution

# Créer une migration
php bin/console make:migration

# Exécuter les migrations
php bin/console doctrine:migrations:migrate

# Voir le statut
php bin/console doctrine:migrations:status

# Revenir en arrière
php bin/console doctrine:migrations:migrate prev

Exemple de fichier de migration

addSql('CREATE TABLE user (
            id INT AUTO_INCREMENT NOT NULL, 
            email VARCHAR(180) NOT NULL, 
            name VARCHAR(100) NOT NULL, 
            created_at DATETIME NOT NULL, 
            UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), 
            PRIMARY KEY(id)
        )');
    }

    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE user');
    }
}
🔄 Processus de migration :
1. Modifier l'entité
2. make:migration (génère le fichier)
3. Vérifier le SQL généré
4. migrate (appliquer les changements)
⚠️ Production :
• Toujours utiliser les migrations
• Jamais schema:update --force
• Tester sur une copie avant prod
• Sauvegarder avant migration

Entity Manager

Opérations CRUD de base

setEmail('john@example.com');
        $user->setName('John Doe');
        
        $em->persist($user);
        $em->flush();

        // READ
        $user = $em->getRepository(User::class)->find(1);
        $users = $em->getRepository(User::class)->findAll();

        // UPDATE
        $user->setName('Nouveau nom');
        $em->flush(); // Pas de persist() nécessaire

        // DELETE
        $em->remove($user);
        $em->flush();

        return $this->json(['id' => $user->getId()]);
    }
}
🔄 Unit of Work Pattern :
persist() : Marque l'entité pour insertion
remove() : Marque pour suppression
flush() : Exécute toutes les opérations
clear() : Vide le cache des entités
💡 Bonnes pratiques :
• flush() seulement en fin d'opération
• Un EntityManager par requête HTTP
• clear() pour éviter les fuites mémoire
• Transactions pour opérations complexes

Repository Pattern

Repository personnalisé

findOneBy(['email' => $email]);
    }

    public function findActiveUsers(): array
    {
        return $this->findBy(['isActive' => true]);
    }

    public function findRecentUsers(int $limit = 10): array
    {
        return $this->createQueryBuilder('u')
            ->orderBy('u.createdAt', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }
}

Méthodes de base

# Recherche par ID
$user = $userRepository->find(1);

# Recherche par critères
$user = $userRepository->findOneBy(['email' => 'user@example.com']);
$users = $userRepository->findBy(['isActive' => true]);

# Recherche avec tri et limite
$users = $userRepository->findBy(
    ['isActive' => true],
    ['createdAt' => 'DESC'], 
    10,  // limite
    0    // offset
);

# Compter
$count = $userRepository->count(['isActive' => true]);
📚 Repository expliqué :
Encapsulation : Logique de requête centralisée
Réutilisabilité : Méthodes disponibles partout
Testabilité : Facilite les tests unitaires
Un Repository par entité

QueryBuilder

Requêtes dynamiques

public function findUsersByFilters(array $filters): array
{
    $qb = $this->createQueryBuilder('u');

    if (!empty($filters['name'])) {
        $qb->andWhere('u.name LIKE :name')
           ->setParameter('name', '%' . $filters['name'] . '%');
    }

    if (!empty($filters['email'])) {
        $qb->andWhere('u.email = :email')
           ->setParameter('email', $filters['email']);
    }

    if (isset($filters['isActive'])) {
        $qb->andWhere('u.isActive = :isActive')
           ->setParameter('isActive', $filters['isActive']);
    }

    return $qb->orderBy('u.createdAt', 'DESC')
              ->getQuery()
              ->getResult();
}

Jointures et agrégations

public function findUsersWithArticleCount(): array
{
    return $this->createQueryBuilder('u')
        ->select('u', 'COUNT(a.id) as articleCount')
        ->leftJoin('u.articles', 'a')
        ->groupBy('u.id')
        ->having('articleCount > 0')
        ->orderBy('articleCount', 'DESC')
        ->getQuery()
        ->getResult();
}
🔧 QueryBuilder méthodes :
select() : Colonnes à récupérer
where() : Conditions
join() : Jointures
orderBy() : Tri
setParameter() : Paramètres sécurisés
💡 Optimisations :
• Utilisez select() pour limiter les colonnes
• Paramètres liés pour la sécurité
• setMaxResults() pour la pagination
• Index sur colonnes WHERE/JOIN

Relations et Bonnes Pratiques

Relations OneToMany / ManyToOne

# Entity/Article.php
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;

# Entity/User.php
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class)]
private Collection $articles;

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

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

Checklist bonnes pratiques

🏗️ Structure

  • Une entité = Une responsabilité
  • Repository pour les requêtes
  • Services pour la logique métier
  • Migrations versionnées

⚡ Performance

  • JOIN fetch contre N+1
  • Pagination sur gros volumes
  • Index sur colonnes recherchées
  • Cache pour requêtes fréquentes
🏆 Principes fondamentaux :
Sécurité : Paramètres liés, validation, gestion d'erreurs
Performance : Optimisations dès la conception
Maintenabilité : Code propre et testé
Doctrine = Pont entre objets et base de données
⚠️ Erreurs courantes :
• Logique métier dans les entités
• Oublier les migrations en équipe
• Requêtes non optimisées
• Mauvaise gestion des relations

🗄️ Félicitations !

Vous maîtrisez maintenant Doctrine ORM !

Créez des applications robustes avec une base de données bien structurée 🚀