Custom Slug-Handling im TYPO3 Backend
Ausgangslage
Die TYPO3-Standardfunktionalität setzt in der Regel sinnvolle Defaults, die für die meisten Anwendungsfälle gut passen.
Es gibt aber Projekte, bei denen die Standardfunktionalität nicht perfekt passt und daher modifiziert werden sollen.
In diesem Fall war die Anforderung, dass sich die genaue Position einer Einzelseite im Seitenbaum in der URL widerspiegeln sollte, wobei der Redakteur jedoch das letzte Segment ("Slug") der URL frei anpassen können sollte.
In TYPO3 gibt es zwei Standard-Optionen:
- Die URL wird automatisch generiert und der Redakteur darf sie nicht ändern.
- Die URL wird automatisch generiert und der Redakteur darf sie komplett verändern. Also auch Teile der URL, die im Seitenbaum unterhalb der gerade aktuellen Seite liegen.
Wir haben uns dafür entschieden, dem Redakteur zu erlauben, die URL zu ändern, die entsprechende Eingabe beim Speichern aber zu überprüfen und ggf. anzupassen, um so sicherzustellen, dass Subteile der URL nicht manipuliert werden können und die URL-Struktur den Seitenbaum konsistent widerspiegelt.
Hook Registrieren
Hierfür nutzen wir einen Hook, den wir im Folgenden vorstellen wollen.
Zunächst muss der Hook in der ext_localconf.php registriert werden. Dies kann mithilfe des folgenden Codes erreicht werden:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['sitepackage’] = \MyVendor\Sitepackage\Hooks\SlugCorrectBeforeSaveHook::class;
Dieser Code teilt TYPO3 mit, dass wir beim Speichern von Datensätzen die Daten an eine neue Klasse "SlugCorrectBeforeSaveHook" übergeben möchten, die möglicherweise (oder auch nicht) diese Daten verändert.
Hook Implementieren
In der Klasse selbst wird die Logik implementiert. Die entsprechende Klasse könnte also so aussehen:
<?php
namespace MyVendor\Sitepackage\Hooks;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* SlugCorrectOnSaveHook
*/
class SlugCorrectBeforeSaveHook
{
/**
* @var mixed|\TYPO3\CMS\Core\Domain\Repository\PageRepository
*/
public $pageRepository;
public LocalizationUtility $languageUtility;
/**
* Correct slug url if it does not match parent node
*
* @array $params
*/
public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, DataHandler &$pObj): void
{
$initialPID = $GLOBALS['_REQUEST']['popViewId'] ?? null;
$this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$this->languageUtility = GeneralUtility::makeInstance(LocalizationUtility::class);
$page = $this->pageRepository->getPage($id, false);
// Check if table pages is edited, if it is the first call of the request and do a undefinedArrayKey prevention
// Check if slug is edited
if ($table == 'pages' && $id == $initialPID && isset($fieldArray['slug']) && isset($page['slug']) && $fieldArray['slug'] != $page['slug']) {
$parentPage = $this->pageRepository->getPage($page['pid'], false);
// Check if parent slug changed in new slug segment and parent page is not root
if (strncmp(
$fieldArray['slug'],
$parentPage['slug'] . '/',
strlen($parentPage['slug'] . '/'),
) != 0 && $parentPage['uid'] != 1) {
$slug = trim(substr($fieldArray['slug'], strrpos($fieldArray['slug'], '/') + 1));
// Adjust slug to ensure consistency
$fieldArray['slug'] = $parentPage['slug'] . '/' . $slug;
// Notify about the change
// FlashMessage($message, $title, $severity = self::OK, $storeInSession)
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$this->languageUtility->translate('LLL:EXT:sitepackage/Resources/Private/Language/locallang_be.xlf:slugChangeDescription', 'sitepackage', [$fieldArray['slug']]),
$this->languageUtility->translate('LLL:EXT: sitepackage/Resources/Private/Language/locallang_be.xlf:slugChangeHeadline', 'sitepackage'),
FlashMessage::WARNING,
true
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$messageQueue = $flashMessageService->getMessageQueueByIdentifier();
$messageQueue->addMessage($message);
}
}
}
}
Die Hook-Methode processDatamap_preProcessFieldArray() wird aufgerufen, um die zu speichernden Felder und ihre Werte zu überprüfen und zu ändern. Der erste Teil des Codes stellt sicher, dass die Änderungen nur an Seiten-Datensätzen vorgenommen werden, die gerade bearbeitet werden. Es wird auch geprüft, ob das Feld “slug” aktualisiert wurde und ob der neue Wert vom ursprünglichen Wert abweicht. Wenn diese Bedingungen erfüllt sind, wird der Slug automatisch aktualisiert, um mit dem Elternknoten übereinzustimmen.
Zunächst wird der Parent-Knoten der aktuellen Seite ermittelt. Dann wird geprüft, ob der Slug des Parent-Knotens in der neuen URL des aktualisierten Slug-Segments enthalten ist. Wenn dies nicht der Fall ist und der Parent-Knoten nicht die Root-Seite ist, wird der Slug automatisch aktualisiert, um mit dem Parent-Knoten übereinzustimmen. Dies wird durch Hinzufügen des Parent-Knoten-Slugs zum neuen Slug-Segment erreicht.
Schließlich wird der Benutzer über die Änderung des Slugs informiert, indem eine Flash-Nachricht generiert wird. Dies erfolgt mithilfe der Flash Message-Klasse von TYPO3, die eine Nachricht mit einer bestimmten Schwere und einem optionalen Titel erstellt. Die Nachricht wird dann in der Nachrichtenwarteschlange gespeichert, damit sie dem Benutzer angezeigt werden kann.
Insgesamt zeigt auch dieses einfach Hook-Beispiel, wie mächtig und flexibel TYPO3 Hooks sind und wie sie verwendet werden können, um die Funktionalität des Systems zu erweitern oder zu ändern. Weiterhin ist es auch ein gutes Beispiel dafür, wie man ein Problem angehen kann, das in der Standardimplementierung von TYPO3 möglicherweise nicht berücksichtigt wurde.