Updated June 2026. Tested on Symfony 7.x and Doctrine ORM 3. Part of the Techalyst Symfony series.

If the EntityManager is how data goes into the database, repositories are how it comes back out. Every entity has a repository, a class whose job is to fetch instances of that entity. For simple lookups you get methods for free, and for anything custom you write query methods using the query builder. Keeping your queries in the repository, rather than scattered through controllers, is one of the habits that keeps a Symfony app clean.

The finders you get for free

Every repository comes with four lookup methods, no code required:

$products->find(3);                              // by primary key, or null
$products->findOneBy(['slug' => 'keyboard']);    // first match for criteria, or null
$products->findBy(['active' => true], ['price' => 'ASC'], 10); // criteria, order, limit
$products->findAll();                            // everything

You get the repository by injecting it, type-hinting ProductRepository in a controller action or service the same way as any other dependency. These four cover a surprising amount of everyday fetching.

Custom queries belong in the repository

When a lookup is more involved than findBy can express, you add a method to the repository and build the query there. That keeps the query named and reusable instead of inline in a controller:

class ProductRepository extends ServiceEntityRepository
{
    public function findActiveInCategory(string $category): array
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.active = :active')
            ->andWhere('p.category = :category')
            ->setParameter('active', true)
            ->setParameter('category', $category)
            ->orderBy('p.price', 'ASC')
            ->getQuery()
            ->getResult();
    }
}

createQueryBuilder('p') starts a query with p as the alias for the entity. You chain andWhere, orderBy, setMaxResults and the rest to build it up, then getQuery() and a result method run it. The controller just calls $products->findActiveInCategory('keyboards') and gets back an array of Product objects.

Getting the right shape of result

The result method you call decides what you get back:

->getResult();            // an array of entities
->getOneOrNullResult();   // a single entity or null
->getScalarResult();      // raw scalar rows, for aggregates

Use getOneOrNullResult when you expect at most one row, and getResult for a list. For a count or a sum you would select the aggregate and read it as a scalar.

Always use parameters

Notice that values go in through setParameter, never concatenated into the query string. This is not optional. Building a query by gluing user input into the string is how SQL injection happens. The query builder's parameters are escaped safely, so make :placeholder plus setParameter your only way to pass values into a query.

Wrapping up

Repositories are where fetching lives in a Symfony app. The built-in find, findOneBy, findBy, and findAll handle simple lookups straight away, and for anything custom you add a named method to the repository built with createQueryBuilder, choosing getResult or getOneOrNullResult for the shape you want. Always pass values with setParameter so queries stay injection-safe. Keep queries in the repository where they are named and testable, and your controllers stay thin.