Extbase repository 

A repository is the only entry point to the database for any 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 therefore will not require any 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.

A minimal Extbase repository 

A repository extends \TYPO3\CMS\Extbase\Persistence\Repository . The class name must follow the convention that 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!

This empty class will provide all the standard methods described below without needing any extra code.

Built-in find methods in Extbase repositories 

Repository contains 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. For 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

These methods provide an initial overview. The full behaviour around a query — which storage pages and languages it covers, how to limit, order and paginate results, and how related objects are loaded — is the subject of the persistence chapter:

Ordering results in Extbase repositories 

There are two distinct places to define the sort order of results, 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 the order. Set the $defaultOrderings class property as follows:

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 queries built inside the 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 set by QueryInterface::ORDER_ASCENDING ( 'ASC') or QueryInterface::ORDER_DESCENDING ( 'DESC').

As an alternative to using the setOrderings() array, the query offers an easier to read form: $query->orderBy('title') sets a single order (replacing any existing ones), and $query->addOrderBy('conferenceDate', QueryInterface::ORDER_DESCENDING) applies an additional sort order. Both take a property name with an optional direction that defaults to ascending. Use whichever reads better — they will produce the same ORDER BY clause.

If neither $defaultOrderings nor method-level setOrderings() is set, no ORDER BY clause is added and the database will return 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 order for any query where the 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 sorting order is entirely your responsibility.

Custom query methods in Extbase repositories 

Use createQuery() when the built-in find methods are not enough. A custom query method builds a query, constrains it, and executes it:

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 full set of constraint methods ( equals(), like(), in(), the comparisons, and combining them with logicalAnd() / logicalOr() / logicalNot()), together with worked examples, is documented on the persistence query page:

The storagePid constraint on repository queries 

Every repository query — except findByUid() and findByIdentifier() — is automatically restricted to records stored on specific TYPO3 pages, the storage pages, configured through the storagePid. Extbase adds this as a WHERE condition to the query; it is not something you opt into.

A repository therefore needs a storagePid before its queries return anything. Configure it in TypoScript:

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

The value can come from several sources that override one another, it can be widened to subpages, and it can be overridden or dropped per query. The whole story lives on its own page:

Injecting repositories with dependency injection 

Inject the repository via constructor injection in your controller or service. Do not use GeneralUtility::makeInstance() as this bypasses the Bootstrap procedure applied by Extbase - any query settings configured in the shared instance will be lost and the repository will not be 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 without needing @inject annotation or a factory call. If the constructor is already taken by a parent controller or you expect the extending constructors to need it, inject methods are an alternative to use.

When to drop out of 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 takes too much time

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, makes it 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!