Lösung für Bilder im Flexform ohne Alternative-Feld

Für ein Custom Content Element kann in einem Feld Überschrift und ein Text eingegeben werden und ein Bild verknüpfte werden. Die Felder sind alle im Flexform definiert und der Kunde bemängelte (zurecht), dass die Möglichkeit zur Eingabe des Alternative Text beim Bild fehle. Kurz gesucht und einen Eintrag im Bezug auf TYPO3 6 gefunden, der unter TYPO3 9 immer noch funktioniert.

So wird das Bild im Flexform definiert:

<image>
    <TCEforms>
        <label>Bild</label>
        <config>
            <type>inline</type>
            <maxitems>1</maxitems>
            <minitems>1</minitems>
            <foreign_table>sys_file_reference</foreign_table>
            <foreign_table_field>tablenames</foreign_table_field>
            <foreign_label>uid_local</foreign_label>
            <foreign_sortby>sorting_foreign</foreign_sortby>
            <foreign_selector>uid_local</foreign_selector>
            <foreign_selector_fieldTcaOverride type="array">
                <config>
                    <appearance>
                        <elementBrowserType>file</elementBrowserType>
                        <elementBrowserAllowed>jpg,png</elementBrowserAllowed>
                    </appearance>
                </config>
            </foreign_selector_fieldTcaOverride>
            <foreign_match_fields type="array">
                <fieldname>image</fieldname>
            </foreign_match_fields>
            <appearance type="array">
                <newRecordLinkAddTitle>1</newRecordLinkAddTitle>
                <headerThumbnail>
                    <field>uid_local</field>
                    <height>64</height>
                    <width>64</width>
                </headerThumbnail>
            </appearance>
        </config>
    </TCEforms>
</image>

Und folgenden Block muss man einfügen, damit das alt-Feld angezeigt wird:

<image>
    <TCEforms>
        <label>Bild</label>
        <config>
            [...]
            <foreign_types type="array">
                <numIndex index="2">
                    <showitem>title,description,alternative</showitem>
                </numIndex>
            </foreign_types>
            [...]
        </config>
    </TCEforms>
</image>

Das war der Artikel, den ich gefunden habe: https://typo3-english.typo3.narkive.com/0GGddVxh/typo3-6-0-using-fal-image-in-a-flexform-as-an-element

Grundsätzlich würde ich davon abraten, Bilder im Flexform zu referenzieren. Gerade in diesem Fall, wenn die Felder wie Überschrift, Text und Bild bereits in der Tabelle vorhanden sind, wäre es deutlich einfacher gewesen, zwar ein neues Inhaltselement zu definieren, jedoch die vorhandenen Felder zu nutzen. Es hat diverse Vorteile: man kann das Element in Typ Text „konvertieren“, ohne die Inhalte zu verlieren und die Inhalte können besser gefunden werden. Das Projekt habe ich von einem anderen Entwickler übernommen und das Element wird bereits häufig genutzt, so dass ein Umbau nicht mehr so einfach ist.

Geschrieben in TYPO3, TYPO3 v8, TYPO3 v9 | Kommentare deaktiviert für Lösung für Bilder im Flexform ohne Alternative-Feld

404 Fehlerseite beim Routing nach Update auf 9.5.20

Für ein selbst-geschriebenes Plugin hatte ich eine funktioniere Routing-Konfiguration umgesetzt. Die Extension ist so eine Art news, aber deutlich abgespeckt. Da die Beiträge alle ein Datum haben, gibt es eine einfache Datumsnavigation. Nach einem Update auf TYPO3 9.5.20 funktionierten die Detail-Links weiterhin, die Links der Datumsnavigation lieferten einen 404-Fehler – Seite nicht gefunden.

Die funktonierende Konfiguration sah so aus.

routeEnhancers:
  Magazine:
    type: Extbase
    extension: MyMagazine
    limitToPages:
      - 18
      - 65
    plugin: Magazine
    defaultController: 'Article::list'
    defaults:
      date-month: ''
      date-year: ''
    routes:
      -
        routePath: '/{article-title}'
        _controller: 'Article::show'
        _arguments:
          article-title: article
      -
        routePath: '/{date-year}-{date-month}'
        _controller: 'Article::list'
        _arguments:
          date-month: overwriteDemand/month
          date-year: overwriteDemand/year
        requirements:
          date-month: \d+
          date-year: \d+
    aspects:
      article-title:
        type: PersistedAliasMapper
        tableName: tx_mymagazine_domain_model_article
        routeFieldName: path_segment
      date-month:
        type: StaticRangeMapper
        start: '01'
        end: '12'
      date-year:
        type: StaticRangeMapper
        start: '2000'
        end: '2030'

