Beiträge der Kategorie TYPO3 v9

Femanager – Profil bearbeiten und das Passwort Problem

Bei einem aktuellen Projekt arbeite ich mit Femanager für die Frontend-Benutzer-Verwaltung und bin dabei auf einige Schwierigkeiten gestoßen, die ich mit einem Workaround umschiffen konnte. Die Benutzer-Registrierung funktioniert sehr gut, die Probleme entstehen im Profil-Bearbeiten-Formular.

Das Standard-Template für Profil bearbeiten (Edit) im Femanager ist relativ einfach: Falls im Backend Felder ausgewählt sind, dann gebe für jedes Feld das Partial aus, ansonsten alle Felder. In meinem Fall ist das Profil bearbeiten Formular etwas komplexer aufgebaut: zweispaltig, Felder teilweise gruppiert.

Zuerst hatte ich das Passwort im Formular drin, hatte jedoch die Passwort-Validierung aus der Konfiguration entfernt und die Einstellung keepPasswordIfEmpty gesetzt. Es kommt eine Fehlermeldung von der Passwort-Hash-Funktion (Femanager Issue).

Ok, also Passwort aus dem Formular entfernt. Dann eine neue Seite erstellt, auf der Profil-Bearbeiten-Plugin platziert und dort nur das Passwort als Feld ausgewählt, weiterhin alle anderen Felder per Konfiguration als der Validierung entfernt. Die clientseitige Validierung hat auch rumgezickt, also habe ich sie auch deaktiviert (zumindest auf der Seite). Mit dieser Konfiguration kann das Passwort auf einer separaten Seite geändert werden.

plugin.tx_femanager.settings {
    edit.validation {
        _enable.client = 0
        password.required = 1
        password_repeat.required = 1
        lastName >
        firstName >
        username >
        email >
        address >
        zip >
        usergroup >
        city >
        country >
        jobState >
        subject >
    }
}

Obwohl ich das Passwort aus dem Template entfernt habe und die Validierung des Passworts aus dem Konfiguration, wird die Passwort-Validierung trotzdem aufgerufen. Schuld daran ist die folgende Zeile im PasswordValidator:

public function isValid($user)
{
    $this->init();
 
    // if password fields are not active or if keep function active
    if (!$this->passwordFieldsAdded() || $this->keepPasswordIfEmpty()) {
        return true;
    }
 
    $password = $user->getPassword();
    $passwordRepeat = isset($this->piVars['password_repeat']) ? $this->piVars['password_repeat'] : '';
 
    if ($password !== $passwordRepeat) {
        $this->addError('validationErrorPasswordRepeat', 'password');
        return false;
    }
 
    return true;
}

Die Funktion passwordFieldsAdded liefert dann true, wenn das Passwort-Feld im Flexform explizit hinzugefügt wurde oder gar kein Feld, was aus der Sicht der Extension bedeutet, dass das Passwort-Feld ja sichtbar ist. Ich habe ja schon erwähnt, dass durch das etwas komplexere Formular die Variante die Felder alle einzeln auszuwählen für mich nicht in Frage kommt. Ich muss also alle Felder ausgeben, durch die Konfiguration der Extension jedoch vorgaukeln, dass das Passwort-Feld nicht gewählt ist. Als Lösung habe ich in der TypoScript-Konfiguration eine neue Einstellung hinzugefügt showAll, die ich dann im Template abfrage. Damit kann ich im Flexform z.B. das Feld ‘firstname’ auswählen und es werden trotzdem alle Felder angezeigt. Auf der Passwort-Bearbeiten-Seite muss ich den Wert auf 0 setzen, damit nur das Passwort-Feld ausgegeben wird.

Das Edit-Template sieht verkürzt so aus:

<f:if condition="{settings.edit.showAll}">
    <f:then>
        ALLE FELDER AUSGEBEN
    </f:then>
    <f:else>
        <f:for each="{femanager:misc.explode(string:'{settings.edit.fields}')}" as="field">
            <f:render partial="Fields/{femanager:misc.upper(string:'{field}')}" arguments="{_all}" />
        </f:for>
        <f:render partial="Fields/SubmitUpdate" arguments="{_all}" />
    </f:else>
</f:if>

Es ist leider die beste Lösung für das Problem, die mir gerade einfällt. Bin gespannt, ob mir es bald um die Ohren fliegt…

