For coders TYPO3 Tech Corner

[TYPO3] Werte aus den Seiteneigenschaften mit DataProzessoren elegant aufbereiten

[TYPO3] Werte aus den Seiteneigenschaften mit DataProzessoren elegant aufbereiten

In der guten alten Zeit hat man hauptsächlich auf TypoScript gesetzt um die Seiteneigenschaften mit neuen Feldern zu erweitern und die Inhalte durch Redakteure gesetzt dann im Frontend wieder ausgegeben. Mit der Einführung von DataProzessoren kann man Werte, die an den View (also Fluid) übergeben werden einfach durchschleifen - oder noch besser: Aufbereiten.

Im nachfolgenden Beispiel zeige ich euch, wie wir Seiteneigenschaften in größeren Projekten erweitern, wo wir die Logik hinpacken und warum das so am besten skaliert.

Neue Felder in den Seiteneigenschaften

Dieses Beispielbild stammt aus dem Backend einer Versicherungsseite. Die Versicherung möchte auf allen Seiten einen Schnellkontakt im Frontend anzeigen. Da es sich um eine Art Sticky-Toolbar im oberen Bereich des Layouts handelt, nennen wir dies auch nachfolgend Toolbar.

Im TCA sind die Felder so konfiguriert, dass sobald ein Haken bei Different configuration from this page von einem Redakteur gesetzt ist, alle weiteren Felder sichtbar werden. Dies bedeutet also: Hier soll eine eigens definierte Toolbar angezeigt werden.

Alle nachfolgenden Änderungen befinden sich bei uns in der Sitepackage Extension.

Einfaches Beispiel im Umgang mit DataProzessoren

Zuerst erweitern wir die Tabelle pages mit ext_tables.sql in der Datenbank um ein paar neue Felder:

CREATE TABLE pages ( toolbar_note varchar(255) DEFAULT '' NOT NULL, toolbar_active tinyint(4) unsigned DEFAULT '0' NOT NULL, toolbar_disable tinyint(4) unsigned DEFAULT '0' NOT NULL, toolbar_note_empty_fields varchar(255) DEFAULT '' NOT NULL, toolbar_phonecall_number varchar(255) DEFAULT '' NOT NULL, toolbar_phonecall_openingtime varchar(255) DEFAULT '' NOT NULL, toolbar_address_street varchar(255) DEFAULT '' NOT NULL, toolbar_address_city varchar(255) DEFAULT '' NOT NULL, toolbar_logo int(11) unsigned DEFAULT '0', );

Das ganze muss dann auch noch im TCA Configuration/TCA/Overrides/pages.php definiert werden, damit man diese Felder im Backend bearbeiten kann:

<?php defined('TYPO3_MODE') || die('Access denied.'); $llPrefix = 'LLL:EXT:in2template/Resources/Private/Language/backend.xlf:'; $llPrefixFrontend = 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:'; $columns = [ 'toolbar_note' => [ 'exclude' => true, 'displayCond' => 'FIELD:toolbar_active:=:0', 'config' => [ 'type' => 'user', 'userFunc' => 'In2code\In2template\Tca\ToolbarNote->render' ], ], 'toolbar_active' => [ 'exclude' => true, 'onChange' => 'reload', 'label' => $llPrefix . 'TCA.pages.toolbar_active', 'config' => [ 'type' => 'check', ], ], 'toolbar_disable' => [ 'exclude' => true, 'displayCond' => 'FIELD:toolbar_active:=:1', 'onChange' => 'reload', 'label' => $llPrefix . 'TCA.pages.toolbar_disable', 'config' => [ 'type' => 'check', ], ], 'toolbar_note_empty_fields' => [ 'exclude' => true, 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'config' => [ 'type' => 'user', 'userFunc' => 'In2code\In2template\Tca\ToolbarNoteEmptyFields->render' ], ], 'toolbar_phonecall_number' => [ 'exclude' => true, 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'label' => $llPrefix . 'TCA.pages.toolbar_phonecall_number', 'config' => [ 'type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => '' ], ], 'toolbar_phonecall_openingtime' => [ 'exclude' => true, 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'label' => $llPrefix . 'TCA.pages.toolbar_phonecall_openingtime', 'config' => [ 'type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => '' ], ], 'toolbar_address_street' => [ 'exclude' => true, 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'label' => $llPrefix . 'TCA.pages.toolbar_address_street', 'config' => [ 'type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => '' ], ], 'toolbar_address_city' => [ 'exclude' => true, 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'label' => $llPrefix . 'TCA.pages.toolbar_address_city', 'config' => [ 'type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => '' ], ], 'toolbar_logo' => [ 'exclude' => true, 'label' => $llPrefix . 'TCA.pages.toolbar_logo', 'displayCond' => [ 'AND' => [ 'FIELD:toolbar_active:=:1', 'FIELD:toolbar_disable:=:0' ] ], 'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig( 'toolbar_logo', [ 'appearance' => [ 'createNewRelationLinkTitle' => $llPrefixFrontend . 'images.addFileReference' ], 'maxitems' => 1, ], $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] ) ], ]; \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('pages', $columns); \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( 'pages', '--div--;' . $llPrefix . 'TCA.pages.tab_toolbar, toolbar_note, ' . '--palette--;' . $llPrefix . 'TCA.pages.palette_toolbar_activate; palette_toolbar_activate, ' . 'toolbar_logo,' . '--palette--;' . $llPrefix . 'TCA.pages.palette_toolbar_phone; palette_toolbar_phone, ' . '--palette--;' . $llPrefix . 'TCA.pages.palette_toolbar_address; palette_toolbar_address,' ); /** * Define palettes */ $GLOBALS['TCA']['pages']['palettes']['palette_toolbar_activate'] = [ 'showitem' => 'toolbar_active, toolbar_disable, --linebreak--, toolbar_note_empty_fields, ' ]; $GLOBALS['TCA']['pages']['palettes']['palette_toolbar_phone'] = [ 'showitem' => 'toolbar_phonecall_number, toolbar_phonecall_openingtime, --linebreak--, toolbar_phonecall_link' ]; $GLOBALS['TCA']['pages']['palettes']['palette_toolbar_address'] = [ 'showitem' => 'toolbar_address_street, toolbar_address_city' ];

