S7
zurück
30.05.2019
Thema: TYPO3

Typo3 9.5, Extbase Models generiert im Frontend und sprechende URLs

Da ist sie, die schöne neue sprechende URL-Welt in TYPO3 9.5. Einfach ein Feld vom Typ 'slug' konfigurieren, und der Datensatz hat eine schöne, sprechende, SEO sichere URL. Außer, naja, außer der Datensatz wurde im Frontend, oder via Cron oder sonst irgendwie in der Extbase Welt generiert. Was tun?

Auch Markus und ich (Frank) standen vor diesem Problem im vergangen Projekt, hier wurden sehr viele Datensätze, also Fragen, Berichte oder Events aus dem Frontend heraus generiert, alles gut und brav in Domain/Models und Repositories gegossen. Zukunftssicher eben. Aber was ist mit den slugs, man braucht ja schöne URLs nacher, auch wenn die Daten aus dem Frontend kommen?

Der erste Gedanke war, dass man das im setter für den Titel machen kann, also abprüfen ob das slug_feld leer ist, und wenn nicht, irgendeine slugify Funktion hernehmen und einen slug daraus generieren:

class Article extends AbstractEntity {

  public static function slugify($text)
  {
    // replace non letter or digits by -
    $text = preg_replace('~[^\pL\d]+~u', '-', $text);
    
    // transliterate
    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
    
    // remove unwanted characters
    $text = preg_replace('~[^-\w]+~', '', $text);
    
    // trim
    $text = trim($text, '-');
    
    // remove duplicate -
    $text = preg_replace('~-+~', '-', $text);
    
    // lowercase
    $text = strtolower($text);
    
    if (empty($text)) {
      return 'n-a';
    }
    
    return $text;
  }

  public function setTitle($title) {
    $this->title = $title;
    if (empty($this->slugfield)) {
      $this->slugfield = self::slugify($title);
    }
  }
}

Das würde zwar tun, aber ist

1. nicht sonderlich elegant und - viel wichtiger -

2. lässt dies die TCA Konfiguration außer Acht, welche ja viel feiner und genauer arbeitet und Spezial- und Grenzfälle mit abdecken kann.

Um eine Lösung zu finden, haben wir uns erst einmal im DataHandler die Stelle gesucht, an welcher slugs berechnet werden, und da haben wir folgendes gefunden:

DataHandler.php

<?php
  // typo3/sysext/core/Classes/DataHandling/DataHandler.php 
  
class DataHandler {  
  
  // etwa 1975 Zeilen später 
  
  
    protected function checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray = []): array
    {
        $workspaceId = $this->BE_USER->workspace;
        $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $tcaFieldConf, $workspaceId);
        $fullRecord = array_replace_recursive($this->checkValue_currentRecord, $incomingFieldArray ?? []);
        // Generate a value if there is none, otherwise ensure that all characters are cleaned up
        if ($value === '') {
            $value = $helper->generate($fullRecord, $realPid);
        } else {
            $value = $helper->sanitize($value);
        }

        // In case a workspace is given, and the $realPid(!) still is negative
        // this is most probably triggered by versionizeRecord() and a raw record
        // copy - thus, uniqueness cannot be determined without having the
        // real information
        // @todo This is still not explicit, but probably should be
        if ($workspaceId > 0 && $realPid === -1
            && !MathUtility::canBeInterpretedAsInteger($id)
        ) {
            return ['value' => $value];
        }

        // Return directly in case no evaluations are defined
        if (empty($tcaFieldConf['eval'])) {
            return ['value' => $value];
        }

        $state = RecordStateFactory::forName($table)
            ->fromArray($fullRecord, $realPid, $id);
        $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
        if (in_array('uniqueInSite', $evalCodesArray, true)) {
            $value = $helper->buildSlugForUniqueInSite($value, $state);
        }
        if (in_array('uniqueInPid', $evalCodesArray, true)) {
            $value = $helper->buildSlugForUniqueInPid($value, $state);
        }

        return ['value' => $value];
    }
}

Gar nicht so kompliziert. Ein Service wäre uns lieber gewesen, aber das ist einfach genug, um es für unsere Bedürfnisse anzupassen.

Also haben wir schnell eine Hilfsmethode ins Model gebaut, die dann etwa so aussah:

