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.
On this page
A minimal Extbase repository
A repository extends
\TYPO3\. The
class name must follow the convention that the model class
\Domain\ maps to
\Domain\.
Extbase resolves this automatically.
namespace MyVendor\MyExtension\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
class ConferenceRepository extends Repository {}
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:
| Name | Type | Default |
|---|---|---|
Query
|
||
object
|
||
object
|
||
Query
|
||
object
|
||
int
|
||
int
|
findAll()
-
- Type
QueryResult Interface
Returns all records from the repository's storage page(s).
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)
-
- Type
object|null
Alias for
find— the identifier is the UID.By Uid ()
findBy(array $criteria, ?array $orderBy, ?int $limit, ?int $offset)
-
- Type
QueryResult Interface
Finds all objects matching the given criteria array. For example:
find.By ( ['published' => true])
findOneBy(array $criteria, ?array $orderBy)
-
- Type
object|null
Returns the first object matching the criteria, or
null.
count(array $criteria)
-
- Type
int
Returns the number of matching objects without loading them.
countAll()
-
- Type
int
Returns the total number of objects in the repository.
Changed in version 14.0
Magic find methods (
find,
find,
count, etc.) were deprecated in v12.3 and removed in v14.
See Magic findBy(), findOneBy(), countBy*() methods removed (TYPO3 v14) for the migration table.
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:
See also
- Querying the database with Extbase — storagePid resolution, query settings, limits, pagination, persisting and debugging.
- Object relations in Extbase — relation cardinalities, lazy loading and the N+1 query trap.
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 —
find,
find, and custom methods that do
not override the order. Set the
$default class property as follows:
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,
];
}
Method-level ordering applies only to queries built inside the method,
overriding the default for that call. Use
$query->set:
public function findUpcomingByTitle(): QueryResultInterface
{
$query = $this->createQuery();
$query->matching(
$query->greaterThanOrEqual('conferenceDate', new \DateTimeImmutable('today'))
);
$query->setOrderings(['title' => QueryInterface::ORDER_ASCENDING]);
return $query->execute();
}
In both cases the keys are property names, not column names. Order
direction is set by
Query (
'ASC') or
Query (
'DESC').
As an alternative to using the
set array, the query offers an
easier to read form:
$query->order sets a single order (replacing
any existing ones), and
$query->add
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
$default nor method-level
set
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_ 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_ /
foreign_), but
for any direct repository query the sorting order is entirely your responsibility.
Custom query methods in Extbase repositories
Use
create when the built-in find methods are not enough. A custom
query method builds a query, constrains it, and executes it:
<?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();
}
}
The full set of constraint methods (
equals,
like,
in,
the comparisons, and combining them with
logical /
logical
/
logical), together with worked examples, is documented on the
persistence query page:
See also
Querying the database with Extbase — building a query with
create, the constraint methods, ordering, limits, offsets, and the storagePid deep-dive.
The storagePid constraint on repository queries
Every repository query — except
find and
find
— 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:
plugin.tx_myextension.persistence.storagePid = 42
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:
See also
The storagePid — the resolution chain, the recursive setting, why storagePid = 0 does not disable the restriction, and how to override or drop it for a single query.
Injecting repositories with dependency injection
Inject the repository via constructor injection in your controller or service.
Do not use
General 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:
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();
}
}
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.
See also
- Extbase controller actions — full controller reference
including how DI works in controllers.
- Dependency injection in TYPO3 — How TYPO3 handles Dependency injection
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
Connection from within the repository:
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
}
}
See also