For coders TYPO3 Tech Corner

[TYPO3] Elegantly prepare values from the page properties with DataProcessors

[TYPO3] Elegantly prepare values from the page properties with DataProcessors

In the good old days, TypoScript was mainly used to expand the page properties with new fields and the content set by the editors and then output again in the frontend. With the introduction of DataProcessors, values that are transferred to the view (i.e. fluid) can simply be looped through - or even better: processed through.

In the following example, I'll show you how we extend page properties in larger projects, where we put the logic and why it scales so best.

New fields in the page properties

This example image comes from the backend of an insurance page. The insurance company would like to display a quick contact in the frontend on all pages. Since it is a kind of sticky toolbar in the upper area of the layout, we also call this toolbar in below code examples.


In the TCA, the fields are configured in such a way that as soon as a checkmark is set for Different configuration from this page by an editor, all other fields become visible. This means: A specially defined toolbar should be displayed here.

All of the following changes can be found in the sitepackage extension.

Simple example in dealing with DataProcessors

First of all we extend the database table pages with file ext_tables.sql:

 

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', );

 

The whole thing has to be defined in TCA Configuration/TCA/Overrides/pages.php so that you can edit these fields in the backend:

 

<?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' ];

 

 

Next we set up a model with Classes/Domain/Model/Toolbar.php, which corresponds to the fields in the backend. Here we do not use a pure anomic model but are already packing a few useful helpers that, for example, prepare the telephone number for a link:

 

<?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; } }

 

The DataProcessor Classes/DataProcessing/PageToolbarProcessor.php example content:

 

<?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; } }

 

Next we get an instance of the model with the factory class 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]; } }

 

Because we're a bit lazy, we use Extbase's DataMapper to get an object from an array. However, if you use this in this way, you also need a mapping in addition to the TCA (see above).

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 } } } } } }

 

Now you can call the new DataProzessor in your TypoScript:

 

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

 

Finally you can use the values in your Fluid template:

 

... <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> ...

 

 

Page properties with inheritance logic

Often you want to be able to inherit the settings in the page properties. This can be achieved by making a small change in the factory. The recursive function getClosestProperties() calls itself until a toolbar has been discovered on the parent pages or until you have reached the top. This means that your entire model is "inherited downwards".

 

<?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; } }

 

 

Logic in the model

From now on you can equip your model with logic and auxiliary functions to enrich the output in the frontend. Common examples of custom getters in the model are:

  • Conditions
  • Datecalculations
  • Imagemanipulation and preperation
  • URL-generating
  • Build relations to other tables
  • Data preperation for a JSON-LD ViewHelper (schema.org)

In principle there are no limits to creativity. Used correctly, the use of a good model saves a large number of IF-conditions, ViewHelpers or even logic in Fluid (which should be avoided).

Tip: Every public function in the model that begins with get ... or is ... can be called directly in the fluid. Boolean functions usually start with is... Example isEnabled() -> {variable.enabled}

Back

"Code faster, look at the time" - does this sound familiar to you?

How about time and respect for code quality? Working in a team? Automated tests?

Join us