namespace SUDHAUS7\OurArticles\Domain\Model;

use ... // lets assume we use well

class Article extends AbstractEntity {

  /**
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
     */
    public function makeSlug(): void
    {
        // get the whole model so far as an array, keys in camelCase
        $properties = $this->_getProperties();
        $record = [];
        
        // we assume TCA is loaded, should be true for any practical case in anything greater Typo3 7
        // tablename lookup is not easy from this point
        $tcaConfig = $GLOBALS['TCA']['tx_ourarticles_domain_model_article']['columns']['path_segment']['config'];
        
        //convert the camelCase Properties into something that resembles the incomingFieldArray
        //used by the DataHandler faciliy
        foreach ( $properties as $k => $v ) {
            $field = GeneralUtility::camelCaseToLowerCaseUnderscored($k);
            // simple relation resolver with uid. we just want to prevent hickups
            $v = \is_object($v) && \method_exists($v, 'getUid') ? $v->getUid() : $v;
            //populate the record array
            $record[$field] = $v;
        }
        
        // we do not use Workspaces for this, this is frontend
        $helper = GeneralUtility::makeInstance(SlugHelper::class, 'tx_ourarticles_domain_model_article' ,'path_segment', $tcaConfig, 0);
        
        $pid = (int)$this->getPid() > 0 ? (int)$this->getPid() : 1; // -1 and 0 won't work as fallback
        
        $value = $helper->generate($record, $pid);
        
        // get state Object
        $state = RecordStateFactory::forName($this->tablename)
            ->fromArray($record, $pid, 'NEW');
        
        $evalCodesArray = GeneralUtility::trimExplode(',', $tcaConfig['eval'], true);
        if ( in_array('uniqueInSite', $evalCodesArray, true) ) {
            $this->pathSegment = $helper->buildSlugForUniqueInSite($this->pathSegment, $state);
        }
        if ( in_array('uniqueInPid', $evalCodesArray, true) ) {
            $this->pathSegment = $helper->buildSlugForUniqueInPid($this->pathSegment, $state);
        }
        
        $this->pathSegment = $value;
    }
  
}

(Anmerkung: wir wussten, dass wir nicht mit Workspaces arbeiten, das Zeug kommt ja nahezu 100% aus dem Frontend, daher haben wir die Workspace-spezifischen Sachen weggelassen)

und im Controller vor dem hinzufügen zum Repository ausgeführt:

//...

public function saveAction($article) {
  //...
  $article->makeSlug();
  //...
}
//..

Aber so richtig ist es das noch nicht was wir eigentlich wollen. 

1.) Die Methode wird immer ausgeführt, ein Slug sollte aber erstmal stabil bleiben (404 lässt Grüssen)

2.) Das ist nicht elegant ;-)

Also ein wenig nachgeforscht, und mal den Lebenszyklus eines Extbase Models angeschaut, und siehe da, es gibt die Methode _isNew() im AbstractEntity.

_isNew() wird vom Persistence Manager aufgerufen um festzustellen, ob ein Model neu ist oder eben, ob es schon existiert. Das passt für uns! _isNew() ist public definiert, darf also überladen werden. Und damit wir das auf mehrere Modelle anwenden können, definieren wir uns eine eigene AbstractEntitiy: 

<?php


namespace NAMESPACE\Extention\Domain;

use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;

abstract class AbstractSlugEntity extends AbstractEntity
{
    /**
     * @var string The name of the slug field
     */
    protected $slug_property;
    
    /**
     * @var string The tablename for this model
     */
    protected $tablename;
    
    /**
     * @return bool
     * @throws NoSuchArgumentException
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
     */
    public function _isNew()
    {
        if (empty($this->slug_property)) {
            throw new NoSuchArgumentException('slug_property can not be empty', 1557844570);
        }
        if (empty($this->tablename)) {
            throw new NoSuchArgumentException('tablename can not be empty', 1557844602);
        }
        
        // run whatever Parent needs to do for this
        $isnew = parent::_isNew();
        if ($isnew) {
            // this is a new record, lets roll
            $this->makeSlug();
        }
        return $isnew;
    }
    
