Common pitfalls in Extbase 

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.

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 _setProperty(), a method defined on AbstractDomainObject. It assigns values using dynamic property access ( $this->{$propertyName} = $value). 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.

findAll() returns nothing on an Extbase repository 

Symptom: $repository->findAll() (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 storagePid) by default. If no storage page is configured, or the records live on a different page than expected, the query returns nothing. findByUid() is the only method that ignores storagePid.

Annotations silently ignored in TYPO3 v14 

Symptom: Lazy loading, cascade delete, or validation rules defined in DocBlock annotations ( @Extbase\ORM\Lazy, @Extbase\Validate, 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.

Magic findBy*() methods removed in TYPO3 v14 

Symptom: Calls to findByTitle($value), findOneBySlug($value), or countByStatus($value) 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.

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_type / "General Plugin" content element was deprecated in v13.4 and removed in v14. Plugins must now be registered as dedicated CType content elements.

AbstractValueObject is not public API 

Symptom: Code extending \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject 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.

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 __trustedProperties token, Extbase denies all properties by default to prevent mass assignment attacks. A token is generated automatically by <f:form> 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 __trustedProperties token, add an initialize*Action() method and call allowProperties() (or allowAllProperties()) on the relevant argument's property mapping configuration.

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, getX(), hasX(), isX(). 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.titel} instead of {conference.title} silently produces nothing.
  • A typo in an array key — for arrays, Fluid resolves {data.title} via $data['title']. A missing or misspelled key produces nothing, just like a missing property on an object.
  • A private property without a public getter — Fluid cannot read a private property directly. Add a public getX() method and Fluid will find it regardless of property visibility.
  • A missing getter — a protected property without a corresponding getX() method is invisible to Fluid.
  • The variable was not assigned in the controller — assign() was 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:render partial="..." arguments="{conference: conference}" />. Pass each variable by name, or use arguments="{_all}" 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.
  • A missing TCA column definition — Extbase only hydrates properties that have a corresponding column in $GLOBALS['TCA'] . A model property with no TCA column is silently skipped during loading and stays at its default value.

What to do: Check the exact property name and visibility. Add a public getX() method if one is missing. In the controller, verify that $this->view->assign('conference', $conference) is called with the correct variable name. In the template itself, use f:debug to dump the value at the point of use:

EXT:my_extension/Resources/Private/Templates/Conference/Show.fluid.html
<f:debug>{conference}</f:debug>
Copied!

This renders a formatted dump of the variable, including its type and accessible properties.

f:debug produces no output 

Symptom: <f:debug>{variable}</f:debug> 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 ProductionErrorHandler deliberately suppresses debug output to avoid leaking internal information to site visitors. In a Production context, f:debug renders nothing.

    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:debug 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. Use inline="1" to render the dump exactly where the tag appears:

    <f:debug inline="1">{conference}</f:debug>
    Copied!

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 5 is ignored in favour of the original at key 10, because 10 is higher and wins.
  • Case mismatch on Linux: List.fluid.html and list.fluid.html are different files. The convention requires an uppercase first letter for both the controller subdirectory and the action file name.
  • Controller subdirectory name mismatch: the subdirectory must match the controller class name without the Controller suffix — ConferenceController requires Conference/, not Conferences/ or conference/.
  • Path array key too low: the overriding path is registered at a key lower than the original (for example 5 vs 10), 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.tx_<extensionkey>.view 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.json.

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.