Beiträge der Kategorie TYPO3

Übersetzung bestimmte Sprache im Fluid Template ausgeben (LocalizationUtility)

Für mein aktuelles Projekt sollte es etwas abgefahrenes sein: In einem Template sollen nebeneinander ein Wort in Sprache 1 und dann das gleiche Wort in Sprache 2 ausgegeben werden. Problem ist, das der <f:translate key="" /> ViewHelper immer nur mit der aktuellen Sprache arbeitet. Mein Ansatz ist also einen eigenen ViewHelper zu schreiben. Nun ruft der TranslateViewHelper (in einer statischen Funktion) die LocalizationUtility auf und diese liefert die Übersetzung. d.h. ich werde wohl auch eine eigene LocalizationUtility brauchen.

Mein ViewHelper soll in der Verwendung so aussehen: <my:translate key="lang[fr]:years" />
Ich kann/möchte kein weiteres Argument an den ViewHelper dranhängen, weil mein TranslateViewHelper dann nicht mehr von zum Teil statischen Core-TranslateViewHelper ableiten kann. Und der $key wird eh durchgereicht, also nutze ich den Key, um meine Sprache zu definieren.

Eigenen TranslateViewHelper implementieren, der „meine“ LocalizationUtility nutzt

namespace My\Extension\ViewHelper;
use My\Extension\Utility\LocalizationUtility;
class TranslateViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\TranslateViewHelper
{
    protected static function translate($id, $extensionName, $arguments)
    {
        return LocalizationUtility::translate($id, $extensionName, $arguments);
    }
}

Dann muss „nur“ noch die LocalizationUtility angepasst werden. In der translate-Funktion wird der $key geparst und falls er einen Hinweis auf eine konkrete Sprache enthält, wird diese in $overrideLanguageKey gesetzt. Wichtig ist keine der anderen Variablen zu ändern, denn sobald ich sie für ein Label ändere, gilt die Änderung für alle folgenden Labels. Später überprüfe ich, ob $overrideLanguageKey gesetzt ist, lade die Übersetzungen und liefere die angeforderte Übersetzung zurück. Das ist nicht der komplette Code, sondern nur die relevanten Auszüge.

<?php
namespace My\Extension\Utility;
class LocalizationUtility extends \TYPO3\CMS\Extbase\Utility\LocalizationUtility
{
    /**
     * @var string
     */
    protected static $overrideLanguageKey = '';
 
    /**
     * Returns the localized label of the LOCAL_LANG key, $key.
     *
     * @param string $key The key from the LOCAL_LANG array for which to return the value.
     * @param string $extensionName The name of the extension
     * @param array $arguments the arguments of the extension, being passed over to vsprintf
     * @return string|NULL The value from LOCAL_LANG or NULL if no translation was found.
     */
    public static function translate($key, $extensionName, $arguments = null)
    {
        if (GeneralUtility::isFirstPartOfStr($key, 'lang[')) {
            preg_match('#^lang\[(.*?)\]\:(.*?)$#', $key, $matches);
            // set key
            $key = $matches[2];
            self::$overrideLanguageKey = $matches[1];
        }
        [...]
            self::initializeLocalization($extensionName);
            /* NEW */
            $languageKey = self::$languageKey;
            if(self::$overrideLanguageKey) {
                self::initializeLocalizationFor($extensionName, self::$overrideLanguageKey);
                $languageKey = self::$overrideLanguageKey;
            }
            // The "from" charset of csConv() is only set for strings from TypoScript via _LOCAL_LANG
            if (!empty(self::$LOCAL_LANG[$extensionName][$languageKey][$key][0]['target'])
                || isset(self::$LOCAL_LANG_UNSET[$extensionName][$languageKey][$key])
            ) {
                // Local language translation for key exists
                $value = self::$LOCAL_LANG[$extensionName][$languageKey][$key][0]['target'];
            } elseif (!empty(self::$alternativeLanguageKeys)) {
            [...]
    }
 
    /**
     * @param string $extensionName
     * @param string $languageKey
     * @return void
     */
    protected static function initializeLocalizationFor($extensionName, $languageKey)
    {
        $locallangPathAndFilename = 'EXT:' . GeneralUtility::camelCaseToLowerCaseUnderscored($extensionName) . '/' . self::$locallangPath . 'locallang.xlf';
        $renderCharset = TYPO3_MODE === 'FE' ? self::getTypoScriptFrontendController()->renderCharset : self::getLanguageService()->charSet;
 
        /** @var $languageFactory LocalizationFactory */
        $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
 
        self::$LOCAL_LANG[$extensionName] = $languageFactory->getParsedData($locallangPathAndFilename, $languageKey, $renderCharset);
    }
}

Im Template können schließlich beide TranslateViewHelper nebeneinander verwendet werden:

<f:translate key="years" />
<my:translate key="lang[fr]:years" />

Hook-Implementierung für Linkhandler

Heute habe ich die Extension linkhandler (allerdings die neue Version) konfiguriert, damit die Redakteure in der Site auf Artikel und weitere von mir programmierte Dateitypen verlinken können. Die Konfiguration des Linkhandler in TsConfig ist sehr gut beschrieben, so ich mir hier die Konfiguration spare. Was allerdings gefehlt hat, war ein Beispiel für den beschriebenen Hook. Ich habe in dem Code unten jeweils Dummy-Namespaces und Extension-Namen eingesetzt, der Code funktioniert, muss aber modifiziert werden.

Damit man z.B. Links zu eigenen Datensätzen erstellen kann, braucht man ein wenig TsConfig:

TCEMAIN.linkHandler.tx_extension_myrecord {
    handler = Cobweb\Linkhandler\RecordLinkHandler
    label = Mein Datentyp
    configuration { ... }
    scanBefore = page
}

In ext_localconf.php wird der Hook registriert:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkhandler']['generateLink'][] = \My\Extension\Hooks\LinkhandlerParameterProcessor::class;

Dann erstellt man im Ordner Classes/Hooks der eigenen Extension die Datei LinkhandlerParameterProcessor.php mit folgendem Inhalt:

<?php
namespace My\Extension\Hooks;
 
use Cobweb\Linkhandler\ProcessLinkParametersInterface;
 
class LinkhandlerParameterProcessor implements ProcessLinkParametersInterface
{
 
