Doctrine ORM : Maîtriser les relations en Symfony

Guide complet des relations Doctrine ORM dans Symfony. OneToMany, ManyToMany, stratégies de chargement et optimisation des performances avec exemples pratiques.

Doctrine ORM relations dans Symfony - Guide complet

Les relations entre entités constituent le cœur de toute application Symfony utilisant Doctrine ORM. Une maîtrise solide des différents types de relations, des stratégies de chargement et des pièges de performance permet de construire des applications robustes et performantes. Ce guide couvre les patterns essentiels pour gérer efficacement les relations Doctrine.

Principe fondamental

Doctrine utilise le concept d'entités propriétaires (owning side) et inversées (inverse side). Comprendre cette distinction évite de nombreux bugs liés à la persistance des relations.

Les types de relations Doctrine

Doctrine propose quatre types de relations entre entités : OneToOne, OneToMany, ManyToOne et ManyToMany. Chaque type répond à un besoin métier spécifique et possède ses propres caractéristiques de performance.

Relation ManyToOne : le cas le plus courant

La relation ManyToOne représente le lien le plus fréquent en base de données. Plusieurs entités d'un côté se rattachent à une seule entité de l'autre côté. Cette relation est toujours propriétaire de l'association.

src/Entity/Comment.phpphp
// Un commentaire appartient à un seul article
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

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

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

    // ManyToOne : plusieurs commentaires → un article
    // Cette entité est propriétaire de la relation (clé étrangère ici)
    #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')]
    #[ORM\JoinColumn(nullable: false)] // La relation est obligatoire
    private Article $article;

    #[ORM\Column]
    private \DateTimeImmutable $createdAt;

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    public function getArticle(): Article
    {
        return $this->article;
    }

    public function setArticle(Article $article): self
    {
        $this->article = $article;
        return $this;
    }
}

L'entité Comment possède la clé étrangère article_id. Doctrine gère automatiquement la persistance : modifier $comment->setArticle($article) suffit pour créer le lien en base.

Relation OneToMany : le côté inverse

La relation OneToMany représente le côté inverse d'une ManyToOne. Elle permet de naviguer depuis l'entité "un" vers les "plusieurs". Cette relation n'est jamais propriétaire et ne contient pas de clé étrangère.

src/Entity/Article.phpphp
// Un article possède plusieurs commentaires
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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;

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

    // OneToMany : un article → plusieurs commentaires
    // Côté inverse : mappedBy pointe vers la propriété de l'entité propriétaire
    #[ORM\OneToMany(
        targetEntity: Comment::class,
        mappedBy: 'article',
        cascade: ['persist', 'remove'], // Cascade les opérations
        orphanRemoval: true              // Supprime les commentaires orphelins
    )]
    private Collection $comments;

    public function __construct()
    {
        // Initialiser la collection dans le constructeur
        $this->comments = new ArrayCollection();
    }

    /**
     * @return Collection<int, Comment>
     */
    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function addComment(Comment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments->add($comment);
            // CRITIQUE : synchroniser le côté propriétaire
            $comment->setArticle($this);
        }
        return $this;
    }

    public function removeComment(Comment $comment): self
    {
        if ($this->comments->removeElement($comment)) {
            // orphanRemoval gère la suppression
        }
        return $this;
    }
}

Les méthodes addComment() et removeComment() assurent la cohérence bidirectionnelle. Sans la ligne $comment->setArticle($this), la relation ne serait pas persistée correctement.

Piège fréquent

Oublier de synchroniser les deux côtés d'une relation bidirectionnelle est la source d'erreur la plus courante. Toujours modifier le côté propriétaire pour garantir la persistance.

Relation ManyToMany : associations multiples

La relation ManyToMany connecte plusieurs entités des deux côtés. Doctrine crée automatiquement une table de jointure. Un côté doit être désigné comme propriétaire via l'attribut inversedBy.

src/Entity/Article.phpphp
// Un article peut avoir plusieurs tags
namespace App\Entity;

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

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
    // ... autres propriétés

    // ManyToMany : côté propriétaire (inversedBy)
    // Table de jointure : article_tag (générée automatiquement)
    #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
    #[ORM\JoinTable(name: 'article_tag')] // Nom explicite de la table
    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): self
    {
        if (!$this->tags->contains($tag)) {
            $this->tags->add($tag);
            // Côté propriétaire : pas besoin de synchroniser l'autre côté
            // pour la persistance, mais recommandé pour la cohérence mémoire
            $tag->addArticle($this);
        }
        return $this;
    }

    public function removeTag(Tag $tag): self
    {
        if ($this->tags->removeElement($tag)) {
            $tag->removeArticle($this);
        }
        return $this;
    }
}
src/Entity/Tag.phpphp
// Un tag peut appartenir à plusieurs articles
namespace App\Entity;

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

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

    #[ORM\Column(length: 50, unique: true)]
    private string $name;

    // Côté inverse : mappedBy pointe vers le propriétaire
    #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'tags')]
    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): self
    {
        if (!$this->articles->contains($article)) {
            $this->articles->add($article);
        }
        return $this;
    }

    public function removeArticle(Article $article): self
    {
        $this->articles->removeElement($article);
        return $this;
    }
}

