Beiträge von natalia

LimitToPages in Site Config dynamisch setzen

Im Moment implementiere ich eine Extension, bei der es unter anderem digitale Produkte gibt. Die Produkte können jeweils von einem Typ sein – z.B. Kurs oder Tutorial. Der Kunde hat sich für die Produkte sprechende URLs gewünscht, die den Produkttyp enthalten – z.B. /kurs/produkt-1 oder /tutorial/produkt-2. Da schien es mir sinnvoll, pro Produkt-Typ eine separate Detailseite anzugelen, deren Slug entsprechend zu setzen und in der Site Config hübsche URLs zu konfigurieren.

Nun gibt es folgendes Problem: die Produkt-Typen stehen in der Datenbank und können erweitert werden. Die Site Config hingegen ist ja dateibasiert und wird versioniert. Bei der Konfiguration der hübschen URLs kann mit limitToPages angegeben werden, für welche Seiten die URLs angewendet werden. Mal absehen davon, dass die IDs der Detailseiten sich lokal, auf Stage und Live durchaus unterscheiden können, wäre es nicht schön, wenn limitToPages dynamisch aus der Datenbank ermittelt werden könnte.

Seit TYPO3 10.3. gibt es die Möglichkeit, in der Site Config Placeholder zu verwenden und diese mit eingenen Prozessoren zu verarbeiten. In der Dokumentation findet sich ein Beispiel: YAML API.

Und so sieht man Implementierung aus. In der Site Configuration config.yaml gebe ich die Platzhalter an:

limitToPages: '%detailPid(product)%'

In LocalConfiguration.php wird der Processor registriert:

<?php
[...]
'yamlLoader' => [
    'placeholderProcessors' => [
        \Example\MyExtension\Configuration\Processor\DetailPagePlaceholderProcessor::class => [
            'after' => [
                \TYPO3\CMS\Core\Configuration\Processor\Placeholder\ValueFromReferenceArrayProcessor::class
            ]
        ]
    ]
],
[...]

Und das ist der Processor:

<?php
declare(strict_types=1);
 
namespace Example\MyExtension\Configuration\Processor;
 
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\PlaceholderProcessorInterface;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
 
class DetailPagePlaceholderProcessor implements PlaceholderProcessorInterface
{
    public function canProcess(string $placeholder, array $referenceArray): bool
    {
        return strpos($placeholder, '%detailPid(') !== false;
    }
 
    public function process(string $value, array $referenceArray)
    {
        $detailPages = $this->getDetailPages();
 
        // Throw this exception if the placeholder can't be substituted
        if (empty($detailPages)) {
            throw new \UnexpectedValueException('No detail page found', 1692345790);
        }
        return $detailPages;
    }
 
