Controller layer in Extbase 

The controller layer sits between the request and the view. It handles the request, coordinates repository and service calls, assigns variables for the view, 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. Direct database queries belong in repositories, not in controllers.

Responses are not tied to Fluid 

Fluid is the recommended way to render output but this is not a hard requirement. Every action must return a \Psr\Http\Message\ResponseInterface — what goes into that response is entirely up to the action.

Calling $this->htmlResponse() without arguments renders the Fluid template and wraps it in a response. Passing a string to $this->htmlResponse() skips Fluid and uses that string as the body directly:

EXT:my_extension/Classes/Controller/ConferenceController.php
// Render the Fluid template for the current action
return $this->htmlResponse();

// Return a hand-built HTML string without Fluid
return $this->htmlResponse('<p>Hello world</p>');

// Return JSON
return $this->jsonResponse(json_encode(['count' => 42]));

// Redirect to another action (returns a redirect response, not a body)
return $this->redirect('list');
Copied!

All of these satisfy the PSR-7 \Psr\Http\Message\ResponseInterface contract. Returning anything other than a \Psr\Http\Message\ResponseInterface from an action throws a \RuntimeException at dispatch time.

Injecting dependencies into Extbase controllers 

Dependencies are added to controllers via dependency injection. Two mechanisms are available:

Constructor injection is the standard approach. Declare dependencies as protected readonly constructor parameters and the DI container will provide them automatically:

EXT:my_extension/Classes/Controller/ConferenceController.php
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ConferenceController extends ActionController
{
    public function __construct(
        protected readonly ConferenceRepository $conferenceRepository,
    ) {}
}
Copied!

Inject methods should be used when a controller extends a parent class that already has a constructor. As PHP only allows one constructor per class, additional dependencies cannot be added without repeating the parent's parameter list. A public method named inject with the dependency name will receive the dependency instead.

A typical case is a child controller that extends a base controller from your own extension or a third-party package. The base controller already owns the constructor, so the child uses inject methods for its own dependencies:

EXT:my_extension/Classes/Controller/ConferenceController.php
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use MyVendor\MyExtension\Service\ConferenceService;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ConferenceController extends ActionController
{
    public function __construct(
        protected readonly ConferenceRepository $conferenceRepository,
    ) {}
}
Copied!
EXT:my_extension/Classes/Controller/SpecialConferenceController.php
use MyVendor\MyExtension\Controller\ConferenceController;
use MyVendor\MyExtension\Service\ConferenceService;

class SpecialConferenceController extends ConferenceController
{
    protected ConferenceService $conferenceService;

    public function injectConferenceService(
        ConferenceService $conferenceService,
    ): void {
        $this->conferenceService = $conferenceService;
    }
}
Copied!

Inject methods are a fully supported DI pattern, not a fallback. Constructor injection is the recommended best practice — dependencies are declared in one place and immediately visible to anyone reading the class. Inject methods are the cleaner solution when a constructor is already owned by a parent class and cannot be extended without repeating its full parameter list.

Both mechanisms can be combined in the same class. Injected properties should be protected, not private, so subclasses can access them.

In this chapter 

ActionController: actions, arguments and responses
Actions, action arguments and automatic object resolution, response helpers, redirect and forward, flash messages, per-action initializers, #[Authorize], #[RateLimit], and errorAction.
Property mapping: request arguments to objects
How raw request data is converted to typed PHP objects; the __trustedProperties mechanism; when manual allowlisting in initialize*Action() is necessary.