Beiträge von natalia

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']
    ),
],

Redirect von URLs mit .html auf /

Vor TYPO3 9 und dem Routing im Core war RealURL sehr gnädig, was URLs anging. So konnte eine Seite mit .html Suffix und auch mit / aufgerufen werden (mein Empfinden). In TYPO3 10 funktioniert diese Verhalten nur in eine Richtung. Konfiguriert man eine .html Endung über den PageSuffix, ist die Seite weiterhin mit Trailing Slash aufrufbar. Es ist eigentltich gut, dass entweder das eine oder das andere geht. Wenn sie jemand die URL mit .html Endung gebookmarked hat und dann statt der Seite 404 bekommt, ist es ärgerlich.

Dank dieses sehr guten Beitrags auf stackoverflow hier mein Redirect-Schnipsel für die htaccess-Datei.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)\.html$ /$1/ [L,R=302]

Als Tipp stand in dem Beitrag, dass man während des Testings den Fehlercode 302 verwenden sollte (found) und erst wenn es final ist, dann 301, weil dieser vom Browser gecached wird.

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 😀

Sport

Dieser Blogbeitrag beschäftigt sich mit meiner zweiten Leidenschaft neben TYPO3 – dem Sport. Ich beschreibe meine Sport-Karriere und wie sich das ganze im letzten Jahr pandemie-geschuldet verändert hat.

(mehr …)

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.