Identitätskrise

Wozu noch Programmierer? Eine Antwort, die ich mir selbst geben musste

Neulich, in einem Meeting, sagte ein Marketingleiter einen Satz, der mich kurz aus der Bahn geworfen hat:

„Wozu überhaupt noch Programmierer beschäftigen? Das macht doch alles die KI.“

Ich habe nicht sofort geantwortet. Nicht, weil ich keine Antwort hatte – sondern weil ein Teil von mir sich fragte, ob er nicht recht hat.

Die kurze Identitätskrise

Ich programmiere seit über 15 Jahren. TYPO3, PHP, Extbase, die ganze Bandbreite. Wenn jemand in einem Meeting so beiläufig meinen Beruf für überflüssig erklärt, trifft das etwas – auch wenn man weiß, dass es zu einfach gedacht ist.

Denn ja: Code zu schreiben ist heute schneller geworden. KI generiert Funktionen, schlägt Refactorings vor, findet Bugs. Vieles, wofür ich früher Stunden gebraucht habe, geht jetzt in Minuten. Das ist keine Bedrohung, das ist Fakt.

Aber der Satz des Marketingleiters verwechselt etwas: Er hält „Code schreiben“ für die eigentliche Arbeit. Das war sie nie.

Was eigentlich meine Arbeit ist

Ich bin nicht in erster Linie jemand, der Zeilen tippt. Ich bin Software-Architektin. Und das bedeutet etwas anderes:

Jemand muss das System als Ganzes verstehen. Nicht nur die einzelne Funktion, sondern wie Datenbank, Frontend, Schnittstellen, Sicherheit und Wartbarkeit zusammenspielen. KI sieht den Ausschnitt, den man ihr zeigt. Sie kennt nicht die Historie eines gewachsenen Systems, nicht die stillen Abhängigkeiten, nicht die Gründe, warum eine Altlast so ist, wie sie ist.

Jemand muss die Richtung vorgeben. KI ist ein extrem leistungsfähiges Werkzeug – aber ein Werkzeug ohne Ziel liefert Beliebiges. Die Entscheidung, welche Architektur trägt, welcher Ansatz in fünf Jahren noch wartbar ist, welches Risiko man eingehen kann und welches nicht: Das sind Urteile, keine Textvervollständigung.

Jemand muss die richtigen Fragen stellen. Ein guter Prompt ist kein Zufallsprodukt. Er entsteht aus Erfahrung – aus dem Wissen, welche Fehler typisch sind, welche Rückfragen wichtig sind, was eine KI-Antwort plausibel klingen lässt, obwohl sie falsch ist.

Die eigentliche Verschiebung

Der Beruf verändert sich nicht durch Wegfall, sondern durch Verschiebung. Weniger Zeit für das Tippen von Boilerplate-Code, mehr Zeit für Systemverständnis, Architekturentscheidungen und Qualitätskontrolle. Die Verantwortung wächst eher, als dass sie kleiner wird – denn am Ende steht immer noch jemand gerade, wenn das System versagt. Und das ist nicht die KI.

Was bleibt

Ich bin nicht die Person, die Code produziert. Ich bin die Person, die versteht, entscheidet und verantwortet, was gebaut wird – mit KI als Werkzeug, nicht als Ersatz. Der Satz im Meeting resultiert aus Nichtwissen und mangelnder Wertschätzung für meine Arbeit.

PS: Könnte man nicht eher Marketingleiter durch eine KI ersetzen? 😉

Fehler bei Aufruf der errorAction ausgeben

Falls es beim Aufruf einer Funktion zu einem Validierungsfehler kommt und dann auf die errorAction umgeleitet wird, dann steht dort im besten Fall nur ‚Es ist ein Fehler aufgetreten‘. Zugegeben, die Lösung unten ist quick and dirty, funktioniert aber:

In callActionMethod im ActionController (Zeile 574ff) einfach folgendes anpassen:

 if (!$validationResult->hasErrors()) {
	$this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments));
	$actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
} else {
// NEU
	DebuggerUtility::var_dump($validationResult, 'errors', 20);
	die;
	$actionResult = $this->{$this->errorMethodName}();
}

Ich habe im Beispiel DebuggerUtility::var_dump verwendet, damit ich die Tiefe der Ausgabe angeben kann.

Geschrieben in TYPO3, TYPO3 v11 | Kommentare deaktiviert für Fehler bei Aufruf der errorAction ausgeben

Manuelle Validierung eines Extbase-Objekts

Es basiert auf dem Beitrag von Torben Hansen von 2017 – es ist einfach nur eine aktuelle Version davon. In dem Fall musste ich sicherstellen, dass vor einer bestimmten Aktion die Benutzerdaten vollständig sind.

use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
 
