Beiträge von natalia

ddev Tipps

Ok, wahrscheinlich ist der Titel „ddev Tipps“ etwas zu hoch gegriffen. Seit einiger Zeit nutze ich ddev (auf Windows mit WSL2) als lokale Entwicklungsumgebung. Meine Anfängliche Skepsis verschwand schnell und nun möchte ich es nicht mehr missen. Ich kann innerhalb von wenigen Minuten jede bestehende Website lokal installieren, wenn man mir Git-Zugangsdaten, einen Datenbank-Dump und fileadmin-Inhalte gibt. Wobei das letztere nicht unbedingt benötigt wird, filefill hilft zur Not auch. PHP Version Umstellen, TYPO3 Updates – alles kein Problem.

Auf dem TYPO3 Camp Rhein-Ruhr habe ich mir einen Vortrag von Wolfgang Wagner angehört zum Thema ddev. Da ich ja schon länger damit arbeite, war mir das meiste schon bekannt, aber ein paar Tipps habe ich mitgenommen.

Mit ddev launch kann man ja das Frontend der Seite aufrufen. Meistens brauche ich direkt das Backend – und das geht mit ddev launch /typo3.

Da ich phpMyAdmin nicht besonders mag und stattdessen HeidiSQL nutze, um auf die Datenbank zuzugreifen, brauche ich den dba-Container nicht, da er unnötig Speicherplatz verbraucht. Man kann in der .ddev/config.yaml in omit_containers in jedem Projekt angeben, dass dieser Container ausgelassen werden soll. Da ich diesen Container nie brauche, kann man man das auch global in ~/.ddev/global_config.yaml konfigurieren.

omit_containers: [dba]

Noch so ein kleiner Tipp: Wenn man mal Mist gebaut hat und die Projektnamen durcheinander gebracht hat, dann finden sich alle ddev-Projekte in dieser Datei ~/.ddev/global_config.yaml. Da kann man dann ein Projekt einfach löschen, falls nötig.

Wie oben beschrieben, greife ich mit HeidiSQL auf meine ddev Datenbanken zu. Dazu definiere ich in ddev einen festen DB Port (host_db_port: "22156"), lege die Verbindung in Heidi einmal an und kann sie immer wieder einfach starten.

Falls mal nur ein Teil der Datenbank aktualisiert werden soll, dann nicht ddev import-db verwenden. Dabei wird die Datenbank gelöscht und neu geschrieben. Falls man nun einen Teil der Daten bekommen hat, dann das hier verwenden. Das Password ist ‚db‘

ddev ssh
mysql -u db -p db < file.sql

Linkhandler mit Links zu Sections

Dieser Fall ist etwas speziell, aber vielleicht hilft es jemandem ja weiter. In einem Ordner liegen Datensätze vom Typ FAQ Einträge. Diese können in einem Plugin ausgewählt werden. Nun hatte die Kunden den Wunsch geäußert, aus dem Text in einem FAQ-Element auf ein anderes zu verlinkten. Ich habe den Linkhandler so konfiguriert, dass man aus einem Text mit dem Linkhandler ein FAQ-Elemeent referenzieren kann.

TCEMAIN {
    linkHandler {
        tx_faq {
            handler = TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler
            label = FAQ
            configuration {
                table = tx_faq_domain_model_faq
                pageTreeMountPoints = 3870
            }
            scanAfter = page
        }
    }
}

In der Ausgabe gehe ich davon aus, dass das FAQ Element auf der gleichen Seite eingesetzt ist. Ich brauche damit nur einen Link mit Section zu generieren:

  1. config.recordLinks.tx_faq {
  2.     forceLink = 0
  3.     typolink {
  4.         #parameter = current
  5.         parameter.data = TSFE:id
  6.         section.data = field:uid
  7.         section.wrap = faq_|
  8.         useCacheHash = 1
  9.     }
  10. }

In Zeile 4 oder 5 bin ich mir sicher, ob es etwas bewirkt. Lasse ich die Angabe parameter komplett weg, wird kein Link generiert. Daher ist diese Stelle „geraten“. Wenn jemand die richtige Konfiguration kennt, dann gerne her damit.

Generiert wird der Link dann wie folgt: <a href="#faq_140">Typoblindtext</a>

Anzahl Elemente in einer Spalte per TypoScript

In einem Projekt wurden die Elemente in der Header-Spalte immer als Slideshow ausgegeben – auch wenn nur ein Element in dieser Spalte sichtbar war. Ich habe nach einer Möglichkeit gesucht, im Template abzufragen, wie viele Elemente dargestellt werden müssen und dann statt Slideshow einfach nur das Bild auszugeben. Mit diesem TypoScript wird die Anzahl der Elemente korrekt ermittelt und kann dann an das Fluid Template übergeben werden. Es funktionert sogar mit Workspaces.