Pour une relation ManyToMany avec des données supplémentaires (date d'association, ordre, etc.), convertir en deux relations ManyToOne vers une entité intermédiaire.

Prêt à réussir tes entretiens Symfony ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Stratégies de chargement et performance

Le chargement des relations constitue le point critique de performance avec Doctrine. Trois stratégies existent : LAZY (par défaut), EAGER et EXTRA_LAZY.

Lazy Loading : chargement à la demande

Le lazy loading charge les relations uniquement lors de leur premier accès. Cette stratégie par défaut évite les requêtes inutiles mais peut créer le problème N+1.

src/Repository/ArticleRepository.phpphp
// Démonstration du problème N+1
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);
    }

    // PROBLÈME N+1 : une requête par article pour les commentaires
    public function findAllWithLazyComments(): array
    {
        // Requête 1 : SELECT * FROM article
        $articles = $this->findAll();

        // Dans la vue ou le service :
        // foreach ($articles as $article) {
        //     $article->getComments(); // Requête N : SELECT * FROM comment WHERE article_id = ?
        // }

        return $articles;
    }
}

Pour 100 articles, ce code génère 101 requêtes SQL : une pour les articles, puis une par article pour charger les commentaires.

Eager Loading avec jointures

Le chargement anticipé résout le problème N+1 en récupérant les relations dans la même requête via des jointures.

src/Repository/ArticleRepository.phpphp
// Chargement optimisé avec jointures
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);
    }

    // SOLUTION : jointure explicite avec fetch join
    public function findAllWithComments(): array
    {
        return $this->createQueryBuilder('a')
            // LEFT JOIN charge les articles même sans commentaires
            ->leftJoin('a.comments', 'c')
            // addSelect inclut les commentaires dans le résultat
            ->addSelect('c')
            // Tri par date de création des commentaires
            ->orderBy('c.createdAt', 'DESC')
            ->getQuery()
            ->getResult();
        // Une seule requête SQL avec JOIN
    }

    // Chargement de plusieurs relations
    public function findAllWithCommentsAndTags(): array
    {
        return $this->createQueryBuilder('a')
            ->leftJoin('a.comments', 'c')
            ->addSelect('c')
            ->leftJoin('a.tags', 't')
            ->addSelect('t')
            ->leftJoin('c.author', 'ca') // Auteur du commentaire
            ->addSelect('ca')
            ->getQuery()
            ->getResult();
    }
}

L'utilisation de addSelect() après chaque leftJoin() est essentielle. Sans addSelect(), Doctrine effectue la jointure mais ne charge pas les entités liées.

Extra Lazy : optimisation des grandes collections

La stratégie EXTRA_LAZY optimise les opérations sur les grandes collections sans charger tous les éléments.

src/Entity/Category.phpphp
// Catégorie avec potentiellement des milliers de produits
namespace App\Entity;

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

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

    #[ORM\Column(length: 100)]
    private string $name;

    // EXTRA_LAZY : optimise count(), contains(), slice()
    #[ORM\OneToMany(
        targetEntity: Product::class,
        mappedBy: 'category',
        fetch: 'EXTRA_LAZY' // Ne charge pas tous les produits
    )]
    private Collection $products;

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

    // count() exécute SELECT COUNT(*) au lieu de charger la collection
    public function getProductCount(): int
    {
        return $this->products->count();
        // SQL : SELECT COUNT(*) FROM product WHERE category_id = ?
    }

    // contains() vérifie l'existence sans tout charger
    public function hasProduct(Product $product): bool
    {
        return $this->products->contains($product);
        // SQL : SELECT 1 FROM product WHERE id = ? AND category_id = ?
    }

    // slice() charge uniquement une portion
    public function getRecentProducts(int $limit = 5): array
    {
        return $this->products->slice(0, $limit);
        // SQL : SELECT * FROM product WHERE category_id = ? LIMIT 5
    }
}

EXTRA_LAZY évite de charger des milliers d'entités pour une simple vérification ou un comptage.

Quand utiliser EXTRA_LAZY

Appliquer EXTRA_LAZY aux collections potentiellement volumineuses (> 100 éléments) où les opérations count(), contains() ou slice() sont fréquentes.

Cascade et gestion du cycle de vie

Les options de cascade automatisent la propagation des opérations sur les entités liées. Trois options principales : persist, remove et orphanRemoval.

