---
title: "ActionController: actions, arguments and responses"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:extbase-action-controller-events@main"
source: "ExtensionArchitecture/Extbase/Controller/ActionController.rst"
modified: "2026-09-15T06:32:51+00:00"
---

# `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\CMS\Extbase\Mvc\Controller\ActionController`.

**On this page**

-   [Structure of an Extbase ActionController](https://docs.typo3.org/permalink/t3coreapi:structure-of-an-extbase-actioncontroller@main)
-   [Action arguments and automatic Extbase object resolution](https://docs.typo3.org/permalink/t3coreapi:action-arguments-and-automatic-extbase-object-resolution@main)
-   [Accessing TypoScript settings in an Extbase controller](https://docs.typo3.org/permalink/t3coreapi:accessing-typoscript-settings-in-an-extbase-controller@main)
-   [Extbase action response helpers](https://docs.typo3.org/permalink/t3coreapi:extbase-action-response-helpers@main)
-   [Redirecting and forwarding from an Extbase action](https://docs.typo3.org/permalink/t3coreapi:redirecting-and-forwarding-from-an-extbase-action@main)
-   [Flash messages in Extbase controllers](https://docs.typo3.org/permalink/t3coreapi:flash-messages-in-extbase-controllers@main)
-   [initializeAction and per-action initialization in Extbase](https://docs.typo3.org/permalink/t3coreapi:initializeaction-and-per-action-initialization-in-extbase@main)
-   [Protecting Extbase actions with #[Authorize]](https://docs.typo3.org/permalink/t3coreapi:protecting-extbase-actions-with-authorize@main)
-   [Rate-limiting Extbase actions with #[RateLimit]](https://docs.typo3.org/permalink/t3coreapi:rate-limiting-extbase-actions-with-ratelimit@main)
-   [errorAction: Extbase validation and argument-mapping errors](https://docs.typo3.org/permalink/t3coreapi:erroraction-extbase-validation-and-argument-mapping-errors@main)

## Structure of an Extbase `ActionController`

Controllers live in `Classes/Controller/`. Public methods with a name
ending in `Action` are actions that can be mapped to plugin actions or
backend module actions.

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
<?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\ActionController`.
-   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](https://docs.typo3.org/permalink/t3coreapi:dependency-injection@main). Injected dependencies
    must be `protected readonly`, not `private 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 class `readonly`
    instead — but controllers extend `ActionController` and cannot use
    `readonly class`.
-   Every action method must return a
    `\Psr\Http\Message\ResponseInterface`.
-   **Frontend plugins:** use `$this->view->assign()` to pass variables
    to the Fluid template and return `$this->htmlResponse()` to render it.
    Actions can also return JSON, a redirect, or any other
    PSR-7
    response.
-   **Backend modules:** inject
    `ModuleTemplateFactory`, create a
    `ModuleTemplate` instance per action,
    assign variables via `$moduleTemplate->assignMultiple()`, and return
    `$moduleTemplate->renderResponse('ActionName')`. Do not use
    `$this->view` or `$this->htmlResponse()` in a module controller.
    See [Registering an Extbase backend module](https://docs.typo3.org/permalink/t3coreapi:extbase-registration-backend-module@main).

## 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\CMS\Extbase\DomainObject\AbstractEntity`) triggers a repository
lookup. Extbase converts the incoming UID to a fully
hydrated object.

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
public function showAction(Conference $conference): ResponseInterface
{
    $this->view->assign('conference', $conference);
    return $this->htmlResponse();
}
```

If, for example, a URL includes `?tx_myextension_pi1[conference]=5`, Extbase
loads `Conference` with UID 5 from the repository and passes the object
directly to `showAction()`. 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 `errorAction()` 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:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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 `errorAction()` instead. See
[errorAction: Extbase validation and argument-mapping errors](https://docs.typo3.org/permalink/t3coreapi:extbase-controller-action-error@main).

> [!NOTE]
> **See also**
>
> [Property mapping: request arguments to objects](https://docs.typo3.org/permalink/t3coreapi:extbase-controller-propertymapping@main)
> 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`:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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.tx_myextension.settings.*`,
`plugin.tx_myextension_myplugin.settings.*`, and any FlexForm
overrides. It has nothing to do with
[site settings](https://docs.typo3.org/permalink/t3coreapi:sitehandling-settings@main) — those are a separate
configuration layer defined in `settings.yaml`. 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.tx_myextension.settings.*` end up in
`$this->settings`. The full resolution order is covered in
[Extbase TypoScript configuration](https://docs.typo3.org/permalink/t3coreapi:extbase-configuration-typoscript-scopes@main).

## Extbase action response helpers

`ActionController` provides two
convenience methods for the most common response types:

-   **`htmlResponse(?string $html = null)`**

    Returns a `text/html`
    PSR-7
    response. Renders the current Fluid view without any arguments. Pass a string
    to use as the body.

-   **`jsonResponse(?string $json = null)`**

    Returns an `application/json` PSR-7 response. Renders the current view
    without any arguments (use with
    `\TYPO3\CMS\Extbase\Mvc\View\JsonView`). Pass a JSON string for the response.

For any other status code or content types, build the response manually using
the injected `$this->responseFactory` and `$this->streamFactory`:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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\Http\Message\ResponseInterface` — 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"](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/303)
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](https://en.wikipedia.org/wiki/Post/Redirect/Get).

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
// 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 |
| --- | --- | --- |
| `$actionName` | (required) | Name of the target action without the `Action` suffix, for example `'list'`. |
| `$controllerName` | `null` | Short class name of the target controller. `null` means the current controller. |
| `$extensionName` | `null` | Extension name in UpperCamelCase. `null` means the current extension. |
| `$arguments` | `[]` | Array of arguments appended to the target URL as query parameters. |
| `$pageUid` | `null` | UID of the target page. `null` keeps the current page. |
| `$statusCode` | `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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/307)
> 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:
>
> **EXT:my_extension/Classes/Controller/ConferenceController.php**
>
> ```php
> return $this->redirect('create', 'Conference', null, [], null, 307);
> ```
>
> This situation does not arise in typical Extbase form handling, where
> `ForwardResponse` is the right tool for re-processing within the same
> request without a browser round-trip.

### `redirectToUri()` — redirect to an arbitrary URL

`redirectToUri(string|\Psr\Http\Message\UriInterface $uri)` 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 `UriBuilder`:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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->uriBuilder` 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

`ForwardResponse` 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:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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, `ForwardResponse`
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.

`ForwardResponse` accepts action name,
`withControllerName()`, `withExtensionName()`, and
`withArguments()` — 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:flashMessages />`. They
are also useful for in-page feedback when no redirect is involved. The message
is rendered in the same response.

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

$this->addFlashMessage(
    'Conference was saved.',
    'Success',
    ContextualFeedbackSeverity::OK,
);
return $this->redirect('list');
```

The five severity levels are:

-   `ContextualFeedbackSeverity::NOTICE`
-   `ContextualFeedbackSeverity::INFO`
-   `ContextualFeedbackSeverity::OK`
-   `ContextualFeedbackSeverity::WARNING`
-   `ContextualFeedbackSeverity::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:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
$this->addFlashMessage('Could not process your request.', '', ContextualFeedbackSeverity::ERROR, false);
```

Render them in Fluid with the `<f:flashMessages />` ViewHelper.

## `initializeAction` and per-action initialization in Extbase

`initializeAction()` 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
`initializeCreateAction()` before `createAction()`). Extbase calls
it automatically before the corresponding action:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
<?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](https://docs.typo3.org/permalink/t3coreapi:extbase-controller-propertymapping-typeconverters@main).

## Protecting Extbase actions with `#[Authorize]`

> [!NOTE]
> **New in version 14.0**

The `#[Authorize]` 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.

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
<?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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403) response
    via `ErrorController`.

-   **`requireGroups: [42]` or `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: 'methodName'` or `callback: [SomeClass::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.php**

    ```php
    public function isOwner(Conference $conference): bool
    {
        $currentUserId = $this->request->getAttribute('frontend.user')?->getUserId();
        return $currentUserId !== null
            && $conference->getOwnerUid() === $currentUserId;
    }
    ```

When access is denied, Extbase dispatches
`BeforeActionAuthorizationDeniedEvent`
which lets event listeners return a custom response instead of the default 403.

> [!NOTE]
> **See also**
>
> [Extbase PHP attributes reference](https://docs.typo3.org/permalink/t3coreapi:extbase-appendix-attributes@main)
> for the full attribute reference including the event for custom denial responses.

## Rate-limiting Extbase actions with `#[RateLimit]`

> [!NOTE]
> **New in version 14.0**

`#[RateLimit]` 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.

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
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](https://www.php.net/manual/en/class.dateinterval.php) or
    [strtotime()](https://www.php.net/manual/en/function.strtotime.php), for
    example `'15 minutes'` or `'1 hour'`.
    Default: `'15 minutes'`.

-   **`policy`**

    Throttling algorithm. Only `'sliding_window'` is available.
    Default: `'sliding_window'`.

-   **`message`**

    LLL key for the message shown when the limit is
    exceeded. Resolved via `LocalizationUtility` against the current
    extension. Leave empty to use the built-in Extbase default message.

When the limit is exceeded, Extbase returns an [HTTP 429 Too many requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429)
response. Dispatch a
`BeforeActionRateLimitResponseEvent`
listener to customize this response.

## `errorAction`: Extbase validation and argument-mapping errors

If argument mapping or validation fails, Extbase calls `errorAction()`
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/400) text response.

Override `getErrorFlashMessage()` to customize the flash message that
appears when validation fails:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
#[\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 `errorAction()`.
Make sure you return a `ResponseInterface` — the contract
is the same as for normal actions.

> [!NOTE]
> **See also**
>
> -   [Extbase validation](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-overview@main)
>     for how validation rules are configured on model properties and action parameters via
>     `#[Validate]` attributes.
> -   [Property mapping: request arguments to objects](https://docs.typo3.org/permalink/t3coreapi:extbase-controller-propertymapping@main)
>     for how to allow properties on action arguments and configure type converters.
