SlugRedirectChangeItemCreatedEvent

New in version 12.2

The PSR-14 event \TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent is fired in the \TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory class and allows extensions to manage the redirect sources for which redirects should be created.

TYPO3 already implements the EXT:redirects/Classes/EventListener/AddPlainSlugReplacementSource.php (GitHub) listener. It is used to add the plain slug value based source type, which provides the same behavior as before. Implementing this as a Core listener gives extension authors the ability to remove the source added by AddPlainSlugReplacementSource when their listeners are registered and executed afterwards. See the example below.

The implementation of the EXT:redirects/Classes/RedirectUpdate/RedirectSourceInterface.php (GitHub) interface is required for custom source classes. Using this interface enables automatic detection of implementations. Additionally, this allows to transport custom information and data.

Examples

Using the PageTypeSource

The source type implementation based on \TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource provides the page type number as additional value. The main use case for this source type is to provide additional source types where the source host and path are taken from a full built URI before the page slug change occurred for a specific page type. This avoids the need for extension authors to implement a custom source type for the same task, and instead providing a custom event listener to build sources for non-zero page types.

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

#[AsEventListener(
    identifier: 'my-extension/custom-page-type-redirect',
    after: 'redirects-add-page-type-zero-source',
)]
final readonly class MyEventListener
{
    private const CUSTOM_PAGE_TYPES = [1234, 169999];

    public function __invoke(
        SlugRedirectChangeItemCreatedEvent $event,
    ): void {
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();

        foreach (self::CUSTOM_PAGE_TYPES as $pageType) {
            try {
                $pageTypeSource = $this->createPageTypeSource(
                    $changeItem->getPageId(),
                    $pageType,
                    $changeItem->getSite(),
                    $changeItem->getSiteLanguage(),
                );
                if ($pageTypeSource === null) {
                    continue;
                }
            } catch (UnableToLinkToPageException) {
                // Could not properly link to page. Continue to next page type
                continue;
            }

            if ($this->isDuplicate($pageTypeSource, ...$sources)) {
                // not adding duplicate,
                continue;
            }

            $sources[] = $pageTypeSource;
        }

        // update sources
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(
                ...array_values($sources),
            ),
        );

        // update change item with updated sources
        $event->setSlugRedirectChangeItem($changeItem);
    }

    private function isDuplicate(
        PageTypeSource $pageTypeSource,
        RedirectSourceInterface ...$sources,
    ): bool {
        foreach ($sources as $existingSource) {
            if ($existingSource instanceof PageTypeSource
                && $existingSource->getHost() === $pageTypeSource->getHost()
                && $existingSource->getPath() === $pageTypeSource->getPath()
            ) {
                // we do not check for the type, as that is irrelevant. Same
                // host+path tuple would lead to duplicated redirects if
                // type differs.
                return true;
            }
        }
        return false;
    }

    private function createPageTypeSource(
        int $pageUid,
        int $pageType,
        Site $site,
        SiteLanguage $siteLanguage,
    ): ?PageTypeSource {
        if ($pageType === 0) {
            // pageType 0 is handled by \TYPO3\CMS\Redirects\EventListener\AddPageTypeZeroSource
            return null;
        }

        try {
            $context = GeneralUtility::makeInstance(Context::class);
            $uri = $site->getRouter($context)->generateUri(
                $pageUid,
                [
                    '_language' => $siteLanguage,
                    'type' => $pageType,
                ],
                '',
                RouterInterface::ABSOLUTE_URL,
            );
            return new PageTypeSource(
                $uri->getHost() ?: '*',
                $uri->getPath(),
                $pageType,
                [
                    'type' => $pageType,
                ],
            );
        } catch (\InvalidArgumentException|InvalidRouteArgumentsException $e) {
            throw new UnableToLinkToPageException(
                sprintf(
                    'The link to the page with ID "%d" and type "%d" could not be generated: %s',
                    $pageUid,
                    $pageType,
                    $e->getMessage(),
                ),
                1675618235,
                $e,
            );
        }
    }
}
Copied!

New in version 13.0

With a custom source implementation

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects\EventListener;