Cascade Persist : persistance automatique

La cascade persist enregistre automatiquement les nouvelles entités liées lors du flush.

src/Entity/Order.phpphp
// Commande avec lignes de commande en cascade
namespace App\Entity;

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

#[ORM\Entity(repositoryClass: OrderRepository::class)]
#[ORM\Table(name: '`order`')] // order est un mot réservé SQL
class Order
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 50)]
    private string $reference;

    // cascade persist : les OrderLine sont persistées avec Order
    #[ORM\OneToMany(
        targetEntity: OrderLine::class,
        mappedBy: 'order',
        cascade: ['persist'] // Persiste automatiquement les lignes
    )]
    private Collection $lines;

    public function __construct()
    {
        $this->lines = new ArrayCollection();
        $this->reference = 'ORD-' . uniqid();
    }

    public function addLine(OrderLine $line): self
    {
        if (!$this->lines->contains($line)) {
            $this->lines->add($line);
            $line->setOrder($this);
        }
        return $this;
    }
}
src/Service/OrderService.phpphp
// Création de commande avec cascade persist
namespace App\Service;

use App\Entity\Order;
use App\Entity\OrderLine;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;

class OrderService
{
    public function __construct(
        private readonly EntityManagerInterface $em
    ) {}

    public function createOrder(array $cartItems): Order
    {
        $order = new Order();

        foreach ($cartItems as $item) {
            $line = new OrderLine();
            $line->setProduct($item['product']);
            $line->setQuantity($item['quantity']);
            $line->setUnitPrice($item['product']->getPrice());

            // addLine synchronise la relation
            $order->addLine($line);
        }

        // Seul Order est persisté explicitement
        // Les OrderLine sont persistées automatiquement (cascade)
        $this->em->persist($order);
        $this->em->flush();

        return $order;
    }
}

Cascade Remove et OrphanRemoval

La cascade remove supprime les entités liées. OrphanRemoval va plus loin en supprimant les entités détachées de la collection.

src/Entity/BlogPost.phpphp
// Article de blog avec images
namespace App\Entity;

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

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

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

    #[ORM\OneToMany(
        targetEntity: Image::class,
        mappedBy: 'blogPost',
        cascade: ['persist', 'remove'], // Supprime les images avec l'article
        orphanRemoval: true              // Supprime aussi les images détachées
    )]
    private Collection $images;

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

    public function removeImage(Image $image): self
    {
        if ($this->images->removeElement($image)) {
            // orphanRemoval : l'image sera supprimée au flush
            // Sans orphanRemoval : l'image resterait en base sans blogPost
        }
        return $this;
    }

    public function clearImages(): self
    {
        // Vide la collection → toutes les images seront supprimées
        $this->images->clear();
        return $this;
    }
}

La différence clé : cascade: ['remove'] supprime les entités liées uniquement lors de la suppression de l'entité parente. orphanRemoval: true supprime également les entités retirées de la collection.

Requêtes DQL avancées pour les relations

Le DQL (Doctrine Query Language) offre une flexibilité maximale pour les requêtes impliquant des relations complexes.

Filtrage sur les relations

src/Repository/ArticleRepository.phpphp
// Requêtes avancées sur les relations
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);
    }

    // Articles avec au moins un commentaire
    public function findWithComments(): array
    {
        return $this->createQueryBuilder('a')
            ->innerJoin('a.comments', 'c') // INNER JOIN exclut les articles sans commentaires
            ->addSelect('c')
            ->groupBy('a.id')
            ->getQuery()
            ->getResult();
    }

    // Articles par tag avec comptage de commentaires
    public function findByTagWithCommentCount(string $tagName): array
    {
        return $this->createQueryBuilder('a')
            ->select('a', 'COUNT(c.id) as commentCount')
            ->leftJoin('a.comments', 'c')
            ->innerJoin('a.tags', 't')
            ->where('t.name = :tagName')
            ->setParameter('tagName', $tagName)
            ->groupBy('a.id')
            ->orderBy('commentCount', 'DESC')
            ->getQuery()
            ->getResult();
    }

    // Articles récents avec auteurs des commentaires
    public function findRecentWithAuthors(\DateTimeInterface $since): array
    {
        return $this->createQueryBuilder('a')
            ->leftJoin('a.comments', 'c')
            ->addSelect('c')
            ->leftJoin('c.author', 'u') // Jointure sur l'auteur du commentaire
            ->addSelect('u')
            ->where('a.publishedAt > :since')
            ->setParameter('since', $since)
            ->orderBy('a.publishedAt', 'DESC')
            ->getQuery()
            ->getResult();
    }
}

Sous-requêtes et agrégations

