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.
On this page
The minimal Extbase repository
A repository extends
\TYPO3\. The
class name must follow the convention: 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 {}
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:
| 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. 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.
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 —
find,
find, and custom methods that do
not override it. Set the
$default class property:
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 the query built in that 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
Query (
'ASC') or
Query (
'DESC').
If neither
$default nor a method-level
set
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_ 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 ordering is entirely your responsibility.
Custom query methods in Extbase repositories
Use
create when the built-in find methods are not enough:
<?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
query <\ API supports these constraint methods:
| Name | Type | Default |
|---|---|---|
equals(string $property, mixed $value)
-
Exact match. Generates a SQL
=comparison.
like(string $property, string $value)
-
Pattern match. Generates SQL
LIKE. Use%as wildcard, for example'%search%'.
lessThan(string $property, mixed $value)
-
Generates SQL
<.
lessThanOrEqual(string $property, mixed $value)
-
Generates SQL
<=.
greaterThan(string $property, mixed $value)
-
Generates SQL
>.
greaterThanOrEqual(string $property, mixed $value)
-
Generates SQL
>=.
in(string $property, array $values)
-
Matches any value in the given array. Generates SQL
IN.(...)
contains(string $property, object $value)
-
Checks whether an
Objectrelation contains the given object.Storage
logicalAnd(mixed ...$constraints)
-
Combines multiple constraints with
AND.
logicalOr(mixed ...$constraints)
-
Combines multiple constraints with
OR.
logicalNot(ConstraintInterface $constraint)
-
Negates a constraint with
NOT.
Chain multiple constraints:
$query->matching(
$query->logicalAnd(
$query->equals('published', true),
$query->greaterThanOrEqual('conferenceDate', new \DateTimeImmutable('today')),
)
);
See also
Persistence queries — the full query reference, including ordering, limits, offsets, and the storagePid deep-dive.
storagePid — when findAll() returns nothing
Every repository query (except
find) is filtered to one or more
storage pages by default. If
find 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
storage.
Configure it for example in TypoScript:
plugin.tx_myextension.persistence.storagePid = 42
See also
Persistence queries — the full resolution chain: TypoScript → plugin-specific TypoScript → FlexForm → PHP override, plus how to debug a storagePid problem.
Injecting repositories with dependency injection
Inject the repository via constructor injection in your controller or service.
Do not use
General — 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:
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. No
@inject annotation, no factory call.
See also
Extbase controller actions — full controller reference including how DI works for controllers.
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
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