Breaking: The project controller is final 

Description 

\FGTCLB\AcademicProjects\Controller\ProjectController is final . It serves both project list plugins, ProjectList and ProjectListSingle.

Its collaborators are private constructor arguments now. The methods injectFilterRedirectExtensionService() and injectFilterTypeResolver() are removed, and redirectFilterSubmission() and pluginControllerActionContext() are private. All of them existed only so that a subclass could keep calling the old constructor and the helpers of the shipped action.

Every plugin controller of the academic extensions is final in 3.0. A plugin is extended through its events, not through a subclass of its controller.

Impact 

A class that extends ProjectController stops loading with a fatal error, Class ... cannot extend final class FGTCLB\AcademicProjects\Controller\ProjectController . That happens as soon as anything loads the subclass, a plugin registered with it renders, or the container is built with it.

An XCLASS of the controller fails the same way. The upgrade check academic:upgrade:check of EXT:academic_base reports it as an error. A configurePlugin() call that points one of the two plugins at a subclass is not reported.

The plugins, their templates and their settings are unchanged. The behaviour is the same on TYPO3 v13 and v14.

Affected Installations 

Installations with a class that extends ProjectController , registered for a plugin through ExtensionUtility::configurePlugin() , as an XCLASS, or in the service container.

Migration 

Remove the subclass, and the configurePlugin() call or the XCLASS registration that points at it, so the shipped controller serves the plugin again. Move each override to its replacement:

  • Changing the filter, the status or the sorting before the query: a listener of \FGTCLB\AcademicProjects\Event\ModifyProjectDemandEvent , which adjusts or replaces the demand. See Feature: Change the project lists by event.
  • A replaced or reduced result, other filter categories, or additional view variables: a listener of \FGTCLB\AcademicProjects\Event\ModifyProjectListEvent .
  • Filter selections that can be bookmarked: the list answers a filter submission with a redirect to a GET URL, see Feature: Project filter selections have a URL.

A subclass that adds a view variable:

Before: EXT:my_extension/Classes/Controller/ProjectController.php
final class ProjectController extends \FGTCLB\AcademicProjects\Controller\ProjectController
{
    public function listAction(?array $demand = null): ResponseInterface
    {
        $this->view->assign('fundingBodies', $this->fundingBodyRepository->findAll());
        return parent::listAction($demand);
    }
}
Copied!

does the same as a listener:

After: EXT:my_extension/Classes/EventListener/AddFundingBodiesToProjectList.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use FGTCLB\AcademicProjects\Event\ModifyProjectListEvent;
use MyVendor\MyExtension\Domain\Repository\FundingBodyRepository;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class AddFundingBodiesToProjectList
{
    public function __construct(
        private FundingBodyRepository $fundingBodyRepository,
    ) {}

    #[AsEventListener(identifier: 'my-extension/add-funding-bodies-to-project-list')]
    public function __invoke(ModifyProjectListEvent $event): void
    {
        $event->getView()->assign('fundingBodies', $this->fundingBodyRepository->findAll());
    }
}
Copied!

The listener acts on both plugins. It reads $event->getPluginControllerActionContext()->getPluginName() where it means only one of them.