---
title: "ModifyProgramDemandEvent"
manual: "Academic Programs"
version: "main"
permalink: "https://docs.typo3.org/permalink/fgtclb/academic-programs:developers-event-modify-program-demand-event@main"
source: "Developers/Events/ModifyProgramDemandEvent.rst"
rendered: "2026-10-17T08:47:58+00:00"
---

# ModifyProgramDemandEvent {#developers-event-modify-program-demand-event}

`\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 {#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](https://docs.typo3.org/permalink/fgtclb/academic-programs:feature-1790226102@main). 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](https://docs.typo3.org/permalink/fgtclb/academic-programs:developers-event-modify-program-list-event@main) follows once the programs
are queried.

## What a listener reads and changes {#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:

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

## Example {#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
<?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),
        ));
    }
}
```

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 {#developers-program-list-events-rules}

> [!WARNING]
> **A demand listener widens as easily as it narrows.** The demand *is* the
> query here, not a constraint added to one, and nothing guards it:
> `setShowHiddenRecords(true)` shows hidden program pages to every
> visitor, `setPages([])` drops the storage restriction the editor
> chose in the content element, and `setSorting()` overrides the
> editor's ordering for every element that renders, though only for a
> value that is one of the `SortingOptions` constants, since anything
> else is ignored without a word. Two listeners that disagree are resolved by
> the order they run in, and the last one wins.
>
> What a listener cannot undo: the page type is pinned unconditionally,
> `showHiddenRecords` reaches the `disabled` flag alone (start- and
> endtime, `fe_group` and `deleted` stay in effect), and a
> `uid` tiebreaker is always appended to the ordering.

**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](https://docs.typo3.org/permalink/fgtclb/academic-programs:program-list-subcategories@main)). 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](https://docs.typo3.org/permalink/fgtclb/academic-programs:configuration-active-filters@main) 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 {#introduced-in}

-   [Feature: Change the program list, finder and page by event](https://docs.typo3.org/permalink/fgtclb/academic-programs:feature-1790604743@main) added the event in 3.0.
-   [Breaking: The program controllers are final](https://docs.typo3.org/permalink/fgtclb/academic-programs:breaking-1791043408@main) made the controllers final and names the event
    as the replacement of a subclass that changed the demand.
-   [Feature: Program filter selections have a URL](https://docs.typo3.org/permalink/fgtclb/academic-programs:feature-1790226102@main) answers a filter submission with a redirect, so
    the event fires on the request that follows it.
