Common pitfalls in Extbase
Note
This page is a work in progress. Content will be added as part of the Extbase documentation rewrite for TYPO3 v14.
This page collects the situations that most commonly trip up Extbase developers — from beginners hitting their first wall to experienced developers upgrading from older TYPO3 versions. Each entry names the symptom, explains briefly why it happens, and points to the full discussion.
If something in your extension is not working and you are not sure why, scan this page first.
On this page
Model properties declared private are never populated
Symptom: A model property always holds its default value after loading from the database, even though the database column contains data. No error is thrown.
Why: Extbase hydrates properties via
_set, a method
defined on
Abstract. It assigns values using dynamic
property access (
$this->). PHP's visibility
rules prevent a parent class method from writing to a
private property
declared in a child class — the assignment silently does nothing. The same
applies in the other direction: dirty-state tracking cannot read private
properties either, so changes are never persisted.
This catches developers who follow the general good-practice rule of keeping
properties as private as possible. In Extbase models,
protected is the
correct visibility — not
private. Public properties also work and are a
valid, shorter alternative (no getters or setters required), but they bypass
lazy-loading proxies and dirty-state tracking, which can matter for relations.
See also
findAll() returns nothing on an Extbase repository
Symptom:
$repository->find (or any repository query) returns
an empty result, but the records clearly exist in the database.
Why: Every repository query is filtered to one or more storage pages
(the
storage) by default. If no storage page is configured, or the
records live on a different page than expected, the query returns nothing.
find is the only method that ignores storagePid.
See also
storagePid — when findAll() returns nothing and the full resolution chain in Persistence queries.
Annotations silently ignored in TYPO3 v14
Symptom: Lazy loading, cascade delete, or validation rules defined in
DocBlock annotations (
@Extbase\,
@Extbase\,
etc.) have no effect. No error is thrown.
Why: DocBlock annotation support was removed in TYPO3 v14. Extbase simply ignores them. The replacement is native PHP attributes.
See also
- Annotations replaced by PHP attributes (TYPO3 v12 / required from v14) — migration steps and
the full before/after example.
Magic findBy*() methods removed in TYPO3 v14
Symptom: Calls to
find,
find,
or
count throw an error or do nothing after upgrading
to TYPO3 v14.
Why: The magic property-name methods were deprecated in v12.3 and removed in v14. The replacements use an explicit array signature.
See also
Magic findBy(), findOneBy(), countBy*() methods removed (TYPO3 v14) — migration table with before/after examples.
Built-in find methods — the current find method reference.
Plugin registered with list_type no longer works
Symptom: An existing plugin content element stops rendering after upgrading to TYPO3 v14, or a newly registered plugin cannot be selected in the backend.
Why: The
list_ / "General Plugin" content element was deprecated
in v13.4 and removed in v14. Plugins must now be registered as dedicated
CType content elements.
See also
Frontend plugin registration — covers the v14 registration approach and the upgrade wizard required for existing records.
AbstractValueObject is not public API
Symptom: Code extending
\TYPO3\ produces
deprecation warnings or breaks unexpectedly.
Why: The class is marked
@internal in v14 — it is not part of the
public Extbase API and may change or be removed without notice. No replacement
class is provided.
What to do instead: Implement value objects as plain PHP classes. The DDD concept is valid; the base class is not.
See also
Frontend form with inline relations produces incomplete saves or silent data loss
Symptom: A frontend form that creates or updates a model with inline relations (speakers, images, tags added dynamically) produces incomplete saves, orphaned records, or silent data loss for dynamically added fields that fail HMAC argument hash validation. Trying to replicate the backend's "add another row" UX via JavaScript makes things worse.
Why: Two independent problems compound each other. First, Extbase's property mapping and persistence layer were not designed to handle a graph of new and modified related objects submitted in a single form — partial saves and inconsistent state are the common outcome. Second, TYPO3's HMAC-based argument hashing protects against mass-assignment attacks by signing the field names known at render time; dynamically generated field names are not covered and either fail or require disabling the protection entirely.
What to do instead: Avoid the pattern rather than work around it. Concrete alternatives: split the form into single-object steps; manage relations in a backend module where the tooling is designed for it; use separate AJAX endpoints that create one related record at a time; consider DataHandler for write operations that need IRRE-style relation management.
Extbase model validation and TCA validation are independent
Symptom: A record created or edited in the TYPO3 backend passes without error, but the same record fails Extbase validation in the frontend — or vice versa. A backend editor saves a record that a frontend action then rejects immediately. Or a record saved through Extbase arrives in the backend in a state the backend form cannot open cleanly.
Why: Extbase validation (#[Validate] attributes on model properties)
and TCA validation (eval, required, and similar TCA column
configuration) are entirely separate systems. Neither one knows about the
other. Extbase validation only runs during frontend request processing; TCA
validation only runs in the backend form engine. There is no shared layer that
enforces both at once.
This gap is most painful when multiple Extbase models map to the same database table — each model may carry different validation rules, but the backend always uses the TCA for that table regardless of which model class is involved.
What to do instead: Treat the two systems as complementary and configure
both deliberately. For every constraint that matters in both contexts, add it
both as a #[Validate] attribute on the model property and as the
corresponding TCA column configuration. For records that must be valid in both
contexts, test both paths explicitly.
When a form needs stricter or different validation than the domain model allows — for example, a multi-step form that validates partial state — consider using a DTO: a plain PHP class that carries only the form fields and their validation rules, separate from the persisted domain model. The DTO is validated by Extbase; only a successfully validated DTO is mapped to the model and persisted.