    /**
     * @param \Cobweb\Linkhandler\TypolinkHandler $linkHandler Back-reference to the calling object
     * @return void
     */
    public function process($linkHandler) {
        $configurationKey = $linkHandler->getConfigurationKey();
        if($configurationKey == 'tx_extension_myrecord.') {
            $record = $linkHandler->getRecord();
            $additionalParams = '&tx_extension_test[parent]='.$record['parent'];
            $additionalParams.= '&tx_extension_test[record]='.$record['uid'];
            $additionalParams.= '&tx_extension_test[controller]=Test';
            $typolinkConf = $linkHandler->getTypolinkConfiguration();
            $typolinkConf['additionalParams'] = $additionalParams;
            $typolinkConf['title'] = $record['title'];
            $linkHandler->setTypolinkConfiguration($typolinkConf);
        }
    }
 
}

Mit $linkHandler->getConfigurationKey() kommt man an der Configuration Key – nämlich den, den man vorher im TsConfig verwendet hat. Es gibt einige Funktionen im LinkHandler wie getTableName() oder getLinkParameters(), von denen scheinbar keine verwendet wird. Somit bleibt nur der Weg über die Typolink Konfiguration: mit getTypolinkConfiguration() auslesen und mit setTypolinkConfiguration() wieder setzen.

In meinem Fall habe ich den Hook benötigt, weil ich an den Link nicht nur die UID des Datensatzes hängen musste, sondern auch noch einen weiteren Parameter aus dem Datensatz.

Nachtrag: Ich habe erst später festgestellt, das ich mein Problem auch komplett mit TypoScript lösen kann. Na ja, vielleicht hilft ja das Beispiel jemandem

plugin.tx_linkhandler {
    tx_extension_myrecord {
        typolink {
            title = {field:title}
            title.insertData = 1
            parameter = 74
            additionalParams = &tx_extension_test[parent]={field:parent}&tx_extension_test[record]={field:uid}&tx_extension_test[controller]=Test
            additionalParams.insertData = 1
            useCacheHash = 1
        }
    }
}

Name von fe_user beim Speichern im Backend aus first_name und last_name setzen

Ich glaube mich daran zu erinnern, dass es mal in irgendeiner Registrierungsextension implementiert war. Man wünscht sich, dass der Name eines FE Benutzers (fe_user) sich automatisch aus Vor- und Nachname ergibt, andernfalls sind die Felder ja redundant.

Im Backend kann man mit einem Hook realisieren.

Erstmal muss man das Namensfeld des fe_user auf readOnly setzen, damit man es im BE nicht bearbeiten kann. Das geht am besten in Configuration/TCA/Overrides/fe_users.php der eigenen Extension:

$GLOBALS['TCA']['fe_users']['columns']['name']['config']['readOnly'] = 1;

Dann definiert man einen Hooks für das Backend-Formular: in ext_localconf.php folgendes einsetzen (My\Extension entsprechend ersetzen):

$GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['extkey'] = 'My\Extension\Hook\TceMain';

Dann im Ordner Classes/Hook der Extension die Datei TceMain.php erstellen und darin eine Funktion wie folgt definieren:

<?php
namespace My\Extension\Hook;
 
use TYPO3\CMS\Backend\Utility\BackendUtility;
 
class TceMain
{
 
