TYPO3 9.5 – Routing Enhancer & Aspekte

Mit dem TYPO3 der 9er Serie wurde im TYPO3-Core das Routing integriert, worüber sprechende URL's erzeugt werden können, was zuvor nur über Extensions, wie z.B. realUrl, möglich war. Da wir immer bemüht sind, vor allem im Hinblick auf zukünftige Upgrades der TYPO3-Systeme auf höhere Versionen, die Anzahl von Extensions/Erweiterungen so minimal wie möglich zu halten, begrüßen wir diese vielversprechende Option.

Das Einrichten des Routings im Backend von TYPO3 ist nicht weiter kompliziert. Nach dem Anlegen einer neuen Seitenkonfiguration über das neue Seitenverwaltungsmodul, wird im Verzeichnis "typo3conf/sites" ein neues Verzeichnis mit dem Namen des eingegebenen "Site Identifier "angelegt, welches die Konfiguration in Form einer YAML-Datei (config.yaml) enthält. Damit erhält man bereits eine Basis-Konfiguration, die man nach seinen Wünschen entsprechend in einem Editor manuell anpassen oder erweitern kann.

Infos über die grundlegende Vorgehensweise:

  1. https://docs.typo3.org/c/typo3/cms-core/master/en-us/Changelog/9.5/Feature-86365-RoutingEnhancersAndAspects.html
  2. https://www.sebkln.de/tutorials/detail/routing-in-typo3-v9-der-extbase-plugin-enhancer/
  3. https://typo3worx.eu/2018/12/typo3-routing-extensions-and-enhancers/
  4. https://www.typo3.net/artikel/typo3-9-routing-enhancer-aspekte/

Eigene Extension

Was aber tun in einer selbst programmierten Extension? Vor allem, wenn es sich dabei um ein TYPO3 Versions-Upgrade handelt, das bereits eine eigene Extension enthält, über die tausende von Produktdatensätze angelegt wurden.

Im neuen TYPO3 9 gibt es für das TCA einen neuen "type" mit dem Namen "slug". Wenn man damit das TCA einer Tabelle der neuen oder bereits vorhandenen Extension erweitert, erhält man im TYPO3-Backend ein neues Eingabefeld, über das man ein sprechendes Pfad-Segment manuell anlegen kann oder beim Speichern des Datensatzen automatisch generiert wird - bekannt aus den Seiteneinstellungen:

TCA Type Slug:

'type' => 'slug',
'config' => [
    'generatorOptions' => [
        'fields' => ['title'],
    ]
    'fallbackCharacter' => '-',
    'prependSlash' => true,
    'eval' => 'uniqueInPid'
]

Im konkreten Fall hatten wir zusätzlich Produktbezeichnungen, die einige, für URL's unspezifische Sonderzeichen enthielten, die gefiltert werden mussten, damit im Ergebnis eine funktionierende URL ausgegeben wird. Aber auch dafür gibt es für die TCA-Konfiguration bereits einen entsprechenden Parameter - "replacements":

