View layer in Extbase
The view layer converts variables into rendered output such as strings of
HTML or JSON. It knows nothing about HTTP responses; that is
the controller's concern. The controller assigns variables and calls the render
method indirectly via
$this->html or
$this->json.
Fluid is the
recommended template engine. It integrates with Extbase out of the box and
does not require any additional setup. Other template engines such as Blade or Twig can
be integrated via
View, but that is outside the
scope of this documentation.
On this page
Assigning variables to the Extbase view
Use
$this->view->assign to make a value available under the variable name in the
template.
assign assigns several values at once:
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class ConferenceController extends ActionController
{
public function __construct(
protected readonly ConferenceRepository $conferenceRepository,
) {}
public function listAction(): ResponseInterface
{
$this->view->assign('conferences', $this->conferenceRepository->findAll());
$this->view->assign('title', 'Upcoming conferences');
return $this->htmlResponse();
}
public function showAction(Conference $conference): ResponseInterface
{
$this->view->assignMultiple([
'conference' => $conference,
'speakers' => $conference->getSpeakers(),
]);
return $this->htmlResponse();
}
}
The name passed to
assign becomes the variable name in the template, for example
{conferences},
{title}, and
{conference} above.
$this->settings is the
settings slice of the full Extbase
configuration array. How that array is assembled — and how
view,
persistence, and
settings relate to TypoScript paths
and FlexForm overrides — is covered in the registration chapters:
How the plugin configuration is assembled and
How the module configuration is assembled.
Assigning variables needed in every action
If a variable must be available in every action of a controller — for example
the current site object or a global configuration value — override
initialize and assign it once there instead of repeating the
call in each action method:
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class ConferenceController extends ActionController
{
public function __construct(
protected readonly ConferenceRepository $conferenceRepository,
) {}
#[\Override]
protected function initializeAction(): void
{
/** @var Site $site */
$site = $this->request->getAttribute('site');
$this->view->assign('siteSettings', $site->getSettings()->all());
}
public function listAction(): ResponseInterface
{
$this->view->assign('conferences', $this->conferenceRepository->findAll());
return $this->htmlResponse();
}
public function showAction(Conference $conference): ResponseInterface
{
$this->view->assign('conference', $conference);
return $this->htmlResponse();
}
}
If a variable is only needed in one or two actions, assign it directly inside
those action methods —
initialize is not required.
How Fluid accesses object properties and collections
If a template variable is an object, Fluid resolves property paths such as
{conference. in the following order:
- A public property
$titledirectly. - A public getter method
get.Title () - A public method
has.Title () - A public method
is.Title ()
This means
protected properties with conventional getters work
transparently in templates —
{conference. calls
get.
Warning
If none of the above exist, Fluid renders an empty string without an error or
exception. A typo in a property name, a missing getter, or a
private
property (which is never accessible to Fluid) will produce silent blank
output. See
Template variable renders empty
in the common pitfalls list.
Property paths can be chained:
{conference. resolves
get on the conference and then
get on the
result.
Arrays and ObjectStorage collections are accessed using f:for to iterate, or with an explicit numeric index:
<!-- Iterate over a QueryResult, ObjectStorage, or plain PHP array -->
<f:for each="{conferences}" as="conference">
<h2>{conference.title}</h2>
</f:for>
<!-- Access a specific index (zero-based) -->
<p>{conferences.0.title}</p>
<!-- Access a named key of an associative array -->
<p>{data.headline}</p>
\TYPO3\ implements the PHP
Traversable interface,
so
f: works with it just like with arrays.
Fluid template file resolution in Extbase
Extbase finds a template file by searching through three configurable path lists:
template,
layout, and
partial.
Each list is a numerically keyed array; Fluid searches from the highest key
downward and uses the first match it finds.
Default paths are always appended at key
0 if not already present:
EXT:my_ extension/ Resources/ Private/ Templates/ EXT:my_ extension/ Resources/ Private/ Layouts/ EXT:my_ extension/ Resources/ Private/ Partials/
Template file naming convention: the file must exist at
Templates/. The controller
name is the class name without the
Controller suffix —
Conference → Conference/. The action name matches the
method name without the
Action suffix, with the first letter
uppercased —
list → List..
Note
File names are case-sensitive on Linux systems, therefore
list. and List. are different files.
The convention requires an uppercase first letter.
Bypassing the naming convention is possible but discouraged. Passing an
explicit template path to
$this->view->render overrides automatic
resolution:
public function listAction(): ResponseInterface
{
$this->view->assign('conferences', $this->conferenceRepository->findAll());
return $this->htmlResponse(
$this->view->render('Conference/CustomList')
);
}
This bypasses the controller/action convention. Reserve it for rare situations where one action needs to render a different template, for example for AJAX variants that share a controller action.
Overriding Fluid template paths via TypoScript
Add paths via TypoScript to extend or override the default resolution. Use the
plugin-specific path
plugin. to target one
plugin, or
plugin. (no plugin suffix) to set
defaults for every plugin of the extension:
plugin.tx_myextension_conferencelist.view {
templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/
layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts/
partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/
}
Fluid searches from the highest key downward, so the path at key
10 above takes precedence over a default at key
0.
Finding the TypoScript object path for a plugin: open the TYPO3 backend, navigate to the site or page containing the plugin, and open Site Management > TypoScript. The Active TypoScript module shows the computed TypoScript tree including all registered plugin objects. See How the plugin configuration is assembled for how the extension-wide and plugin-specific paths are merged.
Overriding Fluid templates from a third-party extension
To replace a template provided by an extension you do not control, add the
override paths to your own extension or site package. Never modify a
third-party extension directly. Add a path at a key higher than the one used in the
original extension. Most extensions register their paths at key
10 or leave the default at key
0. Using key
20 in your sitepackage is therefore safe in the majority of
cases:
plugin.tx_thirdpartyextension.view {
templateRootPaths.20 = EXT:my_sitepackage/Resources/Private/ThirdParty/Templates/
partialRootPaths.20 = EXT:my_sitepackage/Resources/Private/ThirdParty/Partials/
}
Place your overriding template in the same relative path as in the original.
Conference/ overrides Conference/
in the third-party extension.
Key value is the only thing that matters: TypoScript path arrays are
assembled from all active extensions and resolved by numeric key — the higher
key wins. Load order between extensions does not affect this. Declare the
third-party extension as a dependency in composer. to ensure it is
installed, but not because load order influences template resolution:
See also
- Template file not found, or wrong template rendered for common causes and how to debug them.
ViewHelpers in Fluid templates
ViewHelpers
are Fluid template functions and tags. They are used for
conditionals, loops, links, formatting, and much more. The built-in ViewHelpers
use the
f: namespace, which is available in every template without
declaration.
Two equivalent syntaxes exist — tag style and inline style:
<!-- Tag style -->
<f:for each="{conferences}" as="conference">
<f:if condition="{conference.published}">
<h2>{conference.title}</h2>
<p><f:format.date format="d.m.Y">{conference.conferenceDate}</f:format.date></p>
<f:link.action action="show" arguments="{conference: conference}">
Details
</f:link.action>
</f:if>
</f:for>
<!-- Inline style (useful inside HTML attributes) -->
<a href="{f:uri.action(action: 'show', arguments: '{conference: conference}')}">
Details
</a>
<time datetime="{conference.conferenceDate -> f:format.date(format: 'Y-m-d')}">
{conference.conferenceDate -> f:format.date(format: 'd.m.Y')}
</time>
Commonly used ViewHelpers (with links to their full reference) are:
- f:for
— iterate over arrays and
\TYPO3\collections.CMS\ Extbase\ Persistence\ Object Storage - f:if — conditional output.
- f:link.action — generate links to controller actions.
- f:uri.action — generate action URIs for use inside attributes.
- f:format.date — format date objects and timestamps.
- f:flashMessages — render flash messages added by the controller.
- f:form
— build forms with automatic
__trustedPropertiestoken generation. - f:translate
— render localised labels from
locallang..xlf - f:debug
— dump a variable's type and value during development; remove before
deploying to production. By default output is prepended to the page top;
use
inline="1"to render it in place. For deeper introspection of Extbase objects and lazy-loaded relations, the community extension includekrexx provides a richer debug output thanf:.debug
For the complete reference of all built-in ViewHelpers, see the ViewHelper reference.
To write your own ViewHelper, see Creating custom ViewHelpers.
For reusable template fragments with a typed argument contract, Fluid v4
introduced Fluid Components
as an alternative to both partials and custom ViewHelpers. Components are
pure Fluid templates — no PHP class required — with typed, named arguments
declared via
<f:. They are the right choice for
UI building blocks such as buttons, cards, or form
field wrappers that need to be reused across templates.
Custom ViewHelpers are the right choice when logic cannot be expressed in
Fluid alone, or when access to the rendering context is needed — for example
ViewHelpers like
f: that must locate a parent
f: tag. Components do not have access to the parent rendering
context and must not be used in those situations.
JsonView: rendering JSON responses from Extbase
To return JSON from an action, set
$this->default to
\TYPO3\ and then return
$this->json. Extbase will then use
Json instead of Fluid to render
the response body.
Json does not expose all assigned
variables by default. Declare which variables to include with
set, and set which properties of each object are
exposed with
set:
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
class ConferenceController extends ActionController
{
protected ?string $defaultViewObjectName = JsonView::class;
public function listAction(): ResponseInterface
{
$this->view->assign('conferences', $this->conferenceRepository->findAll());
$this->view->setVariablesToRender(['conferences']);
$this->view->setConfiguration([
'conferences' => [
'_descendAll' => [
'_only' => ['title', 'conferenceDate', 'uid'],
],
],
]);
return $this->jsonResponse();
}
}
The configuration keys are:
_only— allowlist of property names to include._exclude— denylist of property names to omit._descend— descend into a named sub-object and apply nested configuration to it._descend— apply the same nested configuration to every element of an array orAll \TYPO3\.CMS\ Extbase\ Persistence\ Object Storage
For simple cases where the JSON structure can be built explicitly, skip
Json by passing a string
to
json directly:
public function countAction(): ResponseInterface
{
return $this->jsonResponse(json_encode([
'count' => $this->conferenceRepository->countAll(),
]));
}
See also
Registration: frontend plugin for how to register a controller action as a cacheable or non-cacheable plugin.