    /**
     * @param $status
     * @param $table
     * @param $id
     * @param array $fieldArray
     * @param \TYPO3\CMS\Core\DataHandling\DataHandler $pObj
     */
    public function processDatamap_postProcessFieldArray($status, $table, $id, array &$fieldArray, \TYPO3\CMS\Core\DataHandling\DataHandler &$pObj)
    {
        if ($table == 'fe_users') {
            if (array_key_exists('first_name', $fieldArray) || array_key_exists('last_name', $fieldArray)) {
                $oldRow = BackendUtility::getRecord('fe_users', $id);
                $firstName = (array_key_exists('first_name', $fieldArray)) ? $fieldArray['first_name'] : $oldRow['first_name'];
                $lastName = (array_key_exists('last_name', $fieldArray)) ? $fieldArray['last_name'] : $oldRow['last_name'];
                $fieldArray['name'] = $firstName . ' ' . $lastName;
            }
        }
    }
 
}

Dabei wird beim Speichern des Datensatzes der Name automatisch aktualisiert, falls first_name oder last_name bearbeitet wurden. Diese Funktion funktioniert in beiden Fällen: wenn man nur einen Datensatz vollständig bearbeitet oder wenn man mehrere Datensätze gleichzeitig bearbeitet.

Landauswahl aus static_info_tables in Extension femanager

Für die Benutzerregistrierung nutze ich die Extension femanager, die sich hervorragend konfigurieren lässt.

Im Extension Manual ist beschrieben, wie man die static_info_tables Tabellen als Quelle für die Länderauswahl einsetzen kann. Das funktioniert erstmal nur im Frontend. Wenn man die Templates so anpasst, wie im Manual beschrieben, dann landet der 3-stellige Iso Code des Landes in der Datenbank.

Nun fände ich es auch schön, wenn die entsprechende Auswahl auch im Backend benutzt werden würde. Und so kann man es konfigurieren:

Als erstes muss das TCA der fe_users Tabelle entsprechend umkonfiguriert werden. Dazu in einer eigenen Extension entweder in ext_tables.php oder (besser) in Configuration/Tca/Overrides/fe_users.php folgenden Code einfügen. Damit wird die Länderauswahl mittels itemProcFunc erstellt. In diesem Fall kann man leider nicht einfach ‚foreign_table‘ verwenden, da als Key automatisch die uid verwendet wird.

$GLOBALS['TCA']['fe_users']['columns']['country']['config'] = [
    'type' => 'select',
    'renderType' => 'selectSingle',
    'itemsProcFunc' => 'My\Extension\UserFunc\TcaProcFunc->staticInfoTablesItems',
    'maxitems' => 1
];

Dann legt man die Klasse TcaProcFunc in Classes/UserFunc an mit folgendem Inhalt:

<?php
namespace My\Extension\UserFunc;
 
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
 
class TcaProcFunc
{
 