    /**
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
     */
    public function makeSlug(): void
    {
        // get the whole model so far as an array, keys in camelCase
        $properties = $this->_getProperties();
        $record = [];
        
        // we assume TCA is loaded, should be true for anything greater Typo3 7
        $tcaConfig = $GLOBALS['TCA'][$this->tablename]['columns'][$this->slug_property]['config'];
        
        //convert the camelCase Properties into something that resembles the incomingFieldArray
        //used by the DataHandler faciliy
        foreach ($properties as $k => $v) {
            $field = GeneralUtility::camelCaseToLowerCaseUnderscored($k);
            // simple relation resolver with uid. we just want to prevent hickups
            $v = \is_object($v) && \method_exists($v, 'getUid') ? $v->getUid() :  $v;
            //populate the record array
            $record[$field] = $v;
        }
        
        // we do not use Workspaces for this, this is frontend
        $helper = GeneralUtility::makeInstance(SlugHelper::class, $this->tablename, $this->slug_property, $tcaConfig, 0);
        
        $pid = (int)$this->getPid() > 0 ? (int)$this->getPid() : 1; // defintion of realPid
        
        $value = $helper->generate($record, $pid);
        
        // get state Object
        $state = RecordStateFactory::forName($this->tablename)
            ->fromArray($record, $pid, 'NEW');
        
        $evalCodesArray = GeneralUtility::trimExplode(',', $tcaConfig['eval'], true);
        
        if (in_array('uniqueInSite', $evalCodesArray, true)) {
            $value = $helper->buildSlugForUniqueInSite($value, $state);
        }
        if (in_array('uniqueInPid', $evalCodesArray, true)) {
            $value = $helper->buildSlugForUniqueInPid($value, $state);
        }
        
        // generate the path segment field in camelcase notation
        $pathsegmentincamelcase = GeneralUtility::underscoredToLowerCamelCase($this->slug_property);
        $this->{$pathsegmentincamelcase} = $value;
    }
}

Wir haben noch zusätzlich 2 properties definiert, $slug_property und $tablename.

$slug_property soll im Model dann einfach den Namen des properties enthalten, welches den Slug speichert. Und  $tablename ist konkret die Tabelle welche das Model referenziert. Der Grund, warum wir uns entschieden haben, den Tabellennamen so zu speichern, ist einfach der, dass aus Extbase heraus es sehr schwer ist, verlässlich den Tabellennamen zu errechnen. Zumindest haben wir auf Anhieb keine elegante Lösung gefunden, nicht ohne entweder viel aus dem Extbase Context oder aus dem DataMapper Context in das Model zu laden. Zumindest rechtfertigte es nicht den Aufwand gegen eine simple statische Definition des Tabellennamens.

Der Effekt ist nun, dass wir im Controller gar nichts mit dem Slug machen müssen, sondern dass, wenn ein Objekt neu ist, der Slug automatisch anhand der TCA-Vorgaben berechnet wird, und vor dem Speichern im passenden Property abgelegt wird.

Genau so, wie man es erwartet.

Gedanken für die Zukunft:

  1. Das mit der PID ist nicht perfekt, aber ohne Kontext schwierig. Eventuell kann man da was finden, dass man die echte PID findet
  2. Errechnen des Tabellennamen wäre eine sinnvolle Sache

 

Ähnliche Beiträge
Exception Handling in TYPO3
28.05.2019

Exceptions in TYPO3, wie gehe ich damit um?

Exception Handling in TYPO3

Jeder kennt diese Meldungen, welche sagen, irgendetwas ist da wohl schief gelaufen. Das ist nervig, sollte programmatisch vermieden werden, kann aber nicht immer verhindert werden. Oder doch?  

Arbeiten mit TYPO3 FlashMessages im FrontEnd
15.05.2019

FlashMessages in TYPO3 Frontend konfigurieren und nutzen

Arbeiten mit TYPO3 FlashMessages im FrontEnd

Man will schnell Meldungen im Frontend auf Interaktionen bringen? Man braucht das nur temporär? Es gibt ja die FlashMessages im Backend. Aber kann ich die auch im Frontend nutzen? Ja, ich kann. Und hier zeige ich, wie.  

Uploadfilter und Leistungsschutzrecht
26.03.2019

Uploadfilter und Leistungsschutzrecht

Worum geht es?

