ActionController: actions, arguments and responses
A controller handles the request, coordinates repository and service calls,
and returns a response. Business logic can live in the controller directly —
for anything complex or reused, a dedicated service class keeps the controller
focused. In Extbase every controller extends
\TYPO3\.
On this page
Structure of an Extbase ActionController
Controllers live in Classes/. Public methods with a name
ending in
Action are actions that can be mapped to plugin actions or
backend module actions.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Domain\Model\Conference;
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());
return $this->htmlResponse();
}
public function showAction(Conference $conference): ResponseInterface
{
$this->view->assign('conference', $conference);
return $this->htmlResponse();
}
}
Key rules:
- Extend
\TYPO3\.CMS\ Extbase\ Mvc\ Controller\ Action Controller - Do not declare the class as
final. Third parties should be able to extend controllers to customise behaviour. - Inject repositories and services via the constructor using
dependency injection. Injected dependencies
must be
protected readonly, notprivate readonly, so subclasses can access them. In a service class that does not extend anything and carries no mutable state, you can declare the whole classreadonlyinstead — but controllers extendActionand cannot useController readonly class. - Every action method must return a
\Psr\.Http\ Message\ Response Interface - Frontend plugins: use
$this->view->assignto pass variables to the Fluid template and return() $this->htmlto render it. Actions can also return JSON, a redirect, or any other PSR-7 response.Response () - Backend modules: inject
Module, create aTemplate Factory Moduleinstance per action, assign variables viaTemplate $module, and returnTemplate->assign Multiple () $module. Do not useTemplate->render Response ('Action Name') $this->viewor$this->htmlin a module controller. See Registering an Extbase backend module.Response ()
Action arguments and automatic Extbase object resolution
Typed action parameters are resolved automatically from the request before the
action is called. Scalars (
int,
string,
bool) are read
directly from the request arguments. A parameter typed as a domain object (a
class extending
\TYPO3\) triggers a repository
lookup. Extbase converts the incoming UID to a fully
hydrated object.
public function showAction(Conference $conference): ResponseInterface
{
$this->view->assign('conference', $conference);
return $this->htmlResponse();
}
If, for example, a URL includes
?tx_, Extbase
loads
Conference with UID 5 from the repository and passes the object
directly to
show. A manual repository call is not necessary.
The lookup ignores the storagePid restriction but always respects enableFields
— hidden records, records outside their starttime/endtime window, deleted
records, and records from other workspaces will not be resolved. If the record
cannot be found, Extbase calls
error instead of the action.
Optional parameters require a default value. A nullable type alone
(
?Conference $conference) is not sufficient — without
= null,
PHP requires the caller to pass explicitly
null, which Extbase will not
do for a missing argument. Always combine the nullable type with a default:
public function listAction(int $page = 1): ResponseInterface { ... }
public function showAction(?Conference $conference = null): ResponseInterface { ... }
If an argument is missing from the request and there is no default value, Extbase
calls
error instead. See
errorAction: Extbase validation and argument-mapping errors.
See also
Property mapping: request arguments to objects for how type conversion turns raw strings and arrays into PHP objects.
Accessing TypoScript settings in an Extbase controller
The merged TypoScript settings for the current plugin are available in every
action via
$this->settings:
public function listAction(): ResponseInterface
{
$limit = (int)($this->settings['itemsPerPage'] ?? 10);
$this->view->assign(
'conferences',
$this->conferenceRepository->findLatest($limit),
);
return $this->htmlResponse();
}
$this->settings contains the merged result of
plugin.,
plugin., and any FlexForm
overrides. It has nothing to do with
site settings — those are a separate
configuration layer defined in settings.. Site settings can feed
into TypoScript constants via the
{$...} syntax, but that is an
explicit mapping, not an automatic merge. Only values that flow through
plugin. end up in
$this->settings. The full resolution order is covered in
Extbase TypoScript configuration.
Extbase action response helpers
Action provides two
convenience methods for the most common response types:
htmlResponse (?string $html = null) - Returns a
text/PSR-7 response. Renders the current Fluid view without any arguments. Pass a string to use as the body.html jsonResponse (?string $json = null) - Returns an
application/PSR-7 response. Renders the current view without any arguments (use withjson \TYPO3\). Pass a JSON string for the response.CMS\ Extbase\ Mvc\ View\ Json View
For any other status code or content types, build the response manually using
the injected
$this->response and
$this->stream:
return $this->responseFactory
->createResponse(202)
->withHeader('Content-Type', 'text/plain; charset=utf-8')
->withBody($this->streamFactory->createStream('Accepted'));
Redirecting and forwarding from an Extbase action
After a write operation (create, update, delete), redirect the user to avoid
a double-submit on page reload. All three methods described here return a
\Psr\ — you must
return them. They do not throw
an exception or stop execution on their own.
redirect() — redirect to another Extbase action
redirect issues an
HTTP 303 "See Other"
response. The browser discards the POST body and issues a new GET request to
the target action URL.
Discarding the POST body is intentional. After a write action (create, update, delete) you want the browser's address bar to show the result page URL, not a form submission URL. If the user presses F5 or the back button, the browser replays a GET — not the original POST — so that the write does not happen a second time. This pattern is called Post/Redirect/Get.
use MyVendor\MyExtension\Domain\Model\Conference;
use Psr\Http\Message\ResponseInterface;
public function updateAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->update($conference);
// Redirect to listAction on the same page, same plugin
return $this->redirect('list');
}
The full signature is:
// Go to showAction and pass a domain object as argument
return $this->redirect('show', null, null, ['conference' => $conference]);
// Go to listAction of ConferenceController on a different page
return $this->redirect('list', 'Conference', null, [], $targetPageUid);
| Parameter | Default | Purpose |
|---|---|---|
$action | (required) | Name of the target action without the Action suffix, for example 'list'. |
$controller |
null | Short class name of the target controller.
null means the current controller. |
$extension |
null | Extension name in UpperCamelCase.
null means the current extension. |
$arguments |
[] | Array of arguments appended to the target URL as query parameters. |
$page |
null | UID of the target page.
null keeps the current page. |
$status |
303 | HTTP status code. Change only when you have a specific reason. |
Note
If you need the browser to resend the original POST to a different Extbase action, for example, to hand off to another controller, pass HTTP 307 Temporary Redirect as the sixth argument. The redirect response itself has no body; it only tells the browser where to go. The browser then re-sends the original POST, including its body, to the new action URL:
return $this->redirect('create', 'Conference', null, [], null, 307);
This situation does not arise in typical Extbase form handling, where
Forward is the right tool for re-processing within the same
request without a browser round-trip.
redirectToUri() — redirect to an arbitrary URL
redirect issues the
same HTTP 303 redirect but accepts a URL rather than an Extbase action
name. Use it when the target URL is assembled outside the action, for example
with
Uri:
use Psr\Http\Message\ResponseInterface;
public function deleteAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->remove($conference);
$uri = $this->uriBuilder
->reset()
->setTargetPageUid(42)
->uriFor('list', [], 'Conference');
return $this->redirectToUri($uri);
}
$this->uri is available in every action. Call
->reset before building a new URI so settings from a previous call
do not leak into the next one.
ForwardResponse — transfer control within the same request
Forward transfers control to
another action within the same request. There is no browser redirect or new HTTP
round-trip. Use it to re-display a form after a validation failure without
losing any submitted data:
use MyVendor\MyExtension\Domain\Model\Conference;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
public function createAction(Conference $conference): ResponseInterface
{
if ($someConditionFails) {
return (new ForwardResponse('new'))
->withArguments(['conference' => $conference]);
}
$this->conferenceRepository->add($conference);
return $this->redirect('list');
}
Unlike a redirect,
Forward
preserves the current request's arguments and flash messages so that the forwarded
action can re-render the form with submitted values still in place. The
browser URL does not change.
Forward accepts action name,
with,
with, and
with — but no explicit page UID. The dispatching always
continues within the current request context. If you need to send the user to a
specific page, use
redirect instead.
Flash messages in Extbase controllers
Flash messages are one-time notifications that survive a redirect or a forward
and are rendered in a Fluid template via
<f:. They
are also useful for in-page feedback when no redirect is involved. The message
is rendered in the same response.
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
$this->addFlashMessage(
'Conference was saved.',
'Success',
ContextualFeedbackSeverity::OK,
);
return $this->redirect('list');
The five severity levels are:
ContextualFeedback Severity:: NOTICE ContextualFeedback Severity:: INFO ContextualFeedback Severity:: OK ContextualFeedback Severity:: WARNING ContextualFeedback Severity:: ERROR
By default, flash messages are stored inside the session and survive the redirect.
To keep a message only for the current request, for example, when forwarding
rather than redirecting, pass
false as the fourth argument:
$this->addFlashMessage('Could not process your request.', '', ContextualFeedbackSeverity::ERROR, false);
Render them in Fluid with the
<f: ViewHelper.
initializeAction and per-action initialization in Extbase
initialize is called before every controller action. Use it
for setup that is common to all actions.
For setup that applies to a single action only, define a method named
initialize + the capitalized action name +
Action (for example
initialize before
create). Extbase calls
it automatically before the corresponding action:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Domain\Model\Conference;
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 initializeCreateAction(): void
{
$this->arguments['conference']
->getPropertyMappingConfiguration()
->allowProperties('title', 'conferenceDate');
}
public function createAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->add($conference);
return $this->redirect('list');
}
}
The per-action initializer is the standard place to configure property mapping, for example, to allow specific properties or to set custom date formats for arguments. See Configuring Extbase type converters.
Protecting Extbase actions with
#[Authorize]
New in version 14.0
The
# attribute restricts access to individual action methods
without having to write boilerplate login checks. It is repeatable so multiple
attributes can be stacked to combine conditions.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Domain\Model\Conference;
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
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());
return $this->htmlResponse();
}
#[Authorize(requireLogin: true)]
public function newAction(): ResponseInterface
{
return $this->htmlResponse();
}
#[Authorize(requireLogin: true)]
public function createAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->add($conference);
return $this->redirect('list');
}
#[Authorize(requireLogin: true, callback: 'isOwner')]
public function deleteAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->remove($conference);
return $this->redirect('list');
}
public function isOwner(Conference $conference): bool
{
$currentUserId = $this->request->getAttribute('frontend.user')?->getUserId();
return $currentUserId !== null && $conference->getOwnerUid() === $currentUserId;
}
}
The three options:
requireLogin: true - Denies access if no frontend user is logged in. Returns a HTTP 403 Forbidden response
via
Error.Controller requireorGroups: [42] requireGroups: ['editors'] - Grants access only if the logged-in user belongs to at least one of the listed groups (by UID or title). Implies a login check.
callback: 'methodorName' callback:[Some Class:: class, 'method'] -
Calls a method on the controller (string form) or on an arbitrary class (array form). The method receives the same arguments as the action and must return
bool. Use this for ownership or record-level checks.The callback method must be
public. Extbase verifies visibility via reflection and throws an exception if it is not.Inside a controller callback, read the current frontend user from the request attribute
frontend.:user EXT:my_extension/Classes/Controller/ConferenceController.phppublic function isOwner(Conference $conference): bool { $currentUserId = $this->request->getAttribute('frontend.user')?->getUserId(); return $currentUserId !== null && $conference->getOwnerUid() === $currentUserId; }Copied!
When access is denied, Extbase dispatches
Before
which lets event listeners return a custom response instead of the default 403.
See also
Extbase PHP attributes reference for the full attribute reference including the event for custom denial responses.
Rate-limiting Extbase actions with
#[RateLimit]
New in version 14.0
# limits how often a visitor (identified by IP address) may
call an action within a sliding time window. It is designed for write actions
like form submissions.
use TYPO3\CMS\Extbase\Attribute\RateLimit;
#[RateLimit(limit: 3, interval: '1 hour')]
public function createAction(Conference $conference): ResponseInterface
{
$this->conferenceRepository->add($conference);
return $this->redirect('list');
}
Options:
limit- Maximum number of calls allowed within the interval. Default:
5. interval- Time window as a string parseable by
\DateInterval or
strtotime(), for
example
'15 minutes'or'1 hour'. Default:'15 minutes'. policy- Throttling algorithm. Only
'sliding_is available. Default:window' 'sliding_.window' message- LLL key for the message shown when the limit is
exceeded. Resolved via
Localizationagainst the current extension. Leave empty to use the built-in Extbase default message.Utility
When the limit is exceeded, Extbase returns an HTTP 429 Too many requests
response. Dispatch a
Before
listener to customize this response.
errorAction: Extbase validation and argument-mapping errors
If argument mapping or validation fails, Extbase calls
error
instead of the original action. The default implementation either dispatches back
to the referring action (re-displaying the form with validation errors) or
returns a plain HTTP 400 Bad Request text response.
Override
get to customize the flash message that
appears when validation fails:
#[\Override]
protected function getErrorFlashMessage(): bool|string
{
return match ($this->actionMethodName) {
'createAction' => 'Please correct the errors in the form.',
default => parent::getErrorFlashMessage(),
};
}
Return
false to suppress the flash message.
For full control over the error response, override
error.
Make sure you return a
\Response — the contract
is the same as for normal actions.
See also
- Extbase validation
for how validation rules are configured on model properties and action parameters via
#attributes.[Validate] - Property mapping: request arguments to objects for how to allow properties on action arguments and configure type converters.