FlexformDataProcessor für Custom Content Elements

Um individuelle Inhaltselemente zu erstellen, nutze ich bereits seit Jahren gerne Mask (zum Teil mit mask_export) und gridelements. So toll mask auch ist, es hat seine Grenzen und dann erstelle ich auch mal komplett eigene Inhaltselemente – “from scratch” sozusagen. Eine gute Anleitung hierfür gibt es bei TYPO3 Explained.

Wenn es um irgendwelche kleinen Einstellungen geht (Farbe, Ausrichtung etc.), dann finde ich es manchmal etwas übertrieben, dafür direkt ein Datenbankfeld zu generieren, vor allem, wenn man diese Einstellungen nur in einem Inhaltselement braucht. Was ich auch niemals mache, ist andere Felder zu zweckentfremden. Spätestens dann, wenn das Originalfeld benötigt wird, hat man ein Problem.

Genau für solchen Zweck gibt es bei TYPO3 die Flexforms. Sie können genauso konfiguriert werden wie TCA, ermöglichen das anlegen von Feldern in Tabs und sind insgesamt sehr flexibel (pun intended). In der Ausgabe hat man dann allerdings das Problem, dass alle Flexform-Werte in einem XML-Konstrukt stehen. Zum Glück hat TYPO3 die Data Prozessoren. Meine Recherche (vor ein paar Monaten) hat keinen fertigen FlexformDataProcessor zutage gebracht, so dass ich kurzerhand meinen eigenen geschrieben habe.

<?php
namespace Vendor\MyExtension\DataProcessing;
 
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
 
class FlexformDataProcessor implements DataProcessorInterface
{
    public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
    {
        if (empty($processedData['data']['pi_flexform'])) {
            return $processedData;
        }
 
        $flexformData = GeneralUtility::xml2array($processedData['data']['pi_flexform']);
        $flexformProcessed = [];
        foreach ($flexformData['data'] as $sheetKey => $sheetValue) {
            foreach ($sheetValue['lDEF'] as $fieldKey => $fieldValue) {
                $key = $sheetKey . '_' . $fieldKey;
                $flexformProcessed[$key] = $fieldValue['vDEF'];
            }
        }
 
        $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'flexform_data');
        $processedData[$targetVariableName] = $flexformProcessed;
 
        return $processedData;
    }
}

Und so kann man den Flexform Data Processor dann einsetzen:

tt_content.mycontentelement = FLUIDTEMPLATE
tt_content.mycontentelement {
    dataProcessing.10 = Vendor\MyExtension\DataProcessing\FlexformDataProcessor
    dataProcessing.10 {
        as = flexform_data
    }
}

Auf das Ergebnis kann man dann im Template mit {flexform_data} zugreifen.

Datensatz-Titel in Breadcrumb-Navigation anzeigen

Vor TYPO9 war es zwar nicht einfacher, den Titel des eigenen Datensatzes in der Breadcrumb (oder auch Rootline) Navigation anzuzeigen, aber es gab Beispiele wie Sand am Meer. Falls man nun bei der Generierung der Menüs auf Data Processoren setzt, dann findet man (also ich per Google) einige Anfragen in Foren, jedoch keine gute Anleitung. Und wie immer in solchen Fällen schaue ich in die Extension news, die mir schon so häufig weitergeholfen hat. Diese Extension ist einfach auf dem neuesten Stand und wenn man mal nicht weiter weiß, dann lohnt sich immer ein Blick in diese Extension. Danke Georg!

Das ist eine kleine Anleitung, wie man die Breadcrumb um den Titel der eigenen Datensätze bereichert. Jede Extension ist anders implementiert, daher ist das nicht als Copy&Paste einfach verwendbar.

Als erstes muss man einen Data Processor anlegen, das kann entweder in der Extension sein, deren Datensätze man anzapft oder in einer anderen. In diesem Beispiel lege ich die Datei AddRecordToMenuProcessor.php an im Unterordner Classes/DataProcessing.

Irgendwo in TypoScript hat man nun die Page-Definition, die an irgendeiner Stelle so aussieht:

page.10 = FLUIDTEMPLATE
page.10 {
    dataProcessing {
        [...]
 
        30 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
        30 {
            special = rootline
            special.range = 0|-1
            includeNotInMenu = 1
            as = menuBreadcrumb
        }
    }
}

