Persistence and the Extbase ORM
Extbase includes an Object-Relational Mapper (ORM). The ORM's job is to
translate between the relational world of database tables and the
object-oriented world of PHP classes — so you work with
Event objects
rather than raw database rows, and Extbase handles the SQL.
On this page
What the ORM does for you
Without an ORM, reading a record means writing a query, iterating over result
rows, and manually constructing PHP objects. Writing a record means constructing
an
INSERT or
UPDATE statement yourself. With the Extbase ORM:
- Reading: you call
$this->eventand receive a collection of fully populatedRepository->find All () Eventobjects. - Writing: you call
$this->eventand the ORM generates the INSERT statement.Repository->add ($event) - Updating: you modify a property on the object and call
$this->event.Repository->update ($event) - Deleting: you call
$this->event.Repository->remove ($event)
A controller action that creates and stores a new event looks like this:
use MyVendor\MyExtension\Domain\Model\Event;
use MyVendor\MyExtension\Domain\Repository\EventRepository;
use Psr\Http\Message\ResponseInterface;
public function createAction(Event $event): ResponseInterface
{
$this->eventRepository->add($event);
return $this->redirect('list');
}
No SQL, no INSERT, no result-set iteration. The ORM handles the mapping.
The ORM also handles relations. If an
Event has many
Speaker
objects stored in an
\TYPO3\,
Extbase loads those related objects automatically when you access the property
— without you writing any JOIN.
This convenience comes with a trade-off: the ORM is optimised for working with individual domain objects, not for bulk operations or complex aggregate queries. When you need those, dropping down to DBAL directly is the right choice. That is not a failure — it is the intended design.
How models map to the database
Extbase derives the database table name from the model class name by convention:
Vendor\MyExtension\Domain\Model\Event
→ tx_myextension_domain_model_event
Property names map to column names by converting camelCase to snake_case:
$eventDate → event_date
$speakerCount → speaker_count
These conventions work automatically. When the defaults do not fit — for example
when mapping to an existing table with its own naming — you can override them in
Configuration/Extbase/Persistence/Classes.php.
The repository as the only door to the database
Controllers should not query the database directly. Every database interaction goes through a repository. This is a deliberate constraint: it keeps persistence logic in one place and keeps controllers thin.
A repository class extends
\TYPO3\ and
is named after its model —
Event for
Event. The base
class provides
find,
find,
find, and
find without
any additional code. Custom queries are added as methods on the repository.
Repositories are singletons — one instance per request, shared across all controllers that inject them.
The storagePid: where Extbase looks for records
This is the single most common source of confusion in Extbase: a repository returns no results even though the records exist in the database.
The cause is almost always the storagePid.
Extbase does not query all records in a table by default. It restricts queries to records stored on a specific TYPO3 page — the storage page (or storagePid). This mirrors how TYPO3 editors organise records: they create a sysfolder, store domain records there, and point the plugin at that folder.
If the storagePid is not configured, or points at the wrong page, the repository returns an empty result — silently.
The storagePid is configured in TypoScript:
plugin.tx_myextension.persistence.storagePid = 42
Or per plugin instance:
plugin.tx_myextension_eventlist.persistence.storagePid = 42
Editors can also set it per plugin content element via the Record Storage Page field in the plugin's properties.
Tip
If your repository returns nothing and the records exist in the database, check the storagePid first. Enable TYPO3's query logging or use the Extbase query debugger to see the exact SQL being executed and which page restriction is applied.
The full storagePid resolution order and how to override it in PHP are covered in Querying the database with Extbase.
Extbase object lifecycle
Extbase tracks the state of every domain object it loads. At the end of a request the persistence manager flushes automatically, comparing each object's current state against its original state and writing only the differences. You do not need to call a "save" method on objects you retrieved and modified — the ORM detects the change.
Objects you create with
new are not tracked until you pass them to a
repository with
add. Objects you pass to
remove are deleted
at flush time.
See also
Extbase domain model — defining models and their properties.
Extbase repository — writing custom repository queries.
Persistence queries — the full query API, storagePid deep-dive, and when to use DBAL instead.