Beiträge der Kategorie TYPO3

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.

TYPO3 auf Ddev installieren

Ja, es gibt Anleitungen da draußen, in denen das drin steht. Ich kann es mir einfach nicht merken … weder den Link noch den Inhalt.

Projekt-Ordner erstellen
mkdir spielwiese11

In Projekt-Ordner wechseln
cd spielwiese11

Ddev Projekt initalisieren
ddev config
Es kommen ein paar Fragen, bei docroot „public“ eingeben und bei Project type „typo3“.

Ddev starten
ddev start

In Ddev wechseln
ddev ssh

TYPO3 per Composer installieren
Hier ist der TYPO3 Composer Helper sehr hilfreich. Einfach die Version auswählen, die installiert werden soll (Minimal, Default oder Full), kopieren, einfügen und ausführen.

Dann kann man die Seite bereits unter https://spielwiese11.ddev.site aufrufen und den Rest im Browser erledigen.

Quellen:
https://docs.typo3.org/m/typo3/guide-contributionworkflow/main/en-us/Appendix/SettingUpTypo3Ddev.html

TYPO3 Extension Settings im Scheduler Task auslesen

Ich brauche die Settings eines Plugins im Scheduler. Es gibt zwar zahlreiche Snippets und Hinweise, wie es gehen soll, die alle nicht funktioniert haben. Dieses Snippet ist getestet unter TYPO3 10.

Da man sich im Backend befindet, müssen die Settings von plugin nach module kopiert werden:

module.tx_myextension < plugin.tx_myextension

Meine Extension heißt beispielhaft my_extension und ich habe im TypoScript die Konfiguration für alle Plugins gesetzt.

Im Scheduler-Task kann man auf die Settings dann wie folgt zugreifen. Zu beachten dabei die Schreibweise des Extensionnames, es ist CamelCase.

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
 
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configuration = $configurationManager->getConfiguration(
    ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
    'MyExtension'
);

Sonderbare Meldung im Modal beim Löschen der Seite

Es ist ein sehr spezieller Fall, den wahrscheinlich kaum jemand kennen wird, aber mir hat es nochmal etwas neues über die Konfiguration in TYPO3 beigebracht.

Mir wurde gemeldet, dass beim Löschen einer Seite aus dem Kontext-Menü im Seitenbaum folgende Meldung im Modal erscheint:

No content provided, please check your Modal configuration.

Ich wusste nicht direkt, welche Modal-Konfiguration gemeint ist, und habe mich durch den Code gehangelt. Erstmal die Meldung suchen – der Locallang-Key ist „mess.delete“. Dann suchen, wo diese Meldung generiert wird – das war im \TYPO3\CMS\Backend\ContextMenu\ItemProviders\RecordProvider in der Funktion getDeleteAdditionalAttributes. Dann rausgefunden, dass die Funktion nicht die korrekten Attribute liefert und dass es an if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) { ... } scheitert.

Schaut man sich die Funktion jsConfirmation genaue an, dann bekommt sie als Parameter eine Bitmaske. Diese Bitmaske steht in User TSconfig unter options.alertPopus. Ich hatte in meinem Fall diesen Wert gesetzt auf 250. Vergleicht man es mit der Dokumentation, ist dort 255 für „Alle Popups“ angegeben.

255 steht für die Bitmaske 11111111 und
250 steht für die Bitmaske 11111010

Das erste Bit von hinten steht für „On Type Change“, das dritte Bit von hinten für „Löschen“. Das ergibt sich aus den Angaben der Werte in der Dokumentation. 1 entspricht der Bitmaske 1, 4 entspricht der Bitmaske 100. Wenn man nun die Zahlen vergleicht, dann unterscheidet sich 255 und 250 genau an diesen zwei Stellen.

Es wäre natürlich besser, wenn das Modal gar nicht erst angezeigt wird, wenn in der Bitmaske das Löschen-Bit 0 ist. In diesem Fall habe ich es einfach dadurch „gelöst“, dass ich den Wert für options.alertPopups auf 255 gesetzt habe. Dann kommt zumindest keine verstörende Meldung 🙂

TCA für Image Manipulation (crop) in eigener Extension

Es gibt ein paar Beispiele im Netz, wie man Bild-Manipuation für tt_content aktiviert und Crop-Varianten (cropVariants) definiert. Ich habe kein Beispiel gefunden, wie man in seiner eigenen Extension direkt einem Bild-Feld Crop-Eigenschaften mitgeben kann. Nach langer Suche und viel rumprobieren ist das mein Ergebnis, das funktioniert.