countHeaderElements = CONTENT
countHeaderElements {
    table = tt_content
    select {
        selectFields = count(uid) AS count
        pidInList = this
        where = {#colPos}=1
        andWhere = (deleted = 0 AND hidden = 0)
    }
 
    renderObj = COA
    renderObj {
        10 = TEXT
        10 {
            data = field:count
        }
    }
}

TCA für Typen überschreiben

Trotz der vielen Anleitungen, ist es jedesmal ein Try and Error das TCA für einen bestimmten Typ zu überschreiben. In diesem Fall sollte für das Element ‚video‘ die Auswahl der Dateitypen eingeschränkt werden.

Um ein Feld für alle Typen zu überschreiben, packt man folgenden Block in my_ext/Configuration/TCA/Overrides/tt_content.php

$GLOBALS['TCA']['tt_content']['columns']['header']['label'] = 'Foobar';

Was überschrieben werden kann, findet man im Backend über das Modul Configuration raus, in dem man sich das TCA anzeigen lässt.

Will man nun etwas für einen bestimmten Typen überschreiben, dann setzt man statt ['columns'] folgendes ein: ['types']['video']['columnsOverrides'].

$GLOBALS['TCA']['tt_content']['types']['introtext']['columnsOverrides']['header']['label'] = 'Foobar';

Bei den Felden mit Kindelementen wird es etwas komplizierter – das ist für alle Typen:

$GLOBALS['TCA']['tt_content']['columns']['assets']['config']['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed'] = 'youtube,vimeo';
$GLOBALS['TCA']['tt_content']['columns']['assets']['config']['filter'][0]['parameters']['allowedFileExtensions'] = 'youtube,vimeo';

Nach dem gleichen Prinzip wie beim Header kann diese Konfiguration nur für einen bestimmten Inhaltstyp vorgenommen werden:

$GLOBALS['TCA']['tt_content']['types']['video']['columnsOverrides']['assets']['config']['filter'][0]['parameters']['allowedFileExtensions'] = 'youtube,vimeo';
$GLOBALS['TCA']['tt_content']['types']['video']['columnsOverrides']['assets']['config']['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed'] = 'youtube,vimeo';

Event Listener mit Dependancy Injection

Um die News-Ausgabe mit weiteren Daten anzureichern, stellt News Events bereit, auf die reagiert werden kann. Das Event wird dabei in Configuration/Services.yaml registiert. In meinem Fall brauche ich in dem EventListener den Zugriff auf ein Repository. ObjectManager->get gilt seit TYPO3 10 als veraltet, injectXXX-Funktionen werden nicht ausgeführt. Was nun? Ein Fall für Dependancy Injection!

Der Basis Event-Listener sieht so aus und wird (in meinem Fall) in Classes/Event abgelegt:

<?php
use GeorgRinger\News\Event\NewsDateMenuActionEvent;
class EventListener
{
    /**
     * @var \MyNamespace\MyExtension\Domain\Repository\FooRepository
     */
    protected $fooRepository;
 
    /**
     * @param \MyNamespace\MyExtension\Domain\Repository\FooRepository $fooRepository
     */
    public function __construct(\MyNamespace\MyExtension\Domain\Repository\FooRepository $fooRepository)
    {
        $this->fooRepository = $fooRepository;
    }
 
    public function enrichWithAdditionalData(NewsDateMenuActionEvent $event): void
    {
 
    }
}

In Configuration/Services.yaml wird der Event-Listener registriert. Gleichzeitig wird definiert, welchen Typ die Variable $fooRepository beim Erstellen des Listeners haben muss. Wichtig sind dabei die letzten zwei Zeilen: dabei wird die Klasse FooRepository als public definiert, damit sie per Depencancy Injection in einem Listener instanziiert werden kann.

services:
  _defaults:
    autowire: true
    autoconfigure: true

  MyNamespace\MyExtension\Event\EventListener:
    arguments:
      $fooRepository: '@MyNamespace\MyExtension\Domain\Repository\FooRepository'
    tags:
      - name: event.listener
        identifier: 'news-event-listener'
        method: 'enrichWithAdditionalData'
        event: GeorgRinger\News\Event\NewsDateMenuActionEvent

  MyNamespace\MyExtension\Domain\Repository\FooRepository:
    public: true

Doku hier: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html

Zugriffsrechte in Tortoise Git ignorieren

Bei einem Projekt wurden die Zugriffsrechte der Dateien angepasst. Daraufhin wurden in Tortoise Git alle Dateien als bearbeitet aufgelistet. Allerding ist dabei die Anzahl Zeilen die dazugekommen bzw. entfernt wurden jeweils 0. Die Lösung ist einfach: (Forum-Eintrag siehe hier: https://superuser.com/questions/138669/why-is-tortoise-git-changing-my-file-permissions)

$ git config core.filemode false

Mask – IRRE Inhaltselemente ausgeben

Beim Erstellen von Mask Inhaltslementen kann man als Typ „Inhalte“ wählen. In der Anleitung fehlt die Angabe, wie man diese Inhaltselemente im Frontend ausgegeben bekommt. Dabei ist es so einfach:

<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:mask="http://typo3.org/ns/MASK/Mask/ViewHelpers"
      data-namespace-typo3-fluid="true">
 
<f:if condition="{data.tx_mask_irre_contents}">
    <f:for each="{data.tx_mask_irre_contents}" as="content">
        <mask:content uid="{content.uid}" />
    </f:for>
</f:if>

Seitentitel anpassen

Das ist wieder so ein Fall von „ich kann es mir nicht merken und die Google-Suche liefert immer die falschen Ergebnisse“.

Früher konnte TYPO3 nur Seitentitel der Form [Sitename]: [Seitentitel] generieren. Die einzige Möglichkeit, das zu ändern, bestand darin, die Option config.noPageTitle zu aktivieren und den Titel unter headerData selbst zusammenzustellen. Dabei gibt es schon seit langem Konfigurationsmöglichkeiten für den Seitentitel. Zum einen kann man die Reihenfolge einstellen, zum anderen kann man den Trenner angeben.

Ich war mir nicht sicher, wie die dafür notwendigen Konfigurationen heißen, daher wollte ich mal die Suche im Internet bemühen. Aber die Google Suche leitet mich nur auf Seiten mit dem config.noPageTitle Beispiel. Dabei ist es so einfach:

config {
    pageTitleFirst = 1
    pageTitleSeparator = -
    pageTitleSeparator.noTrimWrap = | | |
}

Ergebnis:
Meine Site - Seite ABC

Abschnittsnavigation (aka menu_section) mit Gridelements

Wenn man mit Gridelements und somit verschachtelten Inhaltselementen arbeitet, stellt man vielleicht irgendwann fest, dass die Sortierung nicht funktioniert, sobald in der Abschnittsnavigation anzuzeigende Inhaltselemente in Container (2-Spalten etc.) angeordnet werden. Beispiel (in Klammern ist die Position intern):

  • Container A (1)
    • Element A.1 (1)
    • Element A.2 (2)
  • Container B (2)
    • Element B.1 (1)
    • Element B.2 (2)

Je nachdem, in welcher Reihenfolge man die Elemente erstellt hat, kann z.B. folgende Abschnittsnavigation ausgegeben werden:

  • B.1
  • A.1
  • A.2
  • B.2

Ich habe z.B. folgende Lösung mit einem ViewHelper gefunden. Ich wollte jedoch eine Lösung, die auch für bereits vorhandene Inhaltselemente funktioniert und die unabhängig vom Template eingesetzt werden kann. So habe ich mich für einen DataProcessor entschieden.

Als erstes hole ich mir alle Inhaltselemente – unabhängig davon, ob sectionIndex gesetzt ist oder nicht. Dann hänge ich meinen eigenen DataProcessor ein.

tt_content.menu_section.dataProcessing.10.dataProcessing.20 {
    where >
}
tt_content.menu_section.dataProcessing.10.dataProcessing.30 = NP\MyExtension\DataProcessing\SectionProcessor

Im DataProcessor werden die Datensätze gefiltert, so dass nur die mit sectionIndex = 1 übrig bleiben. Weiterhin wird die Sortierung korrigiert und es wird neu sortiert. Beim neuen Sorting wird der Wert der Elemente, die direkt auf der Seite abgelegt sind, mit 10000 multipliziert. Falls das Element in einem Container liegt, dann wird die Sortierung auf den Sorting-Wert des Containers mal 10000 plus die eigene Sortierung gesetzt.

Den DataProcessor lege ich unter Classes\DataProcessing\SectionProcessor.php ab:

namespace NP\MyExtension\DataProcessing;
 
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
 
class SectionProcessor implements DataProcessorInterface
{
 
    /**
     * Process content object data
     *
     * @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
    ) {
        $processedContent = [];
        $sortMapping = [];
        foreach ($processedData['content'] as $content) {
            if ($content['data']['sectionIndex'] == 1) {
                $content['data']['sorting'] = $this->getRealSorting($content, $processedData['content']);
                $processedContent[] = $content;
            }
        }
        // debug($processedContent, 'processed content');
        usort($processedContent, [$this, 'sortContent']);
        $processedData['content'] = $processedContent;
        return $processedData;
    }
 
    /**
     * @param array $content
     * @param array $allElements
     */
    protected function getRealSorting($content, $allElements)
    {
        if ($content['data']['colPos'] != '-1') {
            return $content['data']['sorting'] * 10000;
        }
        $sorting = $content['data']['sorting'];
        foreach ($allElements as $element) {
            if ($element['data']['uid'] == $content['data']['tx_gridelements_container']) {
                $sorting += $element['data']['sorting'] * 10000;
            }
        }
        return $sorting;
    }
 
    /**
     * @param array $a
     * @param array $b
     * @return bool
     */
    protected function sortContent($a, $b)
    {
        return $a['data']['sorting'] > $b['data']['sorting'];
    }
}

Dieser Ansatz wird limitiert durch das Verschachtelungslevel der Elemente. Da könnte man diesen auf die schnelle implementierten Ansatz noch optimieren.