    private function getDetailPages()
    {
        $table = 'tx_myextension_domain_model_type';
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
        $queryBuilder
            ->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
 
        $result = $queryBuilder
            ->select('*')
            ->from($table)
            ->where(
                $queryBuilder->expr()->gt('detail_pid',
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
            )
            ->execute()
            ->fetchAllAssociative();
        $detailPid = [];
        foreach ($result as $row) {
            $detailPid[] = $row['detail_pid'];
        }
        return array_unique($detailPid);
    }
}

Ich will nicht ausschließen, dass das Beispiel nicht funktioniert, denn das ist ein angepasster Demo-Code, der keine tatsächlichen Kundennamen oder Extension-Namen enthält.

Tabellenname in Repository ermitteln

Das ist ein Update zu meinem Beitrag von 2012. Mit diesem Coder kann im Repository der Tabellen-Name des zugehörigen Models ermittelt werden. Falls mal mit QueryBuilder eine Abfrage gebastelt werden muss. So funktioniert es nun in TYPO3 11:

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
[...]
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
$tableName = $dataMapper->getDataMap($this->objectType)->getTableName();

E-Mail Versand defekt nach Plesk Update

Nach einem Plesk Update bei einem meiner Kunden funktionierte auf einmal der Mailversand nicht mehr. Selbst das Senden einer Test-E-Mail aus dem Backend lieferte folgende Fehlermeldung:

Core: Exception handler (WEB): Uncaught TYPO3 Exception: #451: Expected response code "250/251/252" but got code "451", with message "451 qmail-spp failure: plugins/chkrcptto: can't execute (#4.3.0)". | Symfony\Component\Mailer\Exception\TransportException thrown in file [...]vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php

Man kann den Fehler lösen, wenn man den sendmail-Befehlt in den Settings explzit angibt, z.B. /usr/sbin/sendmail -t

Wenn unter $GLOBALS['MAIL']['transport_sendmail_command'] nicht angegeben ist, dann wird als Fallback /usr/sbin/sendmail -bs verwendet. Die Option -bs bedeutet folgendes: „Stand-alone SMTP server mode. Read SMTP commands from standard input, and write responses to standard output. In stand-alone SMTP server mode, mail relaying and other access controls are disabled by default. To enable them, run the process as the mail_owner user.“ (Quelle)

Meta-Felder Fallbacks

Aus dem Stehgreif weiß ich so was natürlich nicht. In der offiziellen Doku ist eine Anleitung am Beispiel von og:image beschrieben, hier ist der Schnipsel für og:title.

page = PAGE
page {
    meta {
        og:title.stdWrap.cObject = TEXT
        og:title.stdWrap.cObject {
            if.isFals.field = og_title
            field = title
        }
    }
}

Warning im TYPO3 Backend

Nach einem Update auf TYPO3 11 und PHP 8.1 hatte ich im Backend folgende Warnung:


Core: Error handler (BE): PHP Warning: Undefined array key type in /var/www/html/vendor/symfony/expression-language/Node/GetAttrNode.php line 97

Ich hatte schon die Vermutung, dass es mit TypoScript-Conditions zu tun hat. Dieser Blog-Artikel auf typo3-probleme.de hat mich auf die richtige Anpassung gebracht.

So muss die Condition angepasst werden, damit es keine Warnung mehr gibt:

[request.getQueryParams()['type'] == 1452982642]
[traverse(request.getQueryParams(), 'type') == 1452982642]

Exports aus TYPO3 Backend

Für einen Kunden habe ich Module entwickelt, in denen die dargestellten Daten als CSV exportiert werden könnne. Ursprünglich für TYPO3 8 entwickelt, wurden die Module in den letzten Jahren mehrfach aktualisiert, im Moment für TYPO3 11.

Ein Export in einem Backend-Modul kann wie folgt implementiert werden:

public function exportContactListAction(): ResponseInterface
    {
        $records = $this->recordRepository->findAll();
 
        $filename = 'records_list' . '_' . date('Y-m-d') . '.csv';
        $export = $this->createExport($records);
 
        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/octet-stream')
            ->withHeader('Content-Length', (string)strlen($export))
            ->withHeader('Content-Disposition', 'attachment; filename=' . PathUtility::basename($filename));
        $response->getBody()->write($export);
 
        return $response;
    }

In der Funktion createExport kann ein beliebiger Content generiert werden. Als praktisch hat sich für mich folgende Funktion erwiesen:

CsvUtility::csvValues($dataRow, ';') . "\n";

Man packt die zu exportierenden Daten in ein Array und macht ein Csv daraus.

Update Stress und neues Setup

Seit bestimmt zwei Jahren arbeite ich wie folgt: Docker Desktop, DDEV und WSL mit Ubuntu. Dazu nutze ich schon seit langer, langer Zeit mit PhpStorm mit ein paar nützlichen Plugins etc. Dabei liegen die Projekte in der Ubuntu Distribution und ich greife per \\wsl$\ auf sie zu. Im letzten Jahr hat PhpStorm schonmal Probleme bereitet – seit einem Update konnten auf einmal keine Projekte von WSL mehr geladen werden. Zum Glück war ich nicht die einzige mit diesem Problem und durch ein Rollback auf die vorherige Version konnte das Problem behoben werden.

Seit einem Windows Update (?) am Dienstag hatte ich nun wieder das Problem, dass PhpStorm keine Projekte aus der WSL laden wollte. Ich habe mehrmals verschiedene PhpStorm-Versionen installiert, leider immer das gleiche Ergebnis. Ein Kollege von mir schlug vor, alles (WSL, Ubuntu etc.) neu zu installieren. Gute Idee, aber dabei gingen ja die in Ubuntu abgelegten Projekte komplett verloren und mussten daher erstmal gesichert werden. In Linux ist ja das Windows-Laufwerk gemountet, der Zugriff aber ziemlich langsam, daher habe ich alle Projekte aus Ubuntu nach Windows verschoben. Das hat entsprechend den ganzen Tag gedauert. Am Ende des Tages alles neu installiert – und wieder das gleiche Ergebnis – langsamer Zugriff im Explorer und kein Zugriff aufs WSL in PhpStorm.

Dieser Kollege hat aber ein anderes Setup: bei ihm läuft DDEV direkt in Windows. Er startet ddev als nicht wie ich aus Ubuntu, sondern in PowerShell. Das hat bei mir leider nicht funktioniert. Beim Aufruf von ddev in PowerShell kamen nur Fehlermeldungen.

Auf purer Verzweiflung entschloss ich mich, ein vollständiges Backup einzuspielen. Ich nutze seit Jahren Acronis, um vollständiges Backup meiner Hauptplatte zu erstellen. Nun sollte sich zeigen, ob das funktioniert. Das Backup war von Dienstag früh, also zum Glück nicht so alt. Den ganzen Donnerstag rödelte mein armer Rechner, um das Backup einzuspielen (10 Std.?). Die Zeit habe ich zumindest sinnvoll genutzt, um eine Entwicklungsumgebung auf meinem Laptop einzurichten. Und da hat sich gezeigt, dass das folgende Setup erstaunlich gut funktioniert: Docker mit Hyper-V. Voraussetzung dafür ist, dass man Windows Professional nutzt und Virtualisierung erlaubt ist. Dann musste ich Docker nur noch neu installieren und dabei den Haken bei „WSL nutzen“ entfernen.

Das Einspielen des Backups hat sehr gut funktioniert – ein hoch auf Acronis. Das Problem war aber immer noch da – der Zugriff auf WSL war erstaunlich lahm. Nun habe ich also auf meinem Desktop-Rechner ebenfalls das genannte Setup mit Hyper-V und hoffe, dass es lange und update-sicher funktioniert.

TYPO3 Vhs Sprachnavigation Standardsprache fehlt

Ich selbst nutze die Extension Vhs sehr selten, betreue gelegentlich Projekte, die es einsetzen und komme daher nicht drumrum, mich damit zu beschäftigen.

Folgendes Setup: eine Seite mit drei Sprachen – Deutsch [0], Englisch [1] und Polnisch [2]. Die Zahl in Klammern ist die ID der Sprache im System. Wenn ich in Deutsch bin, dann möchte ich zur Auswahl Englisch und Polnisch, auf der englischen Seite Deutsch und Polnisch und auf der polnischen eben Deutsch und Englisch.

So kann man in Flux die Sprachnavigation einsetzen:

<v:page.languageMenu layout="name" />

Wenn man die Auswahl der Sprachen einschränken möchte, dann kann man einen weiteren Parameter mitgeben und darin eine kommaseparierte Liste mit Sprachen.

<v:page.languageMenu layout="name" languages="0,1,2" />

Meiner Logik nach sollte die Sprachauswahl bleiben, wie sie ist. Stattdessen verschwindet die Standardsprache aus der Navigation. Warum das?
In vhs/Classes/ViewHelpers/Page/LanguageMenuViewHelper.php Zeile 292ff.

$limitLanguages = static::arrayFromArrayOrTraversableOrCSVStatic($this->arguments['languages'] ?? []);
$limitLanguages = array_filter($limitLanguages);

Da wird der Wert aus dem Argument mit array_filter verarbeitet. Wird array_filter kein Callback übergeben, dann entfernt es alle leeren Elemente aus dem Array gem. der Definition von empty. Und empty sieht 0 auch als leer an. Damit wird aus der Liste 0,1,2 die 0 entfernt.

Und dann ist da ja noch das Problem mit dem ausblenden der aktiven Sprache. Da wäre ein Parameter sinnvoll gewesen. Die Lösung, um immer die nicht aktiven Sprachen anzuzeigen ist somit ein manuelles Rendering der Sprachnavigation, ohne die Sprachliste einzuschränken, ist somit diese:

<v:page.languageMenu as="languageMenu" flagImageType="png" layout="name">
    <ul>
        <f:for each="{languageMenu}" as="language">
            <f:if condition="{language.current} != 1">
                <li>
                    <a href="{language.url}">{language.label}</a>
                </li>
            </f:if>
        </f:for>
    </ul>
</v:page.languageMenu>

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>

Geschrieben in TYPO3, TYPO3 v10 | Kommentare deaktiviert für Linkhandler mit Links zu Sections