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.
Property mapping denied: form fields not saved without a trusted-properties token
Symptom: A domain object argument arrives in an action with all properties set to default values even though the form or URL contains data. No error or validation failure is shown.
Why: When a request does not carry a __ token,
Extbase denies all properties by default to prevent
mass assignment
attacks. A token is generated automatically by
<f: and covers
the fields rendered in the form exactly. Requests that bypass this — URL
parameters, custom forms without a token, or JSON payloads — don't carry a token,
so properties aren't allowed unless a controller explicitly permits it.
What to do: If the request will never carry a __
token, add an
initialize*Action method and call
allow (or
allow) on the relevant
argument's property mapping configuration.
See also
Manually allowing properties on Extbase action arguments — how to write the initializer and which methods to use.
Template variable renders empty in a Fluid template
Symptom: A variable or property path in a Fluid template has no output. No error is thrown and there is no exception in the log. The surrounding HTML renders normally; only the value is blank.
Why: Fluid silently renders an empty string when property resolution fails.
Resolution is attempted in the following order: the public property,
get,
has,
is. If none of these exist or they are
accessible, Fluid gives up without raising an error. Common causes:
- A typo in the property name —
{conference.instead oftitel} {conference.silently produces nothing.title} - A typo in an array key — for arrays, Fluid resolves
{data.viatitle} $data. A missing or misspelled key produces nothing, just like a missing property on an object.['title'] - A
privateproperty without a public getter — Fluid cannot read aprivateproperty directly. Add a publicgetmethod and Fluid will find it regardless of property visibility.X () - A missing getter — a
protectedproperty without a correspondinggetmethod is invisible to Fluid.X () - The variable was not assigned in the controller —
assignwas not called, or was called under a different name.() - The variable was not passed to a partial — variables assigned in the
controller are available in the template, but partials only receive what is
explicitly passed via
<f:. Pass each variable by name, or userender partial="..." arguments=" {conference: conference}" /> arguments="to forward everything the template has.{_ all}" {_is convenient and mostly fine — partials tend to grow and need more input over time — but be aware it makes the partial's dependencies implicit.all} - A missing TCA column definition — Extbase only hydrates properties that
have a corresponding column in
$GLOBALS. A model property with no TCA column is silently skipped during loading and stays at its default value.['TCA']
What to do: Check the exact property name and visibility. Add a
public
get method if one is missing. In the controller, verify that
$this->view->assign is called with the
correct variable name. In the template itself, use
f:debug
to dump the value at the point of use:
<f:debug>{conference}</f:debug>
This renders a formatted dump of the variable, including its type and accessible properties.
f:debug produces no output
Symptom:
<f: is in the template but
nothing appears on the page.
Why: There are 2 possible causes and both are common:
- Cached output: Extbase plugin output is cached by default. If the page is cached, the rendered HTML — without the debug dump — is served from the cache. The ViewHelper will not run again until the cache is cleared. Clear the page cache in the TYPO3 backend or temporarily make the plugin non-cacheable to force the page to be rerendered.
-
Production Application Context suppresses debug output: Default TYPO3 installations run in a Production context. The
Productiondeliberately suppresses debug output to avoid leaking internal information to site visitors. In a Production context,Error Handler f:renders nothing.debug The Application Context must be set via an environment variable or webserver configuration — it cannot be changed from inside TYPO3. See Set the ApplicationContext for all available methods. To check which context is currently active, open System > Environment > Environment overview in the TYPO3 backend.
-
Output is prepended to the page, not rendered in place: By default
f:prepends its output to the top of the DOM rather than rendering at the point of use. This means the dump is easy to miss if you are looking at a specific section of the page, and it is invisible in JSON views or when JavaScript consumes the output. Usedebug inline="1"to render the dump exactly where the tag appears:<f:debug inline="1">{conference}</f:debug>Copied!
See also
How Fluid accesses object properties
for the full resolution order and why
private properties are
never accessible.
Template file not found, or wrong template rendered
Symptom: Extbase throws a "Could not find template file" exception, or renders a template from a different path than expected — for example, a customised template is ignored and the original one is used instead.
Why: Template resolution is based on a numerically keyed path array searched from the highest key downward. Several things can go wrong:
- Wrong key order: a customisation registered at key
5is ignored in favour of the original at key10, because10is higher and wins. - Case mismatch on Linux:
List.andfluid. html list.are different files. The convention requires an uppercase first letter for both the controller subdirectory and the action file name.fluid. html - Controller subdirectory name mismatch: the subdirectory must match the
controller class name without the
Controllersuffix —ConferencerequiresController Conference/, notConferences/orconference/. - Path array key too low: the overriding path is registered at a key
lower than the original (for example
5vs10), so the original wins. The numeric key is the only thing that determines precedence — use a key higher than whatever the original extension uses.
What to do: Use the Active TypoScript module in the TYPO3 backend to
inspect the computed
plugin. paths and
their keys. Verify that file and subdirectory names match the convention
exactly. Check that the overriding extension declares a dependency on the
original in composer..
See also
- Fluid template file resolution in Extbase — naming convention, default paths, and key ordering.
- Overriding Fluid templates from a third-party extension — extension loading order and path registration.
Extbase model validation and TCA validation are independent
Symptom: A record is created or edited in the TYPO3 backend 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 runs during frontend request processing; TCA
validation runs in the backend form engine. There is no shared layer that
enforces both.
This gap is most painful when multiple Extbase models map to the same database table. There may be different models carrying different validation rules for a table, but the backend will always use 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. If a constraint is important in both contexts, add it
both as a #[Validate] attribute on the model property and as a
corresponding TCA column configuration. For records that must be valid in both
contexts, test both paths.
If a form requires different or stricter validation than the domain model allows — for example, a multi-step form that validates partial state — consider using a DTO. This is a plain PHP class that includes only form fields with their validation rules and is separate from the persisted domain model. DTOs are validated by Extbase. Only successfully validated DTOs are mapped to models and persisted.
See also
Validation in Extbase
— lifecycle,
# placement, and how
error is triggered.