src/Repository/UserRepository.phpphp
// Requêtes complexes avec sous-requêtes
namespace App\Repository;

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

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

    // Utilisateurs les plus actifs (par nombre d'articles)
    public function findMostActiveAuthors(int $limit = 10): array
    {
        return $this->createQueryBuilder('u')
            ->select('u', 'COUNT(a.id) as articleCount')
            ->leftJoin('u.articles', 'a')
            ->where('a.status = :published')
            ->setParameter('published', 'published')
            ->groupBy('u.id')
            ->having('COUNT(a.id) > 0') // HAVING pour filtrer après GROUP BY
            ->orderBy('articleCount', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    // Utilisateurs avec articles ayant plus de 10 commentaires
    public function findAuthorsWithPopularArticles(): array
    {
        // Sous-requête DQL
        $em = $this->getEntityManager();

        $subQuery = $em->createQueryBuilder()
            ->select('IDENTITY(a2.author)')
            ->from('App\Entity\Article', 'a2')
            ->leftJoin('a2.comments', 'c2')
            ->groupBy('a2.id')
            ->having('COUNT(c2.id) > 10')
            ->getDQL();

        return $this->createQueryBuilder('u')
            ->where('u.id IN (' . $subQuery . ')')
            ->getQuery()
            ->getResult();
    }
}

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Bonnes pratiques et patterns avancés

Initialisation correcte des collections

Toujours initialiser les collections dans le constructeur pour éviter les erreurs de type.

src/Entity/Author.phpphp
// Initialisation propre des relations
namespace App\Entity;

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

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

    #[ORM\OneToMany(targetEntity: Book::class, mappedBy: 'author')]
    private Collection $books;

    #[ORM\ManyToMany(targetEntity: Genre::class)]
    private Collection $favoriteGenres;

    // TOUJOURS initialiser dans le constructeur
    public function __construct()
    {
        $this->books = new ArrayCollection();
        $this->favoriteGenres = new ArrayCollection();
    }

    // Méthode utilitaire pour vérifier si la collection est chargée
    public function areBooksLoaded(): bool
    {
        return $this->books->isInitialized();
    }
}

Éviter les références circulaires

Les relations bidirectionnelles peuvent créer des boucles infinies lors de la sérialisation.

src/Entity/Department.phpphp
// Gestion des références circulaires
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;

#[ORM\Entity(repositoryClass: DepartmentRepository::class)]
class Department
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['department:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    #[Groups(['department:read', 'employee:read'])]
    private string $name;

    // MaxDepth limite la profondeur de sérialisation
    #[ORM\OneToMany(targetEntity: Employee::class, mappedBy: 'department')]
    #[Groups(['department:read'])]
    #[MaxDepth(1)] // Ne sérialise pas les relations des employés
    private Collection $employees;

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

Pattern Repository avec critères dynamiques

src/Repository/ProductRepository.phpphp
// Critères de recherche flexibles
namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

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

    // Recherche avec filtres optionnels
    public function findByCriteria(array $criteria): array
    {
        $qb = $this->createQueryBuilder('p')
            ->leftJoin('p.category', 'c')
            ->addSelect('c')
            ->leftJoin('p.tags', 't')
            ->addSelect('t');

        // Filtres conditionnels
        if (isset($criteria['category'])) {
            $qb->andWhere('c.slug = :category')
               ->setParameter('category', $criteria['category']);
        }

        if (isset($criteria['minPrice'])) {
            $qb->andWhere('p.price >= :minPrice')
               ->setParameter('minPrice', $criteria['minPrice']);
        }

        if (isset($criteria['maxPrice'])) {
            $qb->andWhere('p.price <= :maxPrice')
               ->setParameter('maxPrice', $criteria['maxPrice']);
        }

        if (isset($criteria['tags']) && is_array($criteria['tags'])) {
            $qb->andWhere('t.name IN (:tags)')
               ->setParameter('tags', $criteria['tags']);
        }

        if (isset($criteria['inStock']) && $criteria['inStock']) {
            $qb->andWhere('p.stock > 0');
        }

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

Conclusion

La maîtrise des relations Doctrine ORM repose sur quelques principes fondamentaux :

Côté propriétaire vs inverse : toujours modifier le côté propriétaire pour garantir la persistance

Synchronisation bidirectionnelle : les méthodes add/remove doivent synchroniser les deux côtés

Fetch joins : utiliser addSelect() après chaque jointure pour éviter le problème N+1

EXTRA_LAZY : activer sur les collections volumineuses pour optimiser count() et contains()

Cascade avec précaution : persist souvent utile, remove et orphanRemoval selon le contexte métier

Initialisation des collections : toujours dans le constructeur avec ArrayCollection

Ces patterns constituent la base d'une application Symfony performante. L'étape suivante consiste à maîtriser les index et les requêtes natives pour les cas de performance extrême.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#doctrine
#symfony
#orm
#php
#database

Partager

Articles similaires