    /**
     * @param array $config
     * @return array
     */
    public function staticInfoTablesItems($config)
    {
        $key = 'isoCodeA3';
        $value = 'shortNameLocal';
        $sortbyField = 'isoCodeA3';
        $sorting = 'asc';
 
        $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Object\ObjectManager');
        $countryRepository = $objectManager->get('SJBR\StaticInfoTables\Domain\Repository\CountryRepository');
 
        $countries = $countryRepository->findAllOrderedBy($sortbyField, $sorting);
        $countryList = [];
        $countryList[] = ["", ""];
        foreach ($countries as $country) {
            /** @var $country \SJBR\StaticInfoTables\Domain\Model\Country */
            $countryList[] = [ObjectAccess::getProperty($country, $value), ObjectAccess::getProperty($country, $key)];
        }
        $config['items'] = $countryList;
        return $config;
    }
 
}

Der Code der Function ist aus dem ViewHelper GetCountriesFromStaticInfoTablesViewHelper aus der Extension femanager geklaut 🙂

Vorschau von Plugin-Einstellungen im Backend in TYPO3

Wenn man eingene Plugins implementiert und dabei mit Flexforms arbeitet, kann man bei einem eingesetzen Plugin im Backend nicht erkennen, welche Einstellungen im Flexform vorgenommen wurden. Es gibt in TYPO3 einen Hook, der es ermöglicht, sich in die Backend-Ansicht einzuklinken und diese Einstellungen dort auszugeben. Und so wird das implementiert:

Als erstes muss man den Hook registrieren in ext_localconf.php der eigenen Extension. Der Name der Klasse ist egal. Unter $pluginSignature gibt man den Namen des Plugin an, so wie man ihn registriert hat.

if (TYPO3_MODE === 'BE') {
    // Page module hook - show flexform settings in page module
    $extensionName = \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToUpperCamelCase($_EXTKEY);
    $pluginSignature = strtolower($extensionName) . '_pi';
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$pluginSignature][$_EXTKEY] =
        'My\Extension\Hook\CmsLayout->getExtensionSummary';
}

Dann erstellt man die entsprechende Klasse. In meinem Fall die Klasse CmsLayout im Ordner Classes/Hook. Diese Klasse muss die Funktion getExtensionSummary haben, die als Parameter ein Array übergeben bekommt. Die Funktion gibt einen String zurück, der dann im Backend als Plugin-Zusammenfassung ausgegeben wird.

Dabei ist zu beachten, dass der Hook für alle Plugins aufgerufen wird, d.h. man muss selbst rausfinden, ob man sich in seinem Plugin befindet oder nicht.

Bei mir hat sich mittlerweile dieser Klassenaufbau etabliert:

namespace My\Extension\Hook;
class CmsLayout
{
    public function getExtensionSummary(array $params) {
        $result = null;
        if (strpos($params['row']['list_type'], 'myextension_') !== FALSE) {
            $pluginName = str_replace('myextension_', '', $params['row']['list_type']);
            if ($params['row']['pi_flexform'] != '') {
                $this->flexformData = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($params['row']['pi_flexform']);
            }
            $methodName = 'bePreview' . ucfirst(GeneralUtility::underscoredToLowerCamelCase($pluginName));
            if (method_exists($this, $methodName)) {
                $result = $this->$methodName($params['row']);
            }
        }
        return $result;
    }
 
    protected function getFieldFromFlexform($key, $sheet = 'sDEF') {
        $flexform = $this->flexformData;
        if (isset($flexform['data'])) {
            $flexform = $flexform['data'];
            if (is_array($flexform) && is_array($flexform[$sheet]) && is_array($flexform[$sheet]['lDEF'])
                && is_array($flexform[$sheet]['lDEF'][$key]) && isset($flexform[$sheet]['lDEF'][$key]['vDEF'])
            ) {
                return $flexform[$sheet]['lDEF'][$key]['vDEF'];
            }
        }
 
        return NULL;
    }
 
    protected function bePreviewPi($row) {
        // whatever
        return '';
    }
}

In der Funktion getExtensionSummary werden die Flexform-Daten geparst, falls es sich um eines meiner Plugins handelt. Dann wird die Preview-Funktion aufgerufen, falls sie existiert. Damit spare ich mir if- oder switch-Abfragen.

Hinweis 8.5.: Ich habe mitbekommen, dass sich die Flexform-Definition in der TYPO3 Version 8 geändert hat. d.h. es müssten entsprechend Anpassungen an diesem Code vorgenommen werden.

Bug? Falsche Artikelauswahl auf der News-Seite

Ok, ich versuche mal mein Setup zu beschreiben.

Kategorien sehen ungefähr so aus:

  • Kategorie Eins
    • Subkategorie EinsEins
    • Subkategorie EinsZwei
  • Kategorie Zwe
    • Subkategorie ZweiEins
    • Subkategorie ZweiZwei
  • Kategorie Drei

Dann sind da natürlich News-Beiträge, die immer einer Hauptkategorie und (falls diese Unterkategorien hat) Unterkategorien zugewiesen sind. Pro Hauptkategorie gibt es jeweils eine Ausgabe-Seite, die in der Hauptspalte ein Plugin enthält, wo eingestellt ist, dass nur Beiträge einer Kategorie angezeigt werden. So weit passt alles.

