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->htmlResponse() or $this->jsonResponse().

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 ViewFactoryInterface , but that is outside the scope of this documentation.

Assigning variables to the Extbase view 

Use $this->view->assign() to make a value available under the variable name in the template. assignMultiple() assigns several values at once:

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

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 initializeAction() and assign it once there instead of repeating the call in each action method:

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

If a variable is only needed in one or two actions, assign it directly inside those action methods — initializeAction() is not required.

How Fluid accesses object properties and collections 

If a template variable is an object, Fluid resolves property paths such as {conference.title} in the following order:

  • A public property $title directly.
  • A public getter method getTitle().
  • A public method hasTitle().
  • A public method isTitle().

This means protected properties with conventional getters work transparently in templates — {conference.title} calls getTitle().

Property paths can be chained: {conference.mainSpeaker.name} resolves getMainSpeaker() on the conference and then getName() on the result.

Arrays and ObjectStorage collections are accessed using f:for to iterate, or with an explicit numeric index:

EXT:my_extension/Resources/Private/Templates/Conference/List.fluid.html
<!-- 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>
Copied!

\TYPO3\CMS\Extbase\Persistence\ObjectStorage implements the PHP Traversable interface, so f:for 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: templateRootPaths, layoutRootPaths, and partialRootPaths. 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/{ControllerName}/{ActionName}.fluid.html. The controller name is the class name without the Controller suffix — ConferenceControllerConference/. The action name matches the method name without the Action suffix, with the first letter uppercased — listAction()List.fluid.html.

Bypassing the naming convention is possible but discouraged. Passing an explicit template path to $this->view->render() overrides automatic resolution:

EXT:my_extension/Classes/Controller/ConferenceController.php
public function listAction(): ResponseInterface
{
    $this->view->assign('conferences', $this->conferenceRepository->findAll());
    return $this->htmlResponse(
        $this->view->render('Conference/CustomList')
    );
}
Copied!

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.tx_myextension_myplugin to target one plugin, or plugin.tx_myextension (no plugin suffix) to set defaults for every plugin of the extension:

EXT:my_extension/Configuration/Sets/MyExtension/setup.typoscript
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/
}
Copied!

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:

EXT:my_sitepackage/Configuration/Sets/MySitepackage/setup.typoscript
plugin.tx_thirdpartyextension.view {
    templateRootPaths.20 = EXT:my_sitepackage/Resources/Private/ThirdParty/Templates/
    partialRootPaths.20 = EXT:my_sitepackage/Resources/Private/ThirdParty/Partials/
}
Copied!

Place your overriding template in the same relative path as in the original. Conference/List.fluid.html overrides Conference/List.fluid.html 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.json to ensure it is installed, but not because load order influences template resolution:

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:

EXT:my_extension/Resources/Private/Templates/Conference/List.fluid.html
<!-- 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>
Copied!

Commonly used ViewHelpers (with links to their full reference) are:

  • f:for — iterate over arrays and \TYPO3\CMS\Extbase\Persistence\ObjectStorage collections.
  • 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 __trustedProperties token 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 than f: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:argument>. 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:form.textfield that must locate a parent f:form 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->defaultViewObjectName to \TYPO3\CMS\Extbase\Mvc\View\JsonView::class and then return $this->jsonResponse(). Extbase will then use JsonView instead of Fluid to render the response body.

JsonView does not expose all assigned variables by default. Declare which variables to include with setVariablesToRender(), and set which properties of each object are exposed with setConfiguration():

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

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.
  • _descendAll — apply the same nested configuration to every element of an array or \TYPO3\CMS\Extbase\Persistence\ObjectStorage .

For simple cases where the JSON structure can be built explicitly, skip JsonView by passing a string to jsonResponse() directly:

EXT:my_extension/Classes/Controller/ConferenceController.php
public function countAction(): ResponseInterface
{
    return $this->jsonResponse(json_encode([
        'count' => $this->conferenceRepository->countAll(),
    ]));
}
Copied!