Extbase repository 

A repository is the only entry point to the database for a given model type. Controllers and services ask a repository for objects — they must not query the database directly. This keeps persistence logic in one place and makes controllers easier to test.

Every Extbase extension has one repository per model. The repository class often only needs to exist and requires not a single line of custom code.

Repositories are registered as shared services in the Dependency injection container. That means every consumer that injects a given repository within the same request receives the same instance — query settings configured on it in one place apply everywhere it is used.

The minimal Extbase repository 

A repository extends \TYPO3\CMS\Extbase\Persistence\Repository . The class name must follow the convention: the model class \Domain\Model\Conference maps to \Domain\Repository\ConferenceRepository. Extbase resolves this automatically.

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Extbase\Persistence\Repository;

class ConferenceRepository extends Repository {}
Copied!

That empty class already provides all the standard methods described below.

Built-in find methods in Extbase repositories 

Repository provides these methods for finding, returning, and counting domain objects out of the box:

findAll()

findAll()
Type
QueryResultInterface

Returns all records from the repository's storage page(s).

findByUid(int $uid)

findByUid(int $uid)
Type
object|null

Returns the object with the given UID, or null. Ignores storagePid — always searches across all pages.

findByIdentifier(mixed $identifier)

findByIdentifier(mixed $identifier)
Type
object|null

Alias for findByUid() — the identifier is the UID.

findBy(array $criteria, ?array $orderBy, ?int $limit, ?int $offset)

findBy(array $criteria, ?array $orderBy, ?int $limit, ?int $offset)
Type
QueryResultInterface

Finds all objects matching the given criteria array. Example: findBy(['published' => true]).

findOneBy(array $criteria, ?array $orderBy)

findOneBy(array $criteria, ?array $orderBy)
Type
object|null

Returns the first object matching the criteria, or null.

count(array $criteria)

count(array $criteria)
Type
int

Returns the number of matching objects without loading them.

countAll()

countAll()
Type
int

Returns the total number of objects in the repository.

Changed in version 14.0

Ordering results in Extbase repositories 

There are two distinct places to define ordering, and they serve different purposes.

Repository-wide default ordering applies to every query from this repository — findAll(), findBy(), and custom methods that do not override it. Set the $defaultOrderings class property:

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;

class ConferenceRepository extends Repository
{
    protected $defaultOrderings = [
        'conferenceDate' => QueryInterface::ORDER_ASCENDING,
        'title'     => QueryInterface::ORDER_ASCENDING,
    ];
}
Copied!

Method-level ordering applies only to the query built in that method, overriding the default for that call. Use $query->setOrderings():

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
public function findUpcomingByTitle(): QueryResultInterface
{
    $query = $this->createQuery();
    $query->matching(
        $query->greaterThanOrEqual('conferenceDate', new \DateTimeImmutable('today'))
    );
    $query->setOrderings(['title' => QueryInterface::ORDER_ASCENDING]);
    return $query->execute();
}
Copied!

In both cases the keys are property names, not column names. Order direction is QueryInterface::ORDER_ASCENDING ( 'ASC') or QueryInterface::ORDER_DESCENDING ( 'DESC').

If neither $defaultOrderings nor a method-level setOrderings() is set, no ORDER BY clause is added and the database returns rows in an undefined order. This may appear consistent in development but is not guaranteed — the order can change after inserts, updates, or database maintenance. Always define an explicit ordering for any query whose result order matters to the user.

Ordering in Extbase relations 

The TCA ctrl settings default_sortby and sortby are not applied to repository queries — Extbase does not read them for top-level queries. They do influence the order of child records within relations (via foreign_sortby / foreign_default_sortby), but for any direct repository query the ordering is entirely your responsibility.

Custom query methods in Extbase repositories 

Use createQuery() when the built-in find methods are not enough:

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use MyVendor\MyExtension\Domain\Model\Conference;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;

class ConferenceRepository extends Repository
{
    protected $defaultOrderings = [
        'conferenceDate' => QueryInterface::ORDER_ASCENDING,
    ];

    /** @return QueryResultInterface<Conference> */
    public function findUpcoming(): QueryResultInterface
    {
        $query = $this->createQuery();
        $query->matching(
            $query->greaterThanOrEqual('conferenceDate', new \DateTimeImmutable('today')),
        );
        return $query->execute();
    }