$user = $this->getCurrentUser();
$validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class);
$validator = $validatorResolver->getBaseValidatorConjunction(FrontendUser::class);
$validationResults = $validator->validate($user);
 
if ($validationResults->hasErrors()) {
    // @todo cycle through errors in $validationResults->getFlattenedErrors()
}

Geschrieben in TYPO3, TYPO3 v11 | Kommentare deaktiviert für Manuelle Validierung eines Extbase-Objekts

Backend Module mit JavaScript und Styles

In meinem Beispiel will ich in einem eigenen Modul einen Datepicker verwenden und das Feld leeren können. Weiterhin soll ein kleines CSS im Backend-Modul eingebunden werden.

Die Registrierung der Module hat sich in TYPO3 12 geändert: Backend module configuration. Was ich dort in dieser Anleitung nicht gefunden habe: wie man in einem Modul z.B. einen Datepicker verwenden kann oder eigene CSS-Stylesheets einbindet.

Konfigurationsdatei JavaScriptModules.php im Order Configuration der Extension erstellen. Darin werden die Abhängigkeiten definiert und der Pfad zu den verfügbaren JS-Dateien. Als key in den Import gibt man an, wie das Modul dann im Template geladen werden kann.

Configuration/JavaScriptModules.php

return [
    'dependencies' => [
        'backend',
        'core',
    ],
    'imports' => [
        '@myextension/module/' => 'EXT:my_extension/Resources/Public/JavaScript/',
    ],
];

In der JS-Datei steht folgendes (das habe ich aus einem Core-Modul kopiert). Statt MyModule sollte der Name des eigenen Moduls eingesetzt werden.

Resources/Public/JavaScript/backend-module.js

import DateTimePicker from "@typo3/backend/date-time-picker.js";
import "@typo3/backend/input/clearable.js";
class MyModule {
    constructor() {
        this.clearableElements = null, this.dateTimePickerElements = null, DocumentService.ready().then((() => {
            this.clearableElements = document.querySelectorAll(".t3js-clearable"), this.dateTimePickerElements = document.querySelectorAll(".t3js-datetimepicker"), this.initializeClearableElements(), this.initializeDateTimePickerElements()
        }))
    }
 
    initializeClearableElements() {
        this.clearableElements.forEach((e => e.clearable()))
    }
 
    initializeDateTimePickerElements() {
        this.dateTimePickerElements.forEach((e => DateTimePicker.initialize(e)))
    }
}
 
export default new MyModule;

Im Template des Moduls kommt nun folgendes in die content-section, im Kommentar steht der Namespace des ViewHelpers, das muss natürlich ins html-Tag. Dort wird nun der Pfad eingesetzt, den man oben in JavaScriptModules.php festgelegt hat. Unter includeCssFiles kann man auch CSS-Dateien angeben, die im Modul eingebunden werden.

<!-- xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers" -->
 
<f:be.pageRenderer
    includeJavaScriptModules="{
        0: '@myextension/module/backend-module.js'
    }"
 
    includeCssFiles="{
        0: '{f:uri.resource(path: \'Css/backend.css\')}'
    }"
/>

Sobald in einem Feld die entsprechenden Klassen gesetzt werden, wird dort ein Datepicker generiert:

<div class="input-group">
    <f:form.textfield id="filter_datefrom"
                      property="datefrom"
                      class="form-control t3js-datetimepicker t3js-clearable"
                      data="{date-type: 'date'}"
    />
    <label class="mb-0 btn btn-default" for="filter_datefrom">
        <core:icon identifier="actions-calendar" />
    </label>
</div>

Ich weiß nicht, ob es der einzige Weg ist, oder ob es andere/bessere Möglichkeiten gibt. Ich schaue mir immer an, wie das die Core-Module machen und übernehme es für meine Module. Ich freue mich über Feedback 🙂

Extension Settings in Plugin überschreiben

In TYPO3 gibt es zwei Möglichkeiten settings oder allgemein Einstellungen für Plugins zu konfigurieren. Entweder man konfiguriert für die komplette Extension oder für einzelne Plugins. Da ich das nicht besser beschreiben kann, versuche ich es an einem Beispiel.

Angenommen, die Extension heißt my_extension und hat zwei Plugins pi1 und pi2. Die Konfiguration allgemein kann unter plugin.tx_myextension abgelegt werden und für die einzelnen Plugins unter plugin.tx_myextension_pi1 und plugin.tx_myextension_pi2. Die „allgemeine“ Konfiguration wird dabei an die Plugins vererbt.

plugin.tx_myextension.settings {
  var1 = Variable 1
}
 
plugin.tx_myextension_pi1.settings {
  var2 = Variable 2
}
 
plugin.tx_myextension_pi2.settings {
  var3 = Variable 3
}

Im ersten Plugin pi1 stehen damit in den Settings die Variablen var1 und var2 zur Verfügung, im Plugin pi2 die Variablen var1 und var3.