Und auf einmal werden auf der Seite in Plugin, das eigentlich nur Beiträge der Kategorie „Kategorie Eins“ und Unterkategorien anzeigen soll, auch Beiträge der Kategorie Zwei angezeigt. Ich habe alles überprüft: Ist die Zuordnung im Artikel richtig gesetzt? Ist das Plugin auf der Seite richtig konfiguriert? Sieht alles gut aus. Hab schon angefangen an meinem Verstand zu zweifeln und an einen mysteriösen Bug in News geglaubt.

Lösung: Damit nur Artikel einer bestimmten Kategorie ausgegeben werden, muss im News-Plugin in der Kategorieauswahl „Show items with selected categories“ ausgewählt werden. Wenn man keine weiteren Einstellungen vornimmt, dann kann damit durch URL Parameter die Kategorieauswahl überschrieben werden. Wenn man RealURL verwendet, merkt man das nicht so schnell. Im RealURL Cache für diese Seite war nämlich ungefähr folgende URL eingetragen: seite/?tx_news_pi1[overwriteDemand][categories]=0 und die auch noch an erster Stelle. d.h. Wenn man die Seite aufruft, dann wird die sprechende URL so umgewandelt, dass die Kategorieauswahl aufgehoben wird.

Damit das nicht wieder passiert, kann man im News-Plugin das Überschreiben von Kategorien per URL deaktivieren: im Tab „Additional“ unter „Disable override demand“.

Auswahlgruppe „__Kein Umfluss__“ bei Bildausrichtung entfernen

Bei der Bildausrichtung gibt es in TYPO3 seit Ewigkeiten die Auswahlgruppe (optgroup) „Kein Umfluss“. Das blöde ist, dass alle neuen Items, die man über Tsconfig definiert, landen automatisch darin, egal wie die Zahlen sind. In den meisten Fällen wird der Kontext falsch sein. Daher hatte ich den Wunsch die Auswahlgruppe zu entfernen. Leider gibt es wohl schon länger einen Bug. Dabei wird das erste Element („Bild oben“) ebenfalls aus der Liste entfernt.

Ich habe eine andere Lösung gefunden – ich entferne die Optgroup in der ext_tables.php meiner Extension:

// remove __No wrap__ optgroup
unset($GLOBALS['TCA']['tt_content']['columns']['imageorient']['config']['items'][8]);

Installation TYPO3 8.5 auf Localhost

Da wollte ich doch endlich mal TYPO3 8.5 ausprobieren und hatte nach der Installation gleich zwei unschöne Fehlermeldungen.

Extension Liste

Die Extension-Liste wurde nicht geladen mit der Fehlermeldung: „Could not access remote resource https://repositories.typo3.org/mirrors.xml.gz“. Ich habe viele Beiträge gefunden, keiner richtig hilfreich. Bis auf einen auf Stackoverflow. Die Lösung in meinem Fall war:

  • Zentifikat von http://curl.haxx.se/docs/caextract.html herunterladen und z.B. unter E:\xampp\php\cacert.pem ablegen
  • in der php.ini nach curl.cainfo suchen und dort folgendes eintragen: curl.cainfo=E:\xampp\php\cacert.pem

Apache neu starten nicht vergessen.

Datenbank Encoding

Ich hatte die Fehlermeldung, dass das Encoding meiner Datenbank nicht richtig sei: MySQL database character set check failed Checking database character set failed, got key „latin1“ instead of „utf8“ or „utf8mb4“. Dabei könnte ich schwören, dass ich utf8 eingestellt hatte, egal…
Lösung: Datenbank komplett auf utf8 umstellen.
ALTER DATABASE [dbname] DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
Und dann müssten noch alle Tabellen umgestellt werden. Das geht entweder mit:
ALTER TABLE [tablename] CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci oder mit HeidiSQL über Rechtsklick auf Datenbankname > Wartung.

TYPO3 SQL Queries Debuggen

Wie man TYPO3 SQL Queries debuggen kann, ändert sich ja immer ein bisschen. In der Version 7.6. funktioniert das:

$parser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser');
$queryParts = $parser->parseQuery($query);
\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($queryParts);

Diesen Codeschnipsel in die Repository-Funktion vor $query->execute() einsetzen.