Jetzt ist sie durch: Die EU-Richtlinie zum "Urheberrecht im digitalen Binnenmarkt" ist vom Europäischen Parlament abgesegnet worden. 348 Abgeordnete waren dafür, 274 waren dagegen, 36 enthielten sich. Der Riss ging dabei durch alle Fraktionen. Besonders umstritten sind die Artikel 15 (früher 11) und 17 (früher 13).  

Globales Menü aus Datensätzen generiert
08.03.2019

Globales Menü aus Datensätzen generiert

Für eine Produktübersicht ergab sich die Notwendigkeit, ein immer vorhandenes Menü für die Webseite zu generieren.

Der erste Gedanke war, hier ein Plugin zu bauen, welches die Datensätze holt, aufbereitet und ausliefert, damit im Fluid Template das Menü generiert werden kann. Folgende Szenarien wären damit möglich gewesen:  

TYPO3 Extensions aktualisieren
26.02.2019

Hilfe, meine TYPO3 Extension ist zu alt!

Wie hält man seine TYPO3 Erweiterungen auf dem neuesten Stand?

Jeder der TYPO3 Extensions schreibt und diese über die Jahre pflegt, kommt irgendwann mal an die Stelle, wo die Extension zu alt ist für die glänzend neue TYPO3 Version.  

Uploadfilter
22.02.2019

Uploadfilter

Worum geht es?

Nach monatelangem Tauziehen steht fest: Die Europäische Union verpflichtet künftig Webseiten und Apps zum Filtern von Inhalten. Die Freiheit im Internet schwindet damit, fürchten Netzaktivisten. Am Text der Reform ist nicht mehr zu rütteln, die endgültige Abstimmung kommt in wenigen Wochen.  

Allgemeines Gleichbehandlungsgesetz
12.02.2019

Allgemeines Gleichbehandlungsgesetz

Auswirkungen des dritten Geschlechts „divers“ auf Arbeitgeber und Personalabteilungen in der Praxis

Seit Mitte Dezember ist in Deutschland ein Gesetz in Kraft, das offiziell ein drittes Geschlecht neben Mann und Frau bestätigt. Nach dem Gesetzentwurf wird dieses mit der Bezeichnung „divers“ betitelt. Welche Auswirkungen hat diese Anerkennung des dritten Geschlechts auf Arbeitgeber sowie Personalabteilungen im Besonderen?  

World Usability Day
12.11.2018

World Usability Day 2018

Der Wert von Usability bei der User Experience

Alljährlich findet in vielen Städten der Welt der World Usability Day statt. Es gibt jede Menge Seminare, Workshops und Konferenzen, die sich umfassend mit Themen und Fragen rund um Usability und User Experience auf verschiedenen Gebieten befassen. Unsere Mitarbeiter waren in Stuttgart und Wien auf Veranstaltungen.  

ePrivacy-Verordnung
05.11.2018

Die ePrivacy-Verordnung

Worum geht es?

Nach der Europäischen Datenschutzgrundverordnung (EU-DSGVO) droht nun der nächste Schlag. Die Diskussionen um die ePrivacy-Verordnung tragen zur bereits ohnehin bestehenden Verwirrung bei. Aber worum geht da eigentlich? Und ist das überhaupt neu?  

TYPO3-Baukastensystem
02.11.2018

Unser TYPO3-Baukastensystem

Ein hochleistungsfähiges Multi-Mandantensystem

Viele flächendeckend verteilte Unternehmen und Organisationen haben die Idee und den Anspruch ihren Unternehmenseinheiten ein standardisiertes Tool in einheitlichem Look & Feel zur Verfügung zu stellen, welche innerhalb eines vorgegebenen Rahmens eine größtmögliche Flexibilität an Contentproduktions- bzw. Darstellungsmöglichkeiten haben.  

Editieren von Datensätzen
16.10.2018

Editieren von Datensätzen im Backend-Modul

Erstellung eines Links zur Editieransicht im ViewHelper oder Controller

Die TYPO3-Dokumentation zum Setzen eines Links zur Editieransicht eines Datensatzes in TYPO3 8 und 9 ist im Moment noch veraltet. Ich zeige, wie dies in Backend-Modulen, die die genannten TYPO3-Versionen als Abhängigkeit haben, tatsächlich umgesetzt werden muss.  

Kommentare