Hinter die vorhandenen Data Processoren wird nun ein eigener angehängt:

page.10 = FLUIDTEMPLATE
page.10 {
    dataProcessing {
        [...]
 
        40 = Vendor\MyExtension\DataProcessing\AddRecordToMenuProcessor
        40.menus = menuBreadcrumb
    }
}

Nun arbeitet man in der angelegten Datei weiter.

namespace Vendor\MyExtension\DataProcessing;
 
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
 
class AddRecordToMenuProcessor implements DataProcessorInterface
{
 
    /**
     * @param ContentObjectRenderer $cObj
     * @param array $contentObjectConfiguration
     * @param array $processorConfiguration
     * @param array $processedData
     * @return array
     */
    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData
    ) {
        if (!$processorConfiguration['menus']) {
            return $processedData;
        }
        if (!ExtensionManagementUtility::isLoaded('my_record_extension')) {
            return $processedData;
        }
        $record = $this->getRecord();
        if ($record) {
            $menus = GeneralUtility::trimExplode(',', $processorConfiguration['menus'], true);
            foreach ($menus as $menu) {
                if (isset($processedData[$menu])) {
                    $this->addRecordToMenu($record, $processedData[$menu]);
                }
            }
        }
        return $processedData;
    }
 
    public function addRecordToMenu($record, array &$menu) {
        // remove last element
        array_pop($menu);
 
        $menu[] = [
            'data' => $record,
            'title' => $record['name'],
            'active' => 1,
            'current' => 1,
            'link' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'),
            'isRecord' => true
        ];
    }
 
    /**
     * @return array
     */
    public function getRecord()
    {
        $vars = GeneralUtility::_GET('tx_myrecordextension_pluginname');
        if(!isset($vars['record']) {
            return;
        }
        $recordUid = (int)$vars['record'];
        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
        $recordRepository = $objectManager->get(Vendor\MyRecordExtension\RecordRepository::class);
        $record = $recordRepository->findByUid($recordUid);
        $fields = ['name', 'title', 'email', 'phone'];
        $recordAsArray = [];
        foreach ($fields as $field) {
            $recordAsArray[$field] = $record->_getProperty($field);
        }
        return $recordAsArray;
    }
 
}

Die Funktion process ist notwendig, da man das Interface DataProcessorInterface implementiert. In meinem Fall hab ich den Data Processor in eine andere Extension packen müssen und daher brauche ich die Abfrage, ob die Extension geladen ist (Zeile 28). Ansonsten kann man sich das sparen.

Es wird versucht einen Datensatz der Extension zu holen und falls es funktioniert, wird das Menü um einen Eintrag mit den Daten des Datensatzes erweitert.

In der Funktion getRecord wird der Datesatz geholt. Georg nutzt in seiner news-Extension einen anderen Ansatz: nämlich mit ConnectionPool und QueryBuilder. Es hat den Vorteil, dass man ein Array und nicht wie in meinem Fall ein Object zurückbekommt. Daher brauche in dann die Konvertierung in ein Array. Es ist bestimmt nicht der beste Weg ein Objekt in ein Array zu konvertieren. Ich habe bisher nur nichts anderes gefunden, bin daher für Tipps in diese Richtung dankbar.

In der Funktion addRecordToMenu wird das Menü erweitert. Da habe ich auch einen etwas anderen Ansatz als in news. Dort wird das Menü erweitert. Ich schmeiße den letzten Eintrag raus und füge stattdessen meinen hinzu. Das hat aus meiner Sicht den Vorteil, dass man keinen Link zur Detailseite ohne Parameter hat. Den Ansatz aus News also dann verwenden, wenn die Detailansicht die Listenansicht überlädt. Falls man so wie in meinem Fall, eine separate Detailseite hat, die ohne Parameter keinen Sinn macht oder womöglich gar nicht funktioniert, dann muss man den Ansatz mit rausschmeißen-hinzufügen verfolgen.

TYPO3 Fluid-Templates und PhpStorm

