Breaking: The program controllers are final 

Description 

Both controllers of this extension are final :

  • \FGTCLB\AcademicPrograms\Controller\ProgramController serves the program list (ProgramList) and the program finder (ProgramFinder).
  • \FGTCLB\AcademicPrograms\Controller\DetailsController serves the program details (ProgramDetails).

Their collaborators are private constructor arguments now. The methods injectFilterRedirectExtensionService() , injectFilterTypeResolver() and injectProgramFactsBuilder() are removed, and redirectFilterSubmission() and pluginControllerActionContext() of the program controller are private. All of them existed only so that a subclass could keep calling the old constructor and the helpers of the shipped actions. The details controller no longer takes the demand factory, which it never used.

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 one of the two controllers stops loading with a fatal error, Class ... cannot extend final class FGTCLB\AcademicPrograms\Controller\ProgramController for instance. 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 either 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 three 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 ProgramController or DetailsController , 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:

A subclass that adds a view variable to the details:

Before: EXT:my_extension/Classes/Controller/DetailsController.php
final class DetailsController extends \FGTCLB\AcademicPrograms\Controller\DetailsController
{
    public function showAction(): ResponseInterface
    {
        $this->view->assign('applicationPeriods', $this->applicationPeriodRepository->findAll());
        return parent::showAction();
    }
}
Copied!

does the same as a listener:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use FGTCLB\AcademicBase\Event\ModifyPluginViewEvent;
use MyVendor\MyExtension\Domain\Repository\ApplicationPeriodRepository;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class AddApplicationPeriodsToProgramDetails
{
    public function __construct(
        private ApplicationPeriodRepository $applicationPeriodRepository,
    ) {}

    #[AsEventListener(identifier: 'my-extension/add-application-periods-to-program-details')]
    public function __invoke(ModifyPluginViewEvent $event): void
    {
        $context = $event->getPluginControllerActionContext();
        if ($context->getControllerExtensionName() !== 'AcademicPrograms'
            || $context->getPluginName() !== 'ProgramDetails'
        ) {
            return;
        }
        $event->getView()->assign('applicationPeriods', $this->applicationPeriodRepository->findAll());
    }
}
Copied!

The plugin view event is dispatched by every plugin of the academic extensions, so the listener checks for the one it means.