Als nächstes setzen wir ein Modell mit Classes/Domain/Model/Toolbar.php auf, das den Feldern im Backend entspricht. Hier nutzen wir kein reines Anomic-Model sondern packen auch jetzt schon ein paar nützliche Helferlein hinen, die uns beispielsweise die Telefonnummer entsprechend für einen Link aufbereitet:

<?php namespace In2code\In2template\Domain\Model; use In2code\In2template\Utility\StringUtility; use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; /** * Class Toolbar */ class Toolbar extends AbstractEntity { const TABLE_NAME = 'pages'; /** * @var int */ protected $pid = 0; /** * @var int */ protected $uid = 0; /** * @var string */ protected $title = ''; /** * @var bool */ protected $active = false; /** * @var bool */ protected $disabled = true; /** * @var string */ protected $phoneNumber = ''; /** * @var string */ protected $openingTime = ''; /** * @var string */ protected $addressStreet = ''; /** * @var string */ protected $addressCity = ''; /** * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference */ protected $toolbarLogo = null; /** * @var string */ protected $defaultToolbarLogo = '/typo3conf/ext/in2template/Resources/Public/Images/logo.svg'; /** * @return int */ public function getPid(): int { return $this->pid; } /** * @param int $pid * @return Toolbar */ public function setPid($pid) { $this->pid = $pid; return $this; } /** * @return int */ public function getUid(): int { return $this->uid; } /** * @param int $uid * @return Toolbar */ public function setUid(int $uid) { $this->uid = $uid; return $this; } /** * @return string */ public function getTitle(): string { return $this->title; } /** * @param string $title * @return Toolbar */ public function setTitle(string $title) { $this->title = $title; return $this; } /** * @return bool */ public function isActive(): bool { return $this->active; } /** * @param bool $active * @return Toolbar */ public function setActive(bool $active) { $this->active = $active; return $this; } /** * @return bool */ public function isDisabled(): bool { return $this->disabled; } /** * @param bool $disabled * @return Toolbar */ public function setDisabled(bool $disabled) { $this->disabled = $disabled; return $this; } /** * @return string */ public function getPhoneNumber(): string { return $this->phoneNumber; } /** * How to use a phone number like "0800 123 456" or "+49 (0)800 123456" * together with a A-tag in href where "tel:+49800123456" is needed * * @return string */ public function getPhoneNumberForLinks(): string { $phone = $this->getPhoneNumber(); $phone = preg_replace('~\s|\(0\)~', '', $phone); if (StringUtility::startsWith($phone, '00') === false && StringUtility::startsWith($phone, '+') === false) { if (StringUtility::startsWith($phone, '0')) { $phone = '+49' . ltrim($phone, '0'); } } return $phone; } /** * @param string $phoneNumber * @return Toolbar */ public function setPhoneNumber(string $phoneNumber) { $this->phoneNumber = $phoneNumber; return $this; } /** * @return string */ public function getOpeningTime(): string { return $this->openingTime; } /** * @param string $openingTime * @return Toolbar */ public function setOpeningTime(string $openingTime) { $this->openingTime = $openingTime; return $this; } /** * @return string */ public function getAddressStreet(): string { return $this->addressStreet; } /** * @param string $addressStreet * @return Toolbar */ public function setAddressStreet(string $addressStreet) { $this->addressStreet = $addressStreet; return $this; } /** * @return string */ public function getAddressCity(): string { return $this->addressCity; } /** * @param string $addressCity * @return Toolbar */ public function setAddressCity(string $addressCity) { $this->addressCity = $addressCity; return $this; } /** * @return FileReference */ public function getToolbarLogo() { return $this->toolbarLogo; } /** * @param FileReference $toolbarLogo * @return Toolbar */ public function setToolbarLogo(FileReference $toolbarLogo) { $this->toolbarLogo = $toolbarLogo; return $this; } /** * End of anomic model */ /** * Return path and filename of a toolbal logo and use a default icon if not set. * * @return string */ public function getToolbarLogoPathAndFilename(): string { if ($this->isDifferentToolbarLogo()) { $logoFile = $this->getToolbarLogo()->getOriginalResource()->getPublicUrl(); } else { $logoFile = $this->defaultToolbarLogo; } return $logoFile; } /** * @return bool */ public function isAddressDisplayed(): bool { return $this->getAddressCity() !== '' && $this->getAddressStreet() !== ''; } /** * @return bool */ public function isDifferentToolbarLogo(): bool { return $this->getToolbarLogo() !== null; } }