Nach dem Update und dem 404-Fehler beim Aufruf der Ansicht mit Jahr-Monat-Parametern hatte ich lange gesucht, unterschiedliches ausprobiert, mich durch den Quellcode gehangelt, es half alles nichts. Ich hatte mich bei der Konfiguration nach der Dokumentation von news gerichtet (Hut ab vor Georg Ringer und dieser Extension). Da ist der Block requirements in dem jeweiligen routePath-Block definiert. Die aktuellste Dokumentation des Core sieht da etwas anders aus: der requirements-Block steht dort an der gleichen Ebene wie aspects und routes. Die Lösung meines Problems war die Angabe von genauen Requirements auf der gleichen Ebene wie routes und aspects, sicherheitshalber habe ich noch einen neuen Konfigurationsabschnitt angelegt (DateMenu).

  DateMenu:
    type: Extbase
    extension: MyMagazine
    limitToPages:
        - 18
    plugin: Magazine
    defaultController: 'Article::list'
    defaults:
      date-month: ''
      date-year: ''
    routes:
      - routePath: '/{date-year}-{date-month}'
        _controller: 'Article::list'
        _arguments:
          date-month: overwriteDemand/month
          date-year: overwriteDemand/year
    requirements:
      date-month: '[0-9]{2}'
      date-year: '[0-9]{4}'
    aspects:
      date-month:
        type: StaticRangeMapper
        start: '01'
        end: '12'
      date-year:
        type: StaticRangeMapper
        start: '2000'
        end: '2030'

Die nicht mehr funktionierende Konfiguration ist wahrscheinlich auf diesen BUGFIX zurückzuführen – es ist jedoch nur eine Vermutung…

Geschrieben in TYPO3, TYPO3 v9 | Kommentare deaktiviert für 404 Fehlerseite beim Routing nach Update auf 9.5.20

TYPO3 Extension und Composer-Packages ohne Composer

Ich bin schon verwöhnt, seit ich Composer nutze. Da gibt es dann andere Problemchen, aber zumindest braucht man sich keine Gedanken um Abhängigkeiten zu machen. Extension per Composer installieren, notwendige Pakete werden geladen – fertig! Gelegentlich kommt es vor, dass auf einem Kunden-Server Composer nicht installiert werden darf oder der Kunde es nicht wünscht. Ich knirsche dann ein bisschen mit den Zähnen und arbeite so wie früher – „classic way“, wie es auf get.typo3.org so schön heißt.

Aktuell habe ich genau so einen Fall. Ich habe eine Extension installiert, die folgende Pakete benötigt:

"geocoder-php/google-maps-provider": "^4.4",
"php-http/guzzle6-adapter": "^1.0",
"php-http/message": "^1.7",
"geocoder-php/nominatim-provider": "^5.1"

Zunächst habe ich angefangen, mit alle Pakete aus den jeweiligen Git-Repos herunterzuladen, die dann wiederum andere Pakete brauchten etc. So hangelte ich mich von Paket zu Paket. Die Dateien der Pakete müssten natürlich nicht nur da sein, sondern auch eingebunden werden. Am Anfang hatte ich jede Datei einzeln mit require eingebunden – da wird man ja bekloppt! Es wurde mir composer-file-loader empfohlen. Es löst zwar das Problem, dass man die Dateien nicht alle einzeln aufführen muss, aber ein Paket benötigt das andere und sich an den Abhängigkeiten entlanghangeln muss man ja selbst.

Etwas später in einer ruhigen Minute hatte ich die Idee: Ich kann auf dem Server kein Composer nutzen, aber ich kann LOKAL Composer nutzen, um mir die notwendigen Pakete zu laden.

Ich habe eine TYPO3 Extension erstellt, die nur aus ext_localconf.php, ext_emconf.php und composer.json besteht. In die composer.json habe ich die benötigten Pakete aufgelistet:

{
    "name": "myvendor/myextension",
    "type": "typo3-cms-extension",
    "description": "Externe Pakete, da sie nicht per Composer installiert werden können",
    "license": "GPL-2.0+",
    "require": {
        "geocoder-php/google-maps-provider": "^4.4",
        "php-http/guzzle6-adapter": "^1.0",
        "php-http/message": "^1.7",
        "geocoder-php/nominatim-provider": "^5.1"
    },
    "config": {
        "vendor-dir": "vendor"
    }
}