'stage_image' => [
    'exclude' => true,
    'label' => 'LABEL',
    'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
        'stage_image',
        [
            'appearance' => [
                'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference'
            ],
            'overrideChildTca' => [
                'types' => [
                    '0' => [
                        'showitem' => '
                        --palette--;;imageoverlayPalette,
                        --palette--;;filePalette'
                    ],
                    \TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [
                        'showitem' => '
                        --palette--;;imageoverlayPalette,
                        --palette--;;filePalette'
                    ],
                ],
                'columns' => [
                    'crop' => [
                        'config' => [
                            'cropVariants' => [
                                'default' => [
                                    'title' => 'Desktop',
                                    'allowedAspectRatios' => [
                                        'default' => [
                                            'title' => 'Rechteckig 22:9',
                                            'value' => 2.4
                                        ],
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
            'foreign_match_fields' => [
                'fieldname' => 'stage_image',
                'tablenames' => 'xxx',
                'table_local' => 'sys_file',
            ],
            'maxitems' => 1,
        ],
        $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
    ),
],

TYPO3 Meldung bei gesperrten Records im Backend

Ich arbeite ja fast immer in der englischen Version, daher ist mir dieser Fehler nicht aufgefallen. Wenn ein anderer Benutzer einen Datensatz (z.B. eine Seite) zum Bearbeiten geöffnet hat, dann wird im TYPO3 Seitenbaum eine Meldung angezeigt. Im Englischen lautet die Meldung wie folgt:
„The user ‚%s‘ began to edit this record %s ago.“ Dann wird der Benutzertyp und das Datum eingesetzt, passt soweit.

Ist das Backend auf deutsch eingestellt, sieht diese Meldung etwas komisch aus.

Die Anführungsstriche werden mit HTML-Entities dargestellt.

Die Suche im Netz gab diesen wertvollen Beitrag aus der TYPO3-Doku aus: https://docs.typo3.org/m/typo3/reference-coreapi/9.5/en-us/ApiOverview/Internationalization/ManagingTranslations.html

Die Übersetzungen fürs Backend können also in einer eigenen Extension überschrieben werden.

Das in die ext_localconf.php einer eigenen Extension:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']['de']['EXT:core/Resources/Private/Language/locallang_core.xlf'][] = 'EXT:my_extension/Resources/Private/Language/de.locallang-core.xlf';

Dann unter dem angegebenen Pfad die xlf-Datei anlegen und folgendes rein:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2013-03-09T18:44:59Z" product-name="examples">
        <header/>
        <body>
            <trans-unit id="labels.lockedRecord" resname="labels.lockedRecord" approved="yes">
                <source>The user '%s' began to edit this record %s ago.</source>
                <target state="translated">Der Benutzer '%s' hat mit der Bearbeitung dieses Datensatzes vor %s begonnen.</target>
            </trans-unit>
            <trans-unit id="labels.lockedRecord_content" resname="labels.lockedRecord_content" approved="yes">
                <source>The user '%s' began to edit content on this page %s ago.</source>
                <target state="translated">Der Benutzer '%s' hat mit der Bearbeitung dieser Seite vor %s begonnen.</target>
            </trans-unit>
            <trans-unit id="labels.lockedRecordUser" resname="labels.lockedRecordUser" approved="yes">
                <source>The %s '%s' began to edit this record %s ago.</source>
                <target state="translated">Der %s '%s' hat mit der Bearbeitung dieses Datensatzes vor %s begonnen.</target>
            </trans-unit>
            <trans-unit id="labels.lockedRecordUser_content" resname="labels.lockedRecordUser_content" approved="yes">
                <source>The %s '%s' began to edit content on this page %s ago.</source>
                <target state="translated">Der %s '%s' hat mit der Bearbeitung dieser Seite vor %s begonnen.</target>
            </trans-unit>
        </body>
    </file>
</xliff>

Klappt! Hab ich schon erwähnt, dass ich TYPO3 toll finde 😀

TYPO3 – Domain Property als Boolean validieren

Ich habe lange gesucht und kein Beispiel gefunden, daher hoffe ich, dass ich damit jemandem die Sucherei und das Quellcode-Lesen ersparen kann.

Möchte man eine Property als Boolean validieren, dann lautet die Annotation wie folgt:

use TYPO3\CMS\Extbase\Annotation as Extbase;
 
/**
 * @var boolean
 * @Extbase\Validate(validator="Boolean", options={"is": "1"})
 * @Extbase\ORM\Transient
 */
protected $privacy;

Die Begründung ist, dass für die Abwärtskompabitilität nicht per Default validiert werden kann. Denn alle Properties vom Typ Boolean werden damit validiert.

TYPO3 auf ohne Composer umstellen

Ich persönlich arbeite mittlerweile sehr gerne mit Composer. Man kann schnell Extensions installieren/aktualisieren und TYPO3 aktualisieren. Bei jeden neuen Projekt arbeite ich mit Composer, außer es ist aus irgendwelchen Gründen doch nicht möglich – fehlende SSH Zugänge oder ähnliches. Manchmal kommt es vor, dass man eine mit Composer installierte TYPO3-Instanz von Composer wieder trennen muss.

Es ist eigentlich einfach – in der Datei vendor/typo3/cms-composer-installers/autoload-include.php nur den folgenden Abschnitt entfernen:

// TYPO3 is installed via composer. Flag this with a constant.
if (!defined('TYPO3_COMPOSER_MODE')) {
    define('TYPO3_COMPOSER_MODE', TRUE);
}

In meinem Fall hatte ich auch den crawler installiert. Nachdem ich den Abschnitt entfernt und alle Caches gelöscht hatte, wurde mein Backend nicht geladen mit der Fehlermeldung:

Warning: Uncaught TYPO3\CMS\Core\Error\Exception: PHP Warning: require([...]/public/typo3conf/ext/ crawler//Resources/Private/Php/ Libraries/vendor/autoload.php): failed to open stream: No such file or directory

In der ext_localconf.php der Extension crawler findet man folgendes:

if (!\TYPO3\CMS\Core\Core\Environment::isComposerMode()) {
require \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('crawler') . '/Resources/Private/Php/Libraries/vendor/autoload.php';
}

Der angegebene Pfad existiert aber nicht. Ein Ticket beim Crawler-Issue-Tracker und ein paar Stunden später ist die Lösung eigentlich banal: Extension crawler aus dem TER neu installieren. Sobald man das tut, dann werden die erforderlichen Pakete (die bei der Installation per Composer in vendor installiert werden) unter dem entsprechenden Pfad abgelegt.

Gedankensalat zu TYPO3 Umsetzungen

Ich arbeiten ja nun bereits seit mehreren Jahren (seit 2007?) mit TYPO3 und dachte, dass ich schon fast alles gesehen hätte an Implementierungen und Ansätzen. Immer wieder werde ich überrascht von kreativen Ansätzen mancher Entwickler und Agenturen und stelle fest, dass ich auf solche Ansätze nicht gekommen wäre. Es gibt in der Programmierung nicht die eine „Wahrheit“ (auch wenn manche gerne so tun als ob), sondern mehrere Lösungsmöglichkeiten für ein Problem. Manchmal denke ich darüber nach, ob in einem bestimmten Fall mein gewählter Ansatz der richtige ist. In der Retrospektive stelle ich dann bei fast allen Projekten fest, dass ich es aus heutiger Sicht anders gemacht hätte – und das ist auch gut so, das bedeutet, dass man sich persönlich weiterentwickelt hat. Als Freelancerin habe ich Einblick in viele Projekte, an denen vorher andere Agenturen oder Freelancer gearbeitet haben und manchmal würde ich (gelegentlich nach stundenlangem Fluchen) diese gerne fragen, warum sie dieses oder jenes so umgesetzt haben und nicht anders.

Das sind grob die Szenarien, die mir bisher begegnet sind

1) Nicht konfigurierbare Zielseiten/Texte/E-Mail-Adressen etc.
Wenn man schon dabei ist, einen Text oder eine E-Mail-Adresse einzugeben, sollte man sie nicht per TypoScript/Flexform/Datenbank-Feld konfigurierbar machen? Natürlich ist es schön, noch etwas Geld zu verdienen, wenn der Kunde eine Textkorrektur haben möchte oder die E-Mail-Adresse sich geändert hat. Habe ich dazu Lust? Nein.

2) Keine Verwendung von TYPO3 (Core) Konfigurationsmöglichkeiten
Man kann sich in TYPO3 mittels TSconfig doch zu Tode konfigurieren – was ich gerne nutze. Wenn das nicht hilft, gibt es doch Hooks und man kann auch direkt in ext_localconf und ext_tables konfigurerieren. Mit Backend Layouts und Content Defender kann man mittlerweile ziemlich genau einschränken, welche Inhaltselemente in welchen Spalten erlaubt sind. Letztens bin ich über eine Extension gestolpert, die den Content Element Browser und die Auswahl der Inhaltselemente (mit Hooks, höhö) kompett umgeschrieben hat. Welche Inhaltselemente erlaubt sind, steht in einer JSON-Datei. Wenn man diese Schema immer wieder verwendet, ist es natürlich gut. Wenn man als Agentur Entwickler ausbildet, dann zeigt man denen einmal, wie es funktioniert und schon können sie Inhaltselemente zusammenkloppen. Ich bezweifle jedoch, dass sie verstehen, was da im Hintergrund passiert. Jemand, der dieses Schema nicht kennt und das Projekt anschließend übernimmt, muss sich in diese Implementierung reindenken und entweder weiter damit arbeiten oder es so umschreiben, dass es näher an den Vorgaben ist.

3) Inhaltselemente nur für eine bestimmte Verwendung implementieren
Das bezieht sich vor allem aufs Frontend. Immer wieder erlebe ich, dass die Inhaltselemente nur genauso wie bisher eingepflegt gut aussehen und anderswo zwar positioniert werden können, aber dann das Layout zerschießen. Ich sehe TYPO3 mehr wie Duplo: verschiedene Bausteine und ich kann sie beliebig einsetzen. Ein Akkordion z.B. kann im Header, Inhalt und Footer eingesetzt werden und funktioniert dort immer wie erwartet. Wenn das nicht möglich sein soll, dann konfiguriere ich ihn in der Spalte weg (siehe Punkt 2) oder ich sorge per CSS dafür, dass er an allen drei Locations gut aussieht. Bei den meisten Implementierung ist es eher Mikado: So steht es gerade, aber bloß nicht anfassen.
Auch prüfe ich, ob ein Element zweimal auf der Seite einsetzen werden kann! Letztens gerade wieder eine Implementierung gesehen, bei der einem solchen Element ein feste HTML-id zugeteilt wurde und dann mit JavaScript irgendwas damit gemacht wurde. Blöd nur, wenn der Kunde das Element zweimal auf einer Seite einsetzt.

Das reicht fürs erste an Gruselgeschichten. Es hat auch Vorteile, dass mir immer wieder unterschiedliche Projekte begegnen. Zum einen werden meine grauen Zellen stimuliert – ich muss mich in andere Ansätze und Lösungen reindenken und arbeite nicht immer nach dem gleichen Schema. Zum anderen hinterfrage und optimiere ich immer wieder meine eigenen Ansätze und kann so das (zu dem jeweiligen Zeitpunkt) beste Ergebnis abliefern. Und diesen Aspekt mag ich besonders an meinem Freelancer-Dasein…

TYPO3 9 – Twitter-/Facebook Meta-Angaben mit Fallback auf Standardfelder

In TYPO3 9 gibt nach der Installation der Core-Extenson seo in den Seiteneigenschaften Felder, um Meta-Informationen für Facebook und Twitter zu hinterlegen. Diese Informationen werden im Quellcode ausgegeben, wenn man die Felder entsprechend ausgefüllt hat. Nun möchte man nicht explizit immer diese Informationen angeben müssen, vor allem weil der Facebook-/Twitter-Beschreibungstext wahrscheinlich eh identisch ist mit dem Beschreibungstext der Seite. Und die Bilder können automatisch aus dem Feld media generiert werden, wenn es denn befüllt ist.

Mit dem folgenden TypoScript lassen sich Beschreibung und Seitentitel in die Facebook-/Twitter-Meta-Angaben der Seite schreiben. Sobald die „richtigen“ Felder ausgefüllt werden, dann werden die Angaben daraus ausgegeben.

page.meta {
    og:title.cObject = TEXT
    og:title.cObject {
        field = title
    }
 
    og:description.cObject = TEXT
    og:description.cObject {
        field = description
        # data = levelfield:-1 , description, slide
    }
 
    og:image.cObject = IMG_RESOURCE
    og:image.cObject.file {
        import.data = levelmedia:-1, slide
        treatIdAsReference = 1
        import.listNum = 0
        maxW = 1200
        cropVariant = default
    }
    og:image.cObject.stdWrap.dataWrap = {getIndpEnv:TYPO3_REQUEST_HOST}/
}

Damit es mit dem Levelfield Slide funktioniert, müssen die Felder in addRootLineFields aufgenommen werden. Das kann entweder „global“ in der LocalConfiguration.php gemacht werden oder die Extension fügt diese der vorhandenen Liste hinzu.

In meinem Beispiel habe ich weiterehin Crop Varianten für das Media-Feld der Seite definiert und greife auf diese zu. Wenn das nicht gewünscht ist, dann die Zeile mit cropVariants = default einfach weglassen.

Geschrieben in TYPO3, TYPO3 v9 | Kommentare deaktiviert für TYPO3 9 – Twitter-/Facebook Meta-Angaben mit Fallback auf Standardfelder