---
title: "Breaking: The B-ITE jobs controller is final"
manual: "Academic Bite Jobs"
version: "main"
permalink: "https://docs.typo3.org/permalink/fgtclb/academic-bite-jobs:breaking-1791043409@main"
source: "Changelog/3.0/Breaking-BiteJobsControllerIsFinal.rst"
rendered: "2026-10-09T15:00:56+00:00"
---

# Breaking: The B-ITE jobs controller is final {#breaking-1791043409}

## Description {#description}

`\FGTCLB\AcademicBiteJobs\Controller\BiteJobsController` is
`final`. It serves the job list plugin `List`. Its job service is a
private constructor argument now.

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 {#impact}

A class that extends `BiteJobsController` stops loading with a fatal
error, `Class ... cannot extend final class
FGTCLB\AcademicBiteJobs\Controller\BiteJobsController`. 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 the plugin at a subclass
is not reported.

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

## Affected Installations {#affected-installations}

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

## Migration {#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, see [Feature: Request and result events for the B-ITE job list](https://docs.typo3.org/permalink/fgtclb/academic-bite-jobs:feature-1790774400@main):

-   Changing the request to B-ITE, its filter, channel, locale, sorting or
    paging: a listener of
    `\FGTCLB\AcademicBiteJobs\Event\ModifyBiteJobPostingsRequestEvent`.
-   Removing, enriching or grouping the postings: a listener of
    `\FGTCLB\AcademicBiteJobs\Event\ModifyBiteJobPostingsEvent`.
-   Additional view variables: a listener of the plugin view event of
    **academic_base**, see [Feature: The plugin dispatches the plugin view event](https://docs.typo3.org/permalink/fgtclb/academic-bite-jobs:feature-plugin-view-event@main).

A subclass that drops postings:

**Before: EXT:my_extension/Classes/Controller/BiteJobsController.php**

```php
final class BiteJobsController extends \FGTCLB\AcademicBiteJobs\Controller\BiteJobsController
{
    public function listAction(): ResponseInterface
    {
        $jobs = $this->biteJobsService->fetchBiteJobs($this->request);
        $this->view->assign('jobs', array_filter($jobs, $this->isPublished(...)));
        return $this->htmlResponse();
    }
}
```

does the same as a listener, with one difference: the listener runs before the
limit of the content element is applied, so the list still shows as many
postings as the limit allows:

**After: EXT:my_extension/Classes/EventListener/SkipUnpublishedPostings.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use FGTCLB\AcademicBiteJobs\Event\ModifyBiteJobPostingsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class SkipUnpublishedPostings
{
    #[AsEventListener(identifier: 'my-extension/skip-unpublished-postings')]
    public function __invoke(ModifyBiteJobPostingsEvent $event): void
    {
        $event->setJobPostings(array_values(array_filter(
            $event->getJobPostings(),
            static fn(array $posting): bool => ($posting['published'] ?? true) !== false,
        )));
    }
}
```
