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.

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->eventRepository->findAll() and receive a collection of fully populated Event objects.
  • Writing: you call $this->eventRepository->add($event) and the ORM generates the INSERT statement.
  • Updating: you modify a property on the object and call $this->eventRepository->update($event).
  • Deleting: you call $this->eventRepository->remove($event).

A controller action that creates and stores a new event looks like this:

EXT:my_extension/Classes/Controller/EventController.php
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');
}
Copied!

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\CMS\Extbase\Persistence\ObjectStorage , 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
Copied!

Property names map to column names by converting camelCase to snake_case:

$eventDate     →  event_date
$speakerCount  →  speaker_count
Copied!

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\CMS\Extbase\Persistence\Repository and is named after its model — EventRepository for Event. The base class provides findAll(), findByUid(), findBy(array $criteria), and findOneBy(array $criteria) 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
Copied!

Or per plugin instance:

plugin.tx_myextension_eventlist.persistence.storagePid = 42
Copied!

Editors can also set it per plugin content element via the Record Storage Page field in the plugin's properties.

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.