For developers 

The job list asks one service for its postings, and that service dispatches two PSR-14 events: one before the request is sent to the B-ITE API, one after the response is decoded. They are the supported way to filter by a B-ITE custom field, to ask for another locale, or to remove, enrich and group the postings, without copying the service or the controller.

Which classes of this extension are public API, and what that promises, is stated for all academic extensions on the extension points page of academic_base. The job list also dispatches ModifyPluginViewEvent of academic_base when it renders, after both events, and that page describes it.

The request event 

\FGTCLB\AcademicBiteJobs\Event\ModifyBiteJobPostingsRequestEvent is dispatched each time a job list renders, after the request is built from the plugin settings and before it is sent. The payload a listener hands back is sent as it is, encoded as JSON.

Method Returns
getPayload() , setPayload() The request sent to the B-ITE search API, see the keys below.
getSettings() The values stored below settings.jobs in the plugin settings of the content element, which the payload is built from. They are not merged with TypoScript and not normalised, see below.
getRequest() The request of the page being rendered.
getPluginControllerActionContext() The context of the job list that asked, with all its settings, TypoScript included, or null when the service was called outside of a plugin.

The keys of the payload 

The extension sends these keys. A listener changes any of them and adds every other key the B-ITE search API accepts. Renaming or removing one of them in a later version is a breaking change.

Key Sent by the extension
key The job listing key of the content element.
channel 0.
locale de.
page offset 0: the postings from the first one on.
filter An empty filter: every posting of the job listing.
sort order and by, the sort direction and the sort field of the content element.

The result event 

\FGTCLB\AcademicBiteJobs\Event\ModifyBiteJobPostingsEvent is dispatched each time a job list renders, after the response is decoded. It is dispatched after a failed request as well, then with no postings and no response data, so a listener does not need to know how the request went.

Method Returns
getJobPostings() , setJobPostings() The postings the job list renders, a list with one array per posting as B-ITE answers it. The setter refuses anything else.
getResponseData() The decoded response of the B-ITE API, or an empty array when the request failed or the response was not JSON.
getSettings() The values stored below settings.jobs in the plugin settings of the content element, as in the request event.
getRequest() The request of the page being rendered.
getPluginControllerActionContext() The context of the job list that asked, as in the request event.

The limit of the content element is applied to the postings the listeners hand back, so a listener sees every posting of the response and the job list never renders more than the limit.

Example: a custom field filter and a grouping 

Up to version 2.0 the extension filtered by the B-ITE custom field zuordnung of one installation and grouped the postings by its value. Version 2.1 removed that code, see Breaking: Remove project specific custom fields. Two listeners bring it back for the installation that needs it, and only there.

The first one adds the filter to the request. The value comes from a plugin setting of the project's own FlexForm, here settings.jobs.relation:

EXT:my_extension/Classes/EventListener/FilterJobsByRelation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

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

final class FilterJobsByRelation
{
    #[AsEventListener(identifier: 'my-extension/filter-jobs-by-relation')]
    public function __invoke(ModifyBiteJobPostingsRequestEvent $event): void
    {
        $relation = (string)($event->getSettings()['relation'] ?? '');
        if ($relation === '' || $relation === 'all') {
            return;
        }
        $payload = $event->getPayload();
        $payload['filter'] = ['custom.zuordnung' => ['in' => [$relation]]];
        $event->setPayload($payload);
    }
}
Copied!

The second one writes the name of the relation into every posting, so the job list can group by it:

EXT:my_extension/Classes/EventListener/NameTheRelationOfEveryJob.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

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

final class NameTheRelationOfEveryJob
{
    private const RELATION_NAMES = [
        '01' => 'Appointment procedures',
        '02' => 'Academic staff',
        '03' => 'Non-scientific staff',
        '04' => 'Training positions',
    ];

    #[AsEventListener(identifier: 'my-extension/name-the-relation-of-every-job')]
    public function __invoke(ModifyBiteJobPostingsEvent $event): void
    {
        $jobs = $event->getJobPostings();
        foreach ($jobs as $index => $job) {
            $relation = (string)($job['custom']['zuordnung'] ?? '');
            $jobs[$index]['relationName'] = self::RELATION_NAMES[$relation] ?? '';
        }
        $event->setJobPostings($jobs);
    }
}
Copied!

The grouping itself is TypoScript, see Grouping the jobs:

TypoScript setup
plugin.tx_academicbitejobs.settings.jobs.groupBy = relationName
Copied!

Where the custom field sits in a posting, and which values it has, depends on how it is set up in B-ITE, so read one response of your job listing before relying on the path above. A listener that needs the labels of the values can ask the options API of B-ITE for them.

The upgrade wizard academicBiteJobs_listViewFlexFormUpgradeWizard removes the setting settings.jobs.custom.zuordnung of version 2.0 from every job list, so a project FlexForm that brings the field back gives it a name of its own, as settings.jobs.relation above.

Rules worth knowing 

Two kinds of settings. getSettings() returns what the content element stores below settings.jobs , exactly as it is stored: without the TypoScript of the plugin, and with the view value of a content element saved with version 2.0 as it was saved. The settings the job list works with, the TypoScript settings.jobs.groupBy included, are those of the plugin action context, $event->getPluginControllerActionContext()?->getSettings() .

The events run when the page is rendered, not per visitor. A page is cached with the postings the listeners handed back, so a listener cannot show a different list to different visitors of a cached page.

A listener is not guarded. An exception a listener throws reaches the visitor, as any error of project code does. A payload that cannot be encoded as JSON is logged like a failed request, and the job list renders the postings the result listeners hand back.

Two job lists on a page are two requests. Each job list sends its own request and dispatches both events once. A job list whose request fails renders no postings, whatever another job list on the page received.