Manchmal hab ich folgendes Problemchen: ich möchte z.B. falls Bedingung X eintrifft einen öffnenden Tag ausgeben und bei Bedingung Y einen schließenden Tag. PhpStorm fängt dann an, die Tags zu korrigieren, damit das Template aus seiner Sicht eine gültige Struktur hat. Ich habe bei meinem TYPO3-Kollegen Thomas Deuling einen einfachen Trick gesehen, wie man PhpStorm austricksen kann. Man schreibt die Tags einfach in !
Hier ein Beispiel für ein Media/Gallery Partial:

<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:ce="http://typo3.org/ns/TYPO3/CMS/FluidStyledContent/ViewHelpers" data-namespace-typo3-fluid="true">
<f:if condition="{gallery.rows}">
    <f:for each="{gallery.rows}" as="row">
        <f:if condition="{gallery.rows -> f:count()} > 1">
            <f:format.raw value="<div class='row'>"/>
        </f:if>
        <f:for each="{row.columns}" as="column">
            <f:if condition="{row.columns -> f:count()} > 1">
                <f:format.raw value="<div class='col'>"/>
            </f:if>
            <f:if condition="{column.media}">
                <f:render partial="Media/Type"
                          arguments="{file: column.media, dimensions: column.dimensions, data: data, settings: settings}"/>
            </f:if>
            <f:if condition="{row.columns -> f:count()} > 1">
                <f:format.raw value="</div>"/>
            </f:if>
        </f:for>
        <f:if condition="{gallery.rows -> f:count()} > 1">
            <f:format.raw value="</div>"/>
        </f:if>
    </f:for>
</f:if>
</html>

Es hat den kleinen Nachteil, dass man selbst den Code etwas schlechter lesen kann, aber wenigstens stimmt die Struktur und PhpStorm versucht nicht die Tags zu korrigieren.

Vorschau von Bildern aus Page Media im Backend

Es hat mich ein wenig gewundert, dass es keine Extension gibt, die das anbietet. Ich nutze das Feld “Media” unter Resources in den Seiteneingenschaften bei fast allen Projekten. Und gestern ist mir eingefallen, dass es ja eigentlich cool wäre, wenn man das eingepflegte Bild auch im Backend sehen könnte. In der TYPO3 Facebook-Gruppe hat die Nachfrage nach einer Extension nicht zum gewünschten Ergebnis geführt. Also beschloss ich (auch zu Übungszwecken) eine solche Extension selbst zu schreiben. Hier mein Kochrezept und Ergebnis.

Als erstes erstellt man einen Extensionordner mit einer ext_emconf.php. Meine Extension heißt übrigens np_pageresourcepreview.

In TYPO3 gibt es ja mittlerweile Hooks für so ziemlich alles. So auch einen um die Seite im Backend zu rendern. Man registrierert den Hook aus der neuen Extension in ext_localconf.php.

<?php
defined('TYPO3_MODE') || die('Access denied.');
 
if (TYPO3_MODE === 'BE') {
 
    // Hook into the page module
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawHeaderHook'][$_EXTKEY] =
        \Npostnik\NpPageresourcepreview\Hook\PageHook::class . '->render';
}

Im Ordner Classes/Hook erstellt man eine Datei PageHook.php und schreibt folgendes rein:

<?php
namespace Npostnik\NpPageresourcepreview\Hook;
 
use TYPO3\CMS\Backend\Controller\PageLayoutController;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
 
class PageHook
{
 
    /**
     * @var StandaloneView
     */
    protected $view;
 
    public function render(array $params, PageLayoutController $parentObject)
    {
        $pageinfo = $parentObject->pageinfo;
        if($pageinfo['media'] == 0) {
            return '';
        }
        $fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
        $fileObjects = $fileRepository->findByRelation('pages', 'media', $pageinfo['uid']);
 
        //load partial paths info from typoscript
        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
        $this->view->setFormat('html');
 
        $resourcesPath = 'EXT:np_pageresourcepreview/Resources/';
        $this->view->setTemplatePathAndFilename($resourcesPath . 'Private/Templates/PageHook.html');
        $this->view->assign('files', $fileObjects);
        $this->view->assign('page', $parentObject->pageinfo);
        return $this->view->render();
    }
 
}

Und schließlich erstellt man im Ordner Resources/Private/Templates die Datei PageHook.html mit folgendem Inhalt:

<f:if condition="{page}">
    <f:for each="{files}" as="file">
        <f:image image="{file}" maxHeight="300" />
    </f:for>
</f:if>

Das ist schon alles. Installieren und feddich.