ActionController
Most Extbase controllers are based on the
\TYPO3\
. It is theoretically
possible to base a controller directly on the
\TYPO3\
, however there are
rarely use cases for that. Implementing the Controller
does not
guarantee a controller to be dispatchable. It is not recommended to base
your controller directly on the Controller
.
Actions
Most public and protected methods that end in "Action" (for example
index
or show
),
are automatically registered as actions of the controller.
Controller actions must return an instance of the
\Psr\
.
Many of these actions have parameters. You should use strong types for the parameters as this is necessary for the validation.
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
class BlogController extends AbstractController
{
/**
* Displays a form for creating a new blog
*/
public function newAction(?Blog $newBlog = null): ResponseInterface
{
$this->view->assignMultiple([
'newBlog' => $newBlog,
'administrators' => $this->administratorRepository->findAll(),
]);
return $this->htmlResponse();
}
}
The validation of domain object can be explicitly disabled by the annotation
@TYPO3\
. This might be necessary
in actions that show forms or create domain objects.
Default values can, as usual in PHP, just be indicated in the method signature.
In the above case, the default value of the parameter $new
is set to
NULL
.
If the action should render the view you can return $this->html
as a shortcut for taking care of creating the response yourself.
In order to redirect to another action, return $this->redirect
:
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use T3docs\BlogExample\Exception\NoBlogAdminAccessException;
class BlogController extends AbstractController
{
/**
* Updates an existing blog
*
* $blog is a not yet persisted clone of the original blog containing
* the modifications
*
* @throws NoBlogAdminAccessException
*/
public function updateAction(Blog $blog): ResponseInterface
{
$this->checkBlogAdminAccess();
$this->blogRepository->update($blog);
$this->addFlashMessage('updated');
return $this->redirect('index');
}
}
If an exception is thrown while an action is executed you will receive the "Oops an error occurred" screen on a production system or a stack trace on a development system with activated debugging.
Note
The methods
initialize
, initialize
and
error
have special meanings in initialization and error handling
and are no Extbase actions.
Define initialization code
Sometimes it is necessary to execute code before calling an action. For example, if complex arguments must be registered, or required classes must be instantiated.
There is a generic initialization method called initialize
, which
is called after the registration of arguments, but before calling the
appropriate action method itself. After the generic initialize
, if
it exists, a method named initialize[ActionName](), for example
initialize
is called.
In this method you can perform action specific initializations.
In the backend controller of the blog example the method
initialize
is used to discover the page that is currently
activated in the page tree and save it in a variable:
class BackendController extends ActionController
{
protected function initializeAction(): void
{
$this->pageUid = (int)($this->request->getQueryParams()['id'] ?? 0);
}
}
Catching validation errors with errorAction
If an argument validation error has occurred, the method error
is called.
The default implementation sets a flash message, error response with HTTP status 400 and forwards back to the originating action.
This is suitable for most actions dealing with form input.
If you need a to handle errors differently this method can be overridden.
Hint
If a domain object should not be validated, for example in the middle of an
editing process, the validation of that object can be disabled by the
annotation @TYPO3\
.
Forward to a different controller
It is possible to forward from one controller action to an action of the same or a different controller. This is even possible if the controller is in another extension.
This can be done by returning a \TYPO3\
.
In the following example, if the current blog is not found in the
index action of the Post
, we follow to the list of blogs
displayed by the index
of the Blog
.
use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;
class PostController extends AbstractController
{
/**
* Displays a list of posts. If $tag is set only posts matching this tag are shown
*/
public function indexAction(
?Blog $blog = null,
string $tag = '',
int $currentPage = 1,
): ResponseInterface {
if ($blog == null) {
return (new ForwardResponse('index'))
->withControllerName('Blog')
->withExtensionName('blog_example')
->withArguments(['currentPage' => $currentPage]);
}
$this->blogPageTitleProvider->setTitle($blog->getTitle());
if (empty($tag)) {
$posts = $this->postRepository->findBy(['blog' => $blog]);
} else {
$tag = urldecode($tag);
$posts = $this->postRepository->findByTagAndBlog($tag, $blog);
$this->view->assign('tag', $tag);
}
$paginator = new QueryResultPaginator(
$posts,
$currentPage,
(int)($this->settings['itemsPerPage'] ?? 3),
);
$pagination = new SimplePagination($paginator);
$this->view->assignMultiple([
'paginator' => $paginator,
'pagination', $pagination,
'pages' => range(1, $pagination->getLastPageNumber()),
'blog' => $blog,
'posts' => $posts,
]);
return $this->htmlResponse();
}
}
Forwards only work when the target controller and action is properly registered
as an allowed pair. This can be done via an extension's ext_
file
in the relevant Extension
section, or by
filling the $GLOBALS
array and tt_
TypoScript.
Otherwise, the object class name of your target controller cannot be resolved properly,
and container instantiation will fail.
The corresponding example is:
<?php
defined('TYPO3') or die();
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use FriendsOfTYPO3\BlogExample\Controller\PostController;
use FriendsOfTYPO3\BlogExample\Controller\CommentController;
ExtensionUtility::configurePlugin(
'BlogExample',
'PostSingle',
[PostController::class => 'show', CommentController::class => 'create'],
[CommentController::class => 'create']
);
Here, the plugin Blog
would allow jumping between the controllers
Post
and Comment
. To also allow
Blog
in the example above, it would need to get added
like this:
<?php
defined('TYPO3') or die();
use T3docs\BlogExample\Controller\BlogController;
use T3docs\BlogExample\Controller\CommentController;
use T3docs\BlogExample\Controller\PostController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
ExtensionUtility::configurePlugin(
'BlogExample',
'PostSingle',
[
PostController::class => 'show',
CommentController::class => 'create',
BlogController::class => 'index',
],
[
// Non-cached actions
CommentController::class => 'create',
],
);
// ...
Stop further processing in a controller's action
Sometimes you may want to use an Extbase controller action to return a specific output, and then stop the whole request flow.
For example, a download
might provide some binary data,
and should then stop.
By default, Extbase actions need to return an object of type
\Psr\
as described above. The actions
are chained into the TYPO3 request flow (via the page renderer), so the
returned object will be enriched by further processing of TYPO3. Most
importantly, the usual layout of your website will be surrounded
by your Extbase action's returned contents, and other plugin outputs may
come before and after that.
In a download action, this would be unwanted content. To prevent that
from happening, you have multiple options. While you might think placing
a die
or exit
after your download action processing
is a good way, it is not very clean.
The recommended way to deal with this, is to use a
PSR-15 middleware implementation. This is more performant,
because all other request workflows do not even need to be executed, because no other
plugin on the same page needs to be rendered. You would refactor your code so that
download
is not executed (e.g. via <f:
), but instead
point to your middleware routing URI, let the middleware properly
create output, and finally stop its processing by a concrete
\Psr\
result object,
as described in the Middleware chapters.
If there are still reasons for you to utilize Extbase for this, you can use
a special method to stop the request workflow. In such a case a
\TYPO3\
can be thrown. This is automatically
caught by a PSR-15 middleware and the given PSR-7 response is then returned directly.
Example:
<?php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
final class MyController extends ActionController
{
public function downloadAction(): ResponseInterface
{
// ... do something (set $filename, $filePath, ...)
$response = $this->responseFactory->createResponse()
// Must not be cached by a shared cache, such as a proxy server
->withHeader('Cache-Control', 'private')
// Should be downloaded with the given filename
->withHeader('Content-Disposition', sprintf('attachment; filename="%s"', $filename))
->withHeader('Content-Length', (string)filesize($filePath))
// It is a PDF file we provide as a download
->withHeader('Content-Type', 'application/pdf')
->withBody($this->streamFactory->createStreamFromFile($filePath));
throw new PropagateResponseException($response, 200);
}
}
Also, if your controller needs to perform a redirect to a defined URI (internal or external),
you can return a specific object through the response
:
<?php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
final class MyController extends ActionController
{
public function redirectAction(): ResponseInterface
{
// ... do something (set $value, ...)
$uri = $this->uriBuilder->uriFor('show', ['parameter' => $value]);
// $uri could also be https://example.com/any/uri
// $this->resourceFactory is injected as part of the `ActionController` inheritance
return $this->responseFactory->createResponse(307)
->withHeader('Location', $uri);
}
}
Hint
If you want to return a JSON response, see Responses to achieve this
with a special $this->json
method.
Events
Two PSR-14 events are available: