Repositories

The generated repository class, the finder methods it gives you, and why you inject a repository instead of the entity manager.

Syntax$repository->findBy(['status' => 'published'], ['publishedAt' => 'DESC'], 10, 0)

A repository is a class with one job: fetching entities of a single type. Every query about Article lives in ArticleRepository and nowhere else. The generator writes the class; you add methods to it as the application needs them.

The generated class extends ServiceEntityRepository. Its constructor takes a ManagerRegistry — the object that knows which entity manager is responsible for which entity — and passes the entity class name up to the parent. That one line is what binds the repository to Article.

The finders you get for free

  • find($id) returns one entity by primary key, or null.
  • findOneBy(['slug' => $slug]) returns the first match, or null.
  • findBy($criteria, $orderBy, $limit, $offset) returns an array of matches.
  • findAll() returns every row, which is a trap on a large table.
  • count(['status' => 'published']) returns an integer without loading anything.

Inject the repository, not the entity manager

The generated repository is registered as a service, so you type-hint it in a constructor and autowiring supplies it. A controller that needs articles asks for ArticleRepository. A controller holding EntityManagerInterface can query anything, and that is how query strings end up copied across half a dozen controllers.

Shorthand and longhand

$this->createQueryBuilder('a') is shorthand for $this->getEntityManager()->createQueryBuilder()->select('a')->from(Article::class, 'a'). Same object, same query.

What trips people up: criteria keys are entity property names, not column names. publishedAt, never published_at.

Example

Example · php
<?php
// src/Repository/ArticleRepository.php

declare(strict_types=1);

namespace App\Repository;

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

/**
 * @extends ServiceEntityRepository<Article>
 */
class ArticleRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        // The second argument is what ties this repository to one entity class.
        parent::__construct($registry, Article::class);
    }

    public function findOneBySlug(string $slug): ?Article
    {
        return $this->findOneBy(['slug' => $slug]);
    }

    /**
     * @return Article[]
     */
    public function findPublished(int $limit = 10): array
    {
        // Keys are property names on Article, not database columns.
        return $this->findBy(
            ['status' => 'published'],
            ['publishedAt' => 'DESC'],
            $limit,
        );
    }

    public function countPublished(): int
    {
        return $this->count(['status' => 'published']);
    }
}

When to use it

  • Load one article by its slug for a show page and send a 404 when there is none.
  • Give an admin dashboard a published-article count without loading the rows.
  • Keep the definition of "published" in one repository method instead of in every controller that lists articles.

More examples

Injecting the repository into a controller

The repository arrives through the constructor, type-hinted on the class, so the controller cannot query anything else. GET /articles/first-post renders the template with status 200; an unknown slug produces a 404 page. The Route attribute comes from Symfony\Component\Routing\Attribute\Route, which is the namespace used since Symfony 6.4.

Example · php
<?php
// src/Controller/ArticleController.php

declare(strict_types=1);

namespace App\Controller;

use App\Repository\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ArticleController extends AbstractController
{
    public function __construct(
        private readonly ArticleRepository $articles,
    ) {
    }

    #[Route('/articles/{slug}', name: 'app_article_show', methods: ['GET'])]
    public function show(string $slug): Response
    {
        $article = $this->articles->findOneBySlug($slug);

        if (null === $article) {
            throw $this->createNotFoundException('No article with that slug.');
        }

        return $this->render('article/show.html.twig', ['article' => $article]);
    }
}

The same query written short and long

Both methods build the identical query. The short form is what you write; the long form is what you will meet in code that builds a query against several entities at once, or outside a repository. Recognising them as the same thing means you can move between them without re-reading the manual.

Example · php
// src/Repository/ArticleRepository.php (two methods on the generated class)

/**
 * @return Article[]
 */
public function findPublishedShort(): array
{
    // createQueryBuilder('a') already knows the entity and sets the alias.
    return $this->createQueryBuilder('a')
        ->andWhere('a.status = :status')
        ->setParameter('status', 'published')
        ->getQuery()
        ->getResult();
}

/**
 * @return Article[]
 */
public function findPublishedLong(): array
{
    // Exactly what the short form expands to.
    return $this->getEntityManager()
        ->createQueryBuilder()
        ->select('a')
        ->from(Article::class, 'a')
        ->andWhere('a.status = :status')
        ->setParameter('status', 'published')
        ->getQuery()
        ->getResult();
}

When findBy stops being enough

findBy matches on equality, so a date range is where it runs out. Keep the method on the repository rather than building the query in the controller: the next feature that needs recent articles then calls one method instead of copying five lines.

Example · php
// src/Repository/ArticleRepository.php (method on the generated class)

/**
 * findBy can only compare a property to a value or a list of values.
 * A range, a LIKE, or an OR group needs a query.
 *
 * @return Article[]
 */
public function findPublishedSince(\DateTimeImmutable $since): array
{
    return $this->createQueryBuilder('a')
        ->andWhere('a.status = :status')
        ->andWhere('a.publishedAt >= :since')
        ->setParameter('status', 'published')
        ->setParameter('since', $since)
        ->orderBy('a.publishedAt', 'DESC')
        ->getQuery()
        ->getResult();
}

Discussion

  • Be the first to comment on this lesson.