'config' => [
          ...
          'generatorOptions' => [
              'fields' => ['produktname'],
              'replacements' => [
                  '/' => '-',
                  '.' => '',
                  '®' => '',
                  ',' => '',
         '|' => '',
         ' ' => '-',
              ],
          ],
...

Über die Angaben im Array "replacements" wird beim Generieren des Pfadsegments der vorhandene Text gefiltert und die ungewünschten Sonderzeichen entsprechend der Konfiguration ersetzt.

Und bei bereits vorhandenen Datensätzen?

Bei diesem Upgrade Projekt gab es bereits mehrere Tausend Datensätzen in der Datenbank. Nach einiger Recherchearbeit war klar, dass es momentan noch  keine technische Möglichkeit gibt, das sprechende Pfad-Segement auf einfache Weise automatisiert für alle Datensätze zu erstellen. In der oben beschriebenen config.yaml hat man aber die Möglichkeit, sog. "Aspects" zu definieren, über die Manipulationen an dem Pfad-Segement zur Laufzeit möglich sind:

...
aspects:
    prod_title:
        type: <EigenerAspectName>
        tableName: tx_<ext>_domain_model_<name>
        routeFieldName: slug
    acc_title:
        type: <EigenerAspectName>
        tableName: tx_<ext>_domain_model_<name>
        routeFieldName: slug ##slug
...

Es gibt folgende vordefinierte "Aspects":

  1. StaticValueMapper
  2. LocaleModifier
  3. StaticRangeMapper
  4. PersistedAliasMapper
  5. PersistedPatternMapper

Die einzelnen Klassen und Methoden der Aspects kann man sich hier ansehen: api.typo3.org/master/namespace_t_y_p_o3_1_1_c_m_s_1_1_core_1_1_routing_1_1_aspect.html

Nr.4, "PersistedAliasMapper", wäre an dieser Stelle der richtige Aspect, allerdings kann man darüber keine Pfad-Segmente erzeugen, sondern nur bereits vorhadene auslesen. Das brachte uns aber auf die Idee, einen eigenen "Aspect" zu programmieren, der genau das macht.

Den neuen, eigenen "Aspect" definiert man in der ext_localconf.php einer eigenen Extension:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['EigenerAspectName'] = \Vendor\Ext\Routing\Aspect\EigenerAspectName::class;

In der Klasse selbst werden weitere Klassen für die Umsetzung benötigt:

namespace Vendor\Ext\Routing\Aspect;

use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
use TYPO3\CMS\Core\Routing\Aspect\PersistedAliasMapper;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use \TYPO3\CMS\Extbase\Reflection\ObjectAccess;

class EigenerAspectName extends PersistedAliasMapper  {
    use SiteLanguageAwareTrait;

    /**
     * {@inheritdoc}
     */
    public function generate(string $value): ?string
    {
       ...
    }

Da der "PersistedAliasMapper" bereits der richtige "Aspect" gewesen wäre und die Methode "generate()" schon zum Teill das erledigt, was benötigt wird, erweitern wir die Klasse mit unserer Eigenen und ergänzen an geeigneter Stelle die Methode "generate()".

 ...
 //Wenn es keinen Slug-Wert gibt
 if(empty($result[$this->routeFieldName])) {
     //Generiere einen neuen Slug
     $slugHelper = GeneralUtility::makeInstance(
          \TYPO3\CMS\Core\DataHandling\SlugHelper::class,
          $this->tableName,
          $this->routeFieldName,
          $GLOBALS['TCA'][$this->tableName]['columns'][$this->routeFieldName]['config']
     );
 ...

Für das Erstellen eines neuen Slugs benötigen wir eine Instanz der Klasse "SlugHelper". Alle benötigten Werte für den Contructor sind bereits vorhanden.

Mehr Informationen darüber gibt es in der TYPO3-API Dokumentation: api.typo3.org/master/class_t_y_p_o3_1_1_c_m_s_1_1_core_1_1_data_handling_1_1_slug_helper.html

Die SlugHelper Klasse enthält die Methode "generate()" mit der Beschreibung - "Used when no slug exists for a record". Der Methode müssen zwei Parameter übergeben werden - (array) $record, (int) $result['pid']. Bei $record handelt es sich um den Datensatz selbst, der sowohl das Feld für den Slug, als auch das Feld mit dem Text für den Slug enthält. Der Datensatz ist noch nicht vorhanden und muss aus der Datenbank geholt werden. Da im übergebenen Parameter $value die id des Datensatzes gespeichert ist, kann der Datensatz sehr leicht über das entsprechende Repository abgefragt werden:

...
$objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
$repository = $objectManager->get('Vendor\Ext\Domain\Repository\\'.$RepositoryName);
$model = $repository->findByUid($value);
...

Damit das auch für andere Datensätze funktioniert, erstellen wir den Repository-Namen dynamisch aus dem Tabellennamen. In $model haben wir nun das Objekt des entsprecheden Datensatzes - die Methode generate() benötigt aber ein Array, daher muss man das Ergebnis noch in ein Array umwandeln:

...
$availableProperties = ObjectAccess::getGettablePropertyNames($model);
$record = [];
//Model in Array umwandeln
foreach ($availableProperties as $propertyName) {
    if(ObjectAccess::isPropertySettable($model, $propertyName)
       && !in_array($propertyName, array('uid','categories')))
    {
        $record[$propertyName] = ObjectAccess::getProperty($model, $propertyName);
    }
}
... 

Das Array wird dann anschließend an die Methode generate() übergeben:

$slug = $slugHelper->generate($record, $result['pid']);

Wenn es einen Slug gibt, dann muss er wieder im Model/Datensatz gespeichert werden und der erzeugte Slug dem $result-Array übergeben werden, damit der aktuelle Request kein 404-Page Not Found erzeugt :

$model->setSlug($slug);
$repository->update($model);
$persistenceManager = $objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class);
$persistenceManager->persistAll();
$result[$this->routeFieldName] = $slug;


Erstellt: 20.01.2020

Vielen Dank für Ihr Interesse.
Bei Fragen zu TYPO3, Programmierung oder Upgrades stehen wir Ihnen gerne zur Verfügung. -> Kontakt