    /** @return QueryResultInterface<Conference> */
    public function findByTitleContaining(string $search): QueryResultInterface
    {
        $query = $this->createQuery();
        $query->matching(
            $query->like('title', '%' . $search . '%'),
        );
        $query->setOrderings(['title' => QueryInterface::ORDER_ASCENDING]);
        return $query->execute();
    }
}
Copied!

The query <\TYPO3\CMS\Extbase\Persistence\QueryInterface> API supports these constraint methods:

equals(string $property, mixed $value)

equals(string $property, mixed $value)

Exact match. Generates a SQL = comparison.

like(string $property, string $value)

like(string $property, string $value)

Pattern match. Generates SQL LIKE. Use % as wildcard, for example '%search%'.

lessThan(string $property, mixed $value)

lessThan(string $property, mixed $value)

Generates SQL <.

lessThanOrEqual(string $property, mixed $value)

lessThanOrEqual(string $property, mixed $value)

Generates SQL <=.

greaterThan(string $property, mixed $value)

greaterThan(string $property, mixed $value)

Generates SQL >.

greaterThanOrEqual(string $property, mixed $value)

greaterThanOrEqual(string $property, mixed $value)

Generates SQL >=.

in(string $property, array $values)

in(string $property, array $values)

Matches any value in the given array. Generates SQL IN (...).

contains(string $property, object $value)

contains(string $property, object $value)

Checks whether an ObjectStorage relation contains the given object.

logicalAnd(mixed ...$constraints)

logicalAnd(mixed ...$constraints)

Combines multiple constraints with AND.

logicalOr(mixed ...$constraints)

logicalOr(mixed ...$constraints)

Combines multiple constraints with OR.

logicalNot(ConstraintInterface $constraint)

logicalNot(ConstraintInterface $constraint)

Negates a constraint with NOT.

Chain multiple constraints:

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
$query->matching(
    $query->logicalAnd(
        $query->equals('published', true),
        $query->greaterThanOrEqual('conferenceDate', new \DateTimeImmutable('today')),
    )
);
Copied!

storagePid — when findAll() returns nothing 

Every repository query (except findByUid()) is filtered to one or more storage pages by default. If findAll() returns an empty result and records clearly exist in the database, or if there are unexpected objects in the result, the most likely cause is a missing or misconfigured storagePid.

Configure it for example in TypoScript:

EXT:my_extension/Configuration/Sets/MyExtension/setup.typoscript
plugin.tx_myextension.persistence.storagePid = 42
Copied!

Injecting repositories with dependency injection 

Inject the repository via constructor injection in your controller or service. Do not use GeneralUtility::makeInstance() — it bypasses the Bootstrap procedure applied by extbase, so any query settings configured on the shared instance are lost and the repository is not wired with its own injected dependencies:

EXT:my_extension/Classes/Controller/ConferenceController.php
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ConferenceController extends ActionController
{
    public function __construct(
        protected readonly ConferenceRepository $conferenceRepository,
    ) {}

    public function listAction(): ResponseInterface
    {
        $this->view->assign('conferences', $this->conferenceRepository->findAll());
        return $this->htmlResponse();
    }
}
Copied!

TYPO3's DI container resolves the repository automatically. No @inject annotation, no factory call.

When to drop out of the ORM (Object Relational Mapping) 

The Extbase query API covers most common patterns. Use raw DBAL — TYPO3's database layer built on top of Doctrine DBAL — when you need:

  • Aggregate functions ( SUM, AVG, GROUP BY)
  • Bulk inserts or updates across many records
  • Complex multi-table joins that the ORM cannot express
  • Performance-critical queries where loading full objects is wasteful

While you can technically access the database directly from controllers or services, you should limit raw DBAL usage to repository classes. Spreading database calls across controllers and services makes the code harder to test, harder to change the underlying query, and harder to enforce consistent filters (such as storagePid or language overlay).

Access ConnectionPool from within the repository:

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Extbase\Persistence\Repository;

class ConferenceRepository extends Repository
{
    public function __construct(
        protected readonly ConnectionPool $connectionPool,
    ) {
        parent::__construct();
    }

    public function countByYear(int $year): array
    {
        $connection = $this->connectionPool->getConnectionForTable('tx_myextension_domain_model_conference');
        // ... build and execute raw query
    }
}
Copied!