Der DataProzessor Classes/DataProcessing/PageToolbarProcessor.php kann dann so aussehen:

<?php namespace In2code\In2template\DataProcessing; use In2code\In2template\Domain\Factory\Toolbar; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; /** * Class PageToolbarProcessor */ class PageToolbarProcessor implements DataProcessorInterface { /** * Make toolbar accessable in Fluid * * @param ContentObjectRenderer $cObj The data of the content element or page * @param array $contentObjectConfiguration The configuration of Content Object * @param array $processorConfiguration The configuration of this processor * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View) * @return array the processed data as key/value store */ public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData ) { $toolbarFactory = GeneralUtility::makeInstance(Toolbar::class); $processedData['toolbar'] = $toolbarFactory->get($processedData['data']); return $processedData; } }

Wir holen uns noch eine Instanz des Modells über eine Factory Classes/Domain/Factory/Toolbar.php:

<?php declare(strict_types = 1); namespace In2code\In2template\Domain\Factory; use In2code\In2template\Domain\Model\Toolbar as ToolbarModel; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper; /** * Class Toolbar */ class Toolbar { /** * @param array $properties page properties of the current page * @return ToolbarModel */ public function get(array $properties): ToolbarModel { $objectManager = GeneralUtility::makeInstance(ObjectManager::class); $dataMapper = $objectManager->get(DataMapper::class); $toolbarModels = $dataMapper->map(ToolbarModel::class, [$properties]); return $toolbarModels[0]; } }

Weil wir ein bisschen faul sind, nutzen wir den DataMapper von Extbase um aus einem Array ein Object zu erhalten. Wenn man dies jedoch so nutzt, braucht es zu dem TCA (siehe oben) auch noch ein Mapping.

Mapping in TYPO3 >= 10 in Configuration/Extbase/Persistence/Classes.php:

<?php declare(strict_types = 1); return [ \In2code\In2template\Domain\Model\Toolbar::class => [ 'tableName' => 'pages', 'properties' => [ 'uid' => [ 'fieldName' => 'uid' ], 'pid' => [ 'fieldName' => 'pid' ], 'title' => [ 'fieldName' => 'title' ], 'active' => [ 'fieldName' => 'toolbar_active' ], 'disabled' => [ 'fieldName' => 'toolbar_disable' ], 'phoneNumber' => [ 'fieldName' => 'toolbar_phonecall_number' ], 'openingTime' => [ 'fieldName' => 'toolbar_phonecall_openingtime' ], 'addressStreet' => [ 'fieldName' => 'toolbar_address_street' ], 'addressCity' => [ 'fieldName' => 'toolbar_address_city' ], 'toolbarLogo' => [ 'fieldName' => 'toolbar_logo' ], ] ] ]

Mapping in TYPO3 <= 9 (z.B. in ext_typoscript_setup.txt):

