ModifyProgramDemandEvent 

\FGTCLB\AcademicPrograms\Event\ModifyProgramDemandEvent hands a listener the demand a program list or a program finder is about to be queried with. The demand a listener hands back is the one that is queried, and in the list the one assigned to the view as {demand} .

When it is dispatched 

ProgramController dispatches the event once per rendering, in two actions:

  • listAction() of the Course List, after the demand is built from the settings of the content element and the selection of the visitor, and before the programs are queried.
  • finderAction() of the Course Finder, after the demand is built from the settings of the content element, and before the programs the finder offers its categories for are queried. The finder has no selection of the visitor to read.

The list answers a submission of its filter and sorting form with a redirect to a URL carrying the selection, before the event. The event fires on the request that follows, with the selection in the query string (or the route), not in the parsed body, see Feature: Program filter selections have a URL. The finder submits to the list on its target page, so a finder submission reaches the event of that list, not the one of the finder.

ModifyProgramListEvent follows once the programs are queried.

What a listener reads and changes 

Method What it does
getDemand() The demand, \FGTCLB\AcademicPrograms\Domain\Model\Dto\ProgramDemand . It is an object, so a change to it takes effect without setDemand() .
setDemand() Replaces the demand that is queried.
getPluginControllerActionContext() The context of the rendering, \FGTCLB\AcademicBase\Domain\Model\Dto\PluginControllerActionContextInterface : the request, the site and its language, the content object of the element, the settings of the content element, the plugin name and the action name.

What the query reads from the demand:

Getter and setter Meaning
getPages() , setPages() The uids of the pages the program pages sit directly below: the Startingpoint of the element with its Recursive levels already resolved. Empty means every program page of the installation.
getFilterCollection() , setFilterCollection() The selected categories, those of the visitor or the Default categories of the element, as a \FGTCLB\CategoryTypes\Collection\FilterCollection , or null for none. A program has to carry every one of them.
getIncludeSubcategories() , setIncludeSubcategories() Whether each selected category also finds the programs that carry one of its subcategories, the field Include subcategories of the element.
getSorting() , setSorting() , and the field and the direction on their own The ordering, one of the values of \FGTCLB\AcademicPrograms\Enumeration\SortingOptions , for example title asc.
getShowHiddenRecords() , setShowHiddenRecords() Whether hidden program pages are listed, the field Show hidden records of the element.

The plugin name on the context is the name the plugin was registered with, not the content element type: ProgramList for the list and ProgramFinder for the finder. It is how one listener serves both and acts on one:

if ($event->getPluginControllerActionContext()->getPluginName() !== 'ProgramList') {
    return;
}
Copied!

Example 

A site lists only full-time programs in its program lists, whatever else the visitor selects. Full-time is the category with the uid 42 of the category type program_type. The listener adds it to the categories of the demand:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use FGTCLB\AcademicPrograms\Event\ModifyProgramDemandEvent;
use FGTCLB\CategoryTypes\Collection\FilterCollection;
use FGTCLB\CategoryTypes\Domain\Repository\CategoryRepository;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class ListOnlyFullTimePrograms
{
    public function __construct(
        private CategoryRepository $categoryRepository,
    ) {}

    #[AsEventListener(identifier: 'my-extension/list-only-full-time-programs')]
    public function __invoke(ModifyProgramDemandEvent $event): void
    {
        $demand = $event->getDemand();
        // The categories the visitor selected, and the full-time category 42.
        $uids = [42];
        foreach ($demand->getFilterCollection()?->getFilterCategories() ?? [] as $category) {
            $uids[] = $category->getUid();
        }
        $demand->setFilterCollection(new FilterCollection(
            $this->categoryRepository->findByGroupAndUidList('programs', $uids),
        ));
    }
}
Copied!

The listener changes the demand it was handed rather than building a new one, so the storage, the sorting and the other choices of the editor and the visitor stay. It acts on the list and the finder alike: the finder then disables every category no full-time program carries.

Rules and pitfalls 

Subcategories are added when the programs are queried. With the field Include subcategories of the element switched on, the demand holds the categories as they were selected, and the query widens each of them by its subcategories (see Matching subcategories). A category a listener adds is widened the same way, and a listener that calls setIncludeSubcategories() changes how every selected category matches.

A category a listener adds is shown as an active filter. The active filter tags of Active filters, reset link and result count read the demand the list was queried with, as the selects of the filter form do. A category a listener adds to every request is therefore a tag the visitor cannot remove, since the listener adds it again on the page the tag leads to, and every other tag link carries it. A listener that forces a category should leave the tags switched off, or override Partials/Program/ActiveFilters.html. A collection a listener builds without the type identifiers of the group, through new CategoryCollection() and attach() , still filters the list but gets no tags, because the tags are grouped by type.

A replaced demand starts from the defaults. setDemand() is there for a listener that builds its own demand, and such a demand carries none of what the plugin put in the one it was handed: the storage pages, the showHiddenRecords choice of the editor, the subcategory option, and the two the visitor can set themselves through the filter form, the sorting and the filterCollection . A replacement therefore resets the ordering and the category filter under them. Mutate the demand where that is enough, and carry getPages() , getShowHiddenRecords() , getIncludeSubcategories() , getFilterCollection() and the sorting over where it is not.

A field the project adds to the filter form does not reach the event. The redirect after a submission carries the selection the plugin accepted and nothing else, so a listener cannot read a field of its own from the request that follows.

Introduced in