use MyVendor\MyExtension\Redirects\CustomSource;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;

#[AsEventListener(
    identifier: 'my-extension/redirects/add-redirect-source',
    after: 'redirects-add-plain-slug-replacement-source',
)]
final readonly class MyEventListener
{
    public function __invoke(SlugRedirectChangeItemCreatedEvent $event): void
    {
        // Retrieve change item and sources
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();

        // Remove plain slug replacement redirect source from sources
        $sources = array_filter(
            $sources,
            fn($source) => !($source instanceof PlainSlugReplacementRedirectSource),
        );

        // Add custom source implementation
        $sources[] = new CustomSource();

        // Replace sources collection
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(...array_values($sources)),
        );

        // Update changeItem in the event
        $event->setSlugRedirectChangeItem($changeItem);
    }
}
Copied!

New in version 13.0

Example of a CustomSource implementation:

EXT:my_extension/Classes/Redirects/CustomSource.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Redirects;

use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

final class CustomSource implements RedirectSourceInterface
{
    public function getHost(): string
    {
        return '*';
    }

    public function getPath(): string
    {
        return '/some-path';
    }

    public function getTargetLinkParameters(): array
    {
        return [];
    }
}
Copied!

Default event listeners

The listener \TYPO3\CMS\Redirects\EventListener\AddPageTypeZeroSource creates a \TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource for a page before the slug has been changed. The full URI is built to fill the source_host and source_path, which takes configured route enhancers and route decorators into account, for example, the PageType route decorator.

It is not possible to configure for which page types sources should be added. If you need to do so, see Using PageTypeSource which contains an example how to implement a custom event listener based on PageTypeSource.

In case that PageTypeSource for page type 0 results in a different source, the PlainSlugReplacementSource is not removed to keep the original behaviour, which some instances may rely on.

This behaviour can be modified by adding an event listener for SlugRedirectChangeItemCreatedEvent:

Remove plain slug source, if page type 0 differs

EXT:my_extension/Classes/Backend/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource;
use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

#[AsEventListener(
    identifier: 'my-extension/custom-page-type-redirect',
    // Registering after Core listener is important, otherwise we would
    // not know if there is a PageType source for page type 0
    after: 'redirects-add-page-type-zero-source',
)]
final readonly class MyEventListener
{
    public function __invoke(
        SlugRedirectChangeItemCreatedEvent $event,
    ): void {
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();
        $pageTypeZeroSource = $this->getPageTypeZeroSource(
            ...array_values($sources),
        );
        if ($pageTypeZeroSource === null) {
            // nothing we can do - no page type 0 source found
            return;
        }

        // Remove plain slug replacement redirect source from sources. We
        // already know, that if it is there it differs from the page type
        // 0 source, therefor it is safe to simply remove it by class check.
        $sources = array_filter(
            $sources,
            static fn($source) => !($source instanceof PlainSlugReplacementRedirectSource),
        );

        // update sources
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(
                ...array_values($sources),
            ),
        );

        // update change item with updated sources
        $event->setSlugRedirectChangeItem($changeItem);
    }

    private function getPageTypeZeroSource(
        RedirectSourceInterface ...$sources,
    ): ?PageTypeSource {
        foreach ($sources as $source) {
            if ($source instanceof PageTypeSource
                && $source->getPageType() === 0
            ) {
                return $source;
            }
        }
        return null;
    }
}
Copied!

New in version 13.0

API

class SlugRedirectChangeItemCreatedEvent
Fully qualified name
\TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent

This event is fired in the TYPO3CMSRedirectsRedirectUpdateSlugRedirectChangeItemFactory factory if a new SlugRedirectChangeItem is created.

It can be used to add additional sources, remove sources or completely remove the change item itself. A source must implement the RedirectSourceInterface, and for each source a redirect record is created later in the SlugService. If the SlugRedirectChangeItem is set to null, no further action is executed for this slug change.

getSlugRedirectChangeItem ( )
Returns
\TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem
setSlugRedirectChangeItem ( \TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem $slugRedirectChangeItem)
param $slugRedirectChangeItem

the slugRedirectChangeItem