config.tx_extbase { persistence { classes { In2code\In2template\Domain\Model\Toolbar { mapping { tableName = pages columns { pid.mapOnProperty = pid uid.mapOnProperty = uid title.mapOnProperty = title toolbar_active.mapOnProperty = active toolbar_disable.mapOnProperty = disabled toolbar_phonecall_number.mapOnProperty = phoneNumber toolbar_phonecall_openingtime.mapOnProperty = openingTime toolbar_address_street.mapOnProperty = addressStreet toolbar_address_city.mapOnProperty = addressCity toolbar_logo.mapOnProperty = toolbarLogo } } } } } }

Jetzt müsst ihr in eurem TypoScript den Aufruf eines neuen DataProzessors registrieren und das geht ungefähr so:

page = PAGE page { typeNum = 0 10 = FLUIDTEMPLATE 10 { templateName ... settings { ... } dataProcessing { 10 = In2code\In2template\DataProcessing\PageToolbarProcessor } } }

Und danach stehen euch die neuen Felder auch im Fluid für eine Ausgabe zur Verfügung:

... <li class="c-toolbar__section c-toolbar__section--actions"> <ul class="c-toolbar__group"> <f:if condition="{toolbar.phoneNumber}"> <li class="c-toolbar__item"> <a href="tel:{toolbar.phoneNumberForLinks}" class="js-popup c-toolbar__button"> {toolbar.phoneNumber} </a> </li> </f:if> </ul> </li> ...

Seiteneigenschaften mit Vererbungslogik

Oftmals möchte man die Einstellungen in den Seiteneigenschaften auch vererben können. Dies lässt sich durch eine kleine Änderung in der Factory erreichen. Die rekursive Funktion getClosestProperties() ruft sich hierbei so lange selber auf, bis eine Toolbar auf den Elternseiten entdeckt wurde oder bis man ganz oben angekommen ist. Damit wird euer komplettes Modell "nach unten durchvererbt".

<?php declare(strict_types = 1); namespace In2code\In2template\Domain\Factory; use In2code\In2template\Domain\Model\Toolbar as ToolbarModel; use In2code\In2template\Utility\DatabaseUtility; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper; /** * Class Toolbar */ class Toolbar { /** * @param array $properties page properties of the next relevant page * @return ToolbarModel */ public function get(array $properties): ToolbarModel { $objectManager = GeneralUtility::makeInstance(ObjectManager::class); $dataMapper = $objectManager->get(DataMapper::class); $toolbarModels = $dataMapper->map(ToolbarModel::class, [$this->getClosestProperties($properties)]); return $toolbarModels[0]; } /** * @param array $properties * @return array */ protected function getClosestProperties(array $properties): array { if ($properties['toolbar_active'] === 0 && $properties['pid'] > 0) { $properties = $this->getClosestProperties($this->getParentProperties($properties)); } return $properties; } /** * @param array $properties * @return array */ protected function getParentProperties(array $properties): array { $queryBuilder = DatabaseUtility::getQueryBuilderForTable(ToolbarModel::TABLE_NAME); $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class); $properties = $queryBuilder ->select('*') ->from(ToolbarModel::TABLE_NAME) ->where('uid=' . (int)$properties['pid'] . ' and sys_language_uid=0') ->execute() ->fetch(); if ($properties === false) { throw new \LogicException('No parent page found', 1613396489); } return $properties; } }

Logik im Model

Ab jetzt könnt ihr euer Modell mit Logik und Hilfsfunktionen ausstatten, um die Ausgabe im Frontend anzureichern. Übliche Beispiele für eigene Getter im Modell sind:

  • Conditions
  • Datumsberechnungen
  • Bildmanipulation und Vorbereitung
  • URL-Vorbereitung
  • Relationen zu anderen Tabellen aufbereiten
  • Aufbereitung der Daten für einen JSON-LD ViewHelper (schema.org)

Im Prinzip sind der Kreativität keine Grenzen gesetzt. Richtig eingesetzt spart die Verwendung eines guten Modells eine Vielzahl an IF-Conditions, ViewHelpern oder sogar Logik in Fluid (was man vermeiden sollte) ein.

Tipp: Jede public function im Model die mit get... oder is... beginnt, kann direkt im Fluid aufgerufen werden. Üblicherweise fangen boolische Funktion mit is... an. Beispiel isEnabled() -> {variable.enabled}

Zurück

Kennst du das: Immer nur schnell schnell?

Wie wäre es einmal mit Zeit und Respekt für Codequalität? Arbeiten im Team? Automatisierte Tests?

Komm zu uns