Was ist aber, wenn eine Variable entfernt werden soll? In diesem Fall funktioniert der > Operator leider nicht, da die Settings allgemein und Plugin als Array gemergt werden. Die Lösung ist das Wörtchen „__UNSET“, das ein entfernen des Schlüssels zur Folge hat.

plugin.tx_myextension_pi1.settings {
  var1 = __UNSET
}

Storage Pid ignorieren

Ich kann es mir einfach nicht merken… das folgende in die Repository Klasse:

public function initializeObject()
{
    $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class);
    $querySettings->setRespectStoragePage(false);
    $this->setDefaultQuerySettings($querySettings);
}

Fehlermeldung nach TYPO3 12 Update: The rendering context of ViewHelper f:uri.page is missing a valid request object

Wie es der Titel schon sagt, nach einem Update auf TYPO3 12 hat der Mailversand nicht mehr funktioniert, Fehlermeldung

the rendering context of ViewHelper f:uri.page is missing a valid request object

Dank Websuche bin ich schnell auf diesen Eintrag bei stackoverflow gestoßen.

In meinem Fall war es geringfügig anders, ich nutze FluidEmail, dort kann es aber genauso verwendet werden:

/** @var FluidEmail $email */
$email = GeneralUtility::makeInstance(FluidEmail::class);
$email->setRequest($this->request);
$email->subject($subject);
$email->setTemplate($templateName);
$email->assignMultiple($variables);

Geschrieben in TYPO3, TYPO3 v12 | Kommentare deaktiviert für Fehlermeldung nach TYPO3 12 Update: The rendering context of ViewHelper f:uri.page is missing a valid request object

Modul Template für Redakteure freigeben

Für ein Projekt, das ich übernommen habe, brauchen die Redakteure der Seite Zugriff auf TypoScript. Ich hätte es definitiv anders implementiert (Datensätze, Modul, etc.), mein Vorgänger hatte aber den Weg gewählt. Wenn also ein Benutzer Anpassungen am TypoScript vornehmen wollte, brauche er zwangsweise die Admin-Rechte. Seit TYPO3 10 gibt es noch die Abgrenzung zum System Maintainer, aber sobald ein Benutzer Admin-Rechte besitzt, können Sie nicht weiter eingeschränkt werden. „Mit großer Macht kommt große Verantwortung“ (Onkel Ben aus Spiderman) und ich traue den Redakteure nicht über den Weg mit Admin-Rechten. Also habe ich nach der Möglichkeit gesucht, das Modul und die Tabellen für die Redakteure freizugeben, damit sie wieder normale Redakteure sein können.

In TYPO3 11 hat das folgende gut funktioniert. Wahrscheinlich auch unter TYPO3 10.

1. Zugriffsrechte für Modul anpassen in my_extension/ext_tables.php
$GLOBALS['TBE_MODULES']['_configuration']['web_ts']['access'] = 'user,group';

2. Tabellen für Benutzer freigeben (dann tauchen sie in der Liste der erlaubten Tabellen auf) in my_extension/Configuration/TCA/Overrides/sys_template.php
$GLOBALS['TCA']['sys_template']['ctrl']['adminOnly'] = 0;

3. Benutzergruppe oder Benutzer bearbeiten und dort das Modul „Template“ und die Tabelle „sys_template“ erlauben.

In TYPO3 12 hat sich die Registrierung der Module geändert. Abgesehen davon gibt es nun mehrere Template-Module. Es ist natürlich auch ein Vorteil, denn die Redakteure brauchen sie ja nicht alle.

1. Event-Handler für die Modul-Konfiguration registrieren
Backend Module configuration

2. Event Listener implementieren

namespace Vendor\MyExtension\Backend\EventListener;
 
use TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent;
 
final class ModuleEventListener
{
    public function __invoke(BeforeModuleCreationEvent $event): void
    {
        // Change module icon of page module
        $modules = ['web_ts', 'web_typoscript_recordsoverview', 'web_typoscript_infomodify'];
        if (in_array($event->getIdentifier(), $modules)) {
            $event->setConfigurationValue('access', 'user');
        }
    }
}

Es reicht an dieser Stelle nicht, nur web_ts aufzulisten. Die verfügbaren Module sind die Keys in vendor/typo3/cms-tstemplate/Configuration/Backend/Modules.php

3. Benutzergruppe oder Benutzer bearbeiten und dort die Module „Template“, gewünschte Submodule und die Tabelle „sys_template“ erlauben.

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.

Geschrieben in TYPO3, TYPO3 v11 | Kommentare deaktiviert für LimitToPages in Site Config dynamisch setzen

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();

Geschrieben in TYPO3, TYPO3 v11 | Kommentare deaktiviert für Tabellenname in Repository ermitteln