Dann auf der Konsole in dem Extension-Ordner composer install laufen lassen. Nicht nur, dass mir composer alle Pakete mit den Abhängigkeiten lädt, er erstellt auch eine autoload.php, die ich einbinden kann. Somit steht in meiner ext_localconf.php nur noch das:

require_once(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName('EXT:myextension/vendor/autoload.php'));

Da merkt man doch wieder, wie schön Arbeiten mit Composer ist…

Plugins in GridElement scheinbar ohne Flexform-Settings

Falls jemand so wie ich einige Stunden (ok, ich übertreibe etwas) an diesem Problem verzweifeln sollte. Ich habe eine 3-spaltiges GridElement, das ich gemäß der aktuellsten Vorgabe mit dem DataProcessor umgesetzt habe. Übrigens ist die Dokumentation an dieser Stelle falsch.

Dort heißt es:

This is the default TypoScript setting provided while including the special Gridelements w/DataProcessing setup in your TS template editor:

lib.gridelements.defaultGridSetup =< lib.contentElement
lib.gridelements.defaultGridSetup {
[...]

Das Objekt lib.gridelements.defaultGridSetup gibt es nicht. Es wird tt_content.gridelements_pi1 hinzugefügt, der Rest der Angaben in der Dokumentation stimmt soweit.

Nun hatte ich also in einer Spalte ein Plugin mit ein paar Einstellungen, die über ein Flexform vorgenommen werden. In der "normalen" Content-Spalte funktioniert alles - meine Einstellungen werden im Frontend entsprechend ausgewertet. In einer Spalte in diesem GridElement hingegeben nicht - ich bekam in meinem Fall gar keine Ausgabe. Zum Glück bin ich zufällig über einen Issue gestolpert. Das Problem ist, dass der GridChildrenProcessor in Standardfall das Feld pi_flexform der Kinder parst und dabei in ein Array umwandelt. Wenn man die Inhalte der Spalte wie folgt ausgibt, dann wird die Eingabe im Feld pi_flexform nochmal verarbeitet.


Da es ja kein String mit XML-Inhalten, sondern ein Array ist, werden die Settings nicht ausgelesen und im Frontend nicht intepretiert. Das liegt an der Einstellung resolveChildFlexformData, die per default 1 ist. In diesem Issue wird vorgeschlagen, den Wert von resolveChildFlexformData per Default auf 0 zu setzen. Um breaking changes zu vermeiden, wird entschieden, diesen Wert wie vorher zu belassen, d.h. auf 1.

In meinem Fall ist die Lösung somit, diese Einstellung auf 0 zu setzen.

tt_content.gridelements_pi1.dataProcessing.10.default.options.resolveChildFlexFormData = 0

Gridelement auf Daten vom Parent zugreifen

In einer TYPO3 9 Installation mit Gridelements habe ich ein extrem flexibeles Accordion-Element erstellt. Es gibt dabei ein Gridelement „Accordion-Container“, das als Container für alle zusammengehörigen Accordion-Inhaltselemente dienen soll. Weiterhin gibt es ein „Accordion-Content“ – damit können mehrere Inhaltselemente gruppiert werden. Der Container ist notwendig, um das Accordion-typische Verhalten zu realisieren: man klickt ein Element zum Öffnen an und alle anderen schließen sich. Die klickbare Überschrift wird im Accordion-Content-Element hinterlegt. Im Accordion-Container können Einstellungen wie Farbe und Verhalten für das gesamte Accordion vorgenommen werden.

Und das ist genau das Problem. Sobald ich mich im Context des Accordion-Content befinde, kenne ich die Einstellungen wie Farbe und Verhalten nicht mehr – die sind ja im Container (aka Parent) hinterlegt.

Die neueste Version von Gridelements nutzt DataProcessing, also beschloss ich einen DataProcessor einzusetzen, um an die Daten des übergeordneten Elements ranzukommen:

tt_content.gridelements_pi1.dataProcessing {
    20 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
    20 {
        if.equals.field = tx_gridelements_backend_layout
        if.value = accordion-content
        table = tt_content
        pidInList.field = pid
        where = uid=###uid### AND deleted=0 AND hidden=0
        markers.uid.field = tx_gridelements_container
        as = parent
 
        dataProcessing {
            10 = Vendor\MyExtension\DataProcessing\FlexformDataProcessor
            10 {
                as = flexform_data
            }
        }
    }
}

Die Implementierung des FlexformDataProcessor habe ich bereits in einem anderen Beitrag beschrieben.

Nachdem ich das implementiert hatte, fiel mir ein, dass ich mein Problem bestimmt auch mit variable.set aus dem Fluid ViewHelper-Set lösen könnte – und siehe da, das geht auch.

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…

Geschrieben in TYPO3, TYPO3 v9 | Kommentare deaktiviert für Femanager – Profil bearbeiten und das Passwort Problem

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.

Geschrieben in TYPO3, TYPO3 v9 | Kommentare deaktiviert für Datensatz-Titel in Breadcrumb-Navigation anzeigen

Fehlermeldungen in Fluid / Extbase Formularen

Folgendes Setup: Model mit Validatoren bei einigen Properties und ein Formular, um ein neues Objekt dieses Typs anzulegen.

Das Formular kann wie folgt definiert werden:

<f:form action="create" object="{my}" objectName="my" method="post">
    <f:render partial="Form/AllErrors" arguments="{_all}" />
    <label for="foo" class="">Label</label>
    <div class="formWrap">
        <f:form.textfield property="foo" id="foo" />
    </div>
</f:form>

Sobald man für das Formular object und objectName definiert, kann man mit dem Attribut property in den Feldern arbeiten, dabei wird der Wert falls vorhanden automatisch ausgefüllt. Wenn nun während der Validierung Fehler auftreten, dann werden in diesem Beispiel die Fehler ALLE ausgegeben mit dem folgenden Partial:

<f:form.validationResults>
    <f:if condition="{validationResults.flattenedErrors}">
        <div class="errors">
            <f:for each="{validationResults.flattenedErrors}" as="errors" key="propertyPath">
                <ul>
                    <f:for each="{errors}" as="error">
                        <li>{propertyPath}: <span>{error}</span></li>
                    </f:for>
                </ul>
            </f:for>
        </div>
    </f:if>
</f:form.validationResults>

Es wäre natürlich schöner, wenn die Fehlermeldung am Feld dran stehen würde und nicht irgendwo am Anfand des Formulars. Man kann dem Tag f:form.validationResults durch das Argument for mitteilen, für welche Property man die Fehler haben möchte. Und so sieht das geänderte Formular nun so aus:

<f:form action="create" object="{my}" objectName="my" method="post">
    <label for="foo">Label</label>
    <div class="formWrap">
        <f:form.textfield property="foo" id="foo" />
        <f:form.validationResults for="my.foo">
            <f:render partial="Form/FormFieldError"
                      arguments="{validationResults: validationResults}"/>
        </f:form.validationResults>
    </div>
</f:form>

Das Partial dazu sieht so aus:

<f:if condition="{validationResults.errors}">
    <f:for each="{validationResults.errors}" as="error">
        <div class="error-note">
            <small class="has-error">{error.message}</small>
        </div>
    </f:for>
</f:if>

In meinem Fall sollte zusätzlich eine Klasse beim Label gesetzt werden, wenn es zu Validierungsfehlern kommt. Der validationResults ViewHelper ist aber keine Condition, man kann ihn auch nicht inline mit der Klasse „füttern“. Der validationResults ViewHelper setzt nur die Fehler für die entsprechende Property in den Template Variable Container und gibt danach die Kinder aus. Die Kinder können dann auf die entsprende Variable zugreifen, also ein bisschen wie f:alias oder f:map. Und so sieht der Formular-Code am Ende aus:

<f:form action="create" object="{my}" objectName="my" method="post">
    <f:form.validationResults for="my.foo">
        <label for="form-customer_id"
               class="{f:if(condition: validationResults.errors, then: 'has-error')}">Label</label>
        <div class="formWrap">
            <f:form.textfield property="foo" id="foo" />
            <f:render partial="Form/FormFieldError"
                      arguments="{validationResults: validationResults}"/>
        </div>
    </f:form.validationResults>
</f:form>

Geschrieben in TYPO3, TYPO3 v8 | Kommentare deaktiviert für Fehlermeldungen in Fluid / Extbase Formularen

I love TYPO3

Auf dem Camp (Bericht hier) hatte Jochen Weiland aufgefordert, mehr über TYPO3 zu schreiben. Der Aufhänger für diese Aufforderung war: er berichtete über Kommentare von TYPO3-Außenseitern der Art „TYPO3 ist doch schon tot“. Tja, totgeglaubte leben länger.

Ich arbeite mit TYPO3 seit Version 3.7, das ist finsteres Mittelalter. Bereits im Informatik-Studium mit viel C, C++ und Java habe ich Gefallen an PHP und MySQL gefunden. Wir haben damals Informix SQL gelernt und als ein Kommilitone MySQL erwähnte, war der Kommentar des Professors „die halten sich nicht lange“. Tja, siehe oben. PHP war damals rein funktionsbasiert, wahrscheinlich sah ich das Potential schon damals – ähm. Für meine Diplomarbeit schrieb ich eine Web-Applikation mit dem frisch erschienenen PHP 5 – es gab Klassen. OOP! Irgendwann gegen Ende des Studiums erzählte mir ein Kommilitone von einem verrückten Dänen, der im Keller ein CMS geschrieben hat und das sei richtig geil. Als ich ein paar Monate später eine Seite umsetzen musste, wo der Kunde seine Inhalte selbst bearbeiten kann, fiel mir dieses Gespräch wieder ein und so lernte ich TYPO3 kennen.

Jahre vergingen, ich habe mich mal mehr, mal weniger mit TYPO3 beschäftigt. Nach einer TYPO3 Agentur wechselte ich zu einer Agentur, die kein TYPO3 anbot und vermisste es nach einiger Zeit. TYPO3 krebste lange in der Version 4 herum, weil die Version 5 ja was neues werden sollte. Nach außen sah das so aus, als würde es da nicht voran gehen. Das Backend war nach wie vor altbacken, andere CMS spossen aus dem Boden (WordPress, Contao), dagegen sah TYPO3 alt aus. Da durfte ich mir anhören „TYPO3 wird nicht mehr lange geben“ und wurde irgendwie traurig.

Dann kam Version 6 – Namespaces, Extbase und Fluid – endlich! Seitdem gibt es regelmäßig Releases, Updates, neue Technologien werden eingebaut und TYPO3 macht mehr Spaß denn je. Was ist an TYPO3 liebe, ist die Flexibilität bei sauberer Datenbank-Struktur und hoher Code-Qualität. Ok, es gibt im Core nach wie vor ein paar dunkle Ecken, die immer noch eingestaubt und gruselig sind. Dafür ist der aufgeräumte Rest genial. Ich bin überzeugt, dass es nichts gibt, was man mit TYPO3 nicht umsetzen könnte. Gelegentlich schaue ich über den Tellerrand – ich habe schonmal mit anderen CMS (Drupal, WordPress, Contao) und Frameworks (CakePHP, Laravel) gearbeitet, um dann mit einem guten Gefühl weiter mit TYPO3 arbeiten zu können.

Ja, die Einstiegshürde ist hoch – TypoScript. Gut, wo ist denn bitte keine Einstiegshürde? Ich gehe doch auch nicht zur Fahrschule, setze mich ans Steuer und kann fahren? Für viele Dinge im Leben muss man lernen, Erfahrung sammeln, manchmal Frustrationen aufbauen, um voran zu kommen. TYPO3 hat den Vorteil, dass es gut dokumentiert ist und dass es eine Community gibt, die einem helfen kann. Beispiele gibt es wie Sand am Meer und Google hilft ja auch weiter, damals im finsteren Mittelalter war das noch anders.

Dann höre ich manchmal, TYPO3 ist zu kompliziert für Redakteure. Meine Antwort: das System ist falsch eingerichtet. TYPO3 hat ein geniales Benutzermanagement mit einer differenzierten Rechteverwaltung. d.h. nicht TYPO3 ist Schuld, es ist nur falsch konfiguriert. Ich kann verstehen, dass Frau Müller überfordert ist, wenn Sie 80 Buttons hat, die sie klicken kann und eine Menüleiste, bei der sie scrollen muss. Da ist es die Aufgabe des Entwicklers/Integrators TYPO3 entsprechend einzurichten und Frau Müller nur die 5 Buttons und 2 Menüpunkte zu geben, die sie braucht.

Und zum Schluß – seit einigen Jahren gibt es die TYPO3 Zertifizierung. Welches andere CMS hat das bitte? „The TYPO3 certification program is a global standard for TYPO3 knowledge“[1]. Wenn man also mit einem TYPO3 zertifizierten Entwickler zusammen arbeitet, dann ist zumindest sicher gestellt, dass er die API kennt. Da ich die Prüfung abgelegt habe, weiß ich, dass sie nicht ohne ist und dass ich trotz jahrelanger TYPO3 Erfahrung noch einiges lernen musste.

So, genug geschwärmt – wieder an die Arbeit. Mit TYPO3 natürlich 😉

[1] https://typo3.org/certification/