Typo3 Backend: im listview ein Thumbnail anzeigen

19. November 2015

Ich habe ein Model mit einem Feld “image”, wobei es sich einfach um ein VARCHAR handelt, wo der Dateiname eines Bildes drin steht. Die Extension heißt ophi_insta und das Model heißt entry. Beim Speichern der Bilder lege ich diese in uploads/tx_ophiinsta/ ab und speichere nur den Bildnamen (ohne Pfad!) in die Spalte “image”. Nun will ich, dass unter dem Namen des Models das Bild als Thumbnail angezeigt wird und spannenderweise geht das in Typo3 sehr einfach: im ext_tables, wo man $TCA für das model definiert, setzt man einfach ‘thumbnail’ => ‘image’ . Und lustigerweise schafft Typo3 es tatsächlich 2 + 2 zusammenzuzählen und das Bild verfügbar zu machen!

  1. $TCA['tx_ophiinsta_domain_model_entry'] = array(
  2. 'ctrl' => array(
  3. ....
  4. 'thumbnail' => 'image',
  5. ....
  6. ),
  7. );

ERSTAUNLICH!

Typo3 und extbase: Frontendlink in einem Backend Hook generieren

17. November 2015

Beim Speichern eines Models im Backend brauchte ich einen Hook, um, falls hidden auf 0 gesetzt wird, eine Benachrichtigungsmail zu schicken. Dies war die größte Quälerei in Typo3 seit langem, und insbesondere das – wie man meinen sollte – simple Erstellen eines Frontend Links bereitete mir massive Probleme. Ich sage nicht, dass meine Lösung die Beste ist, aber ich habe es geschafft und darum halte ich die gesamte Prozedur hier fest.

Legen wir kurz die Basisdaten fest. Meine Extension heißt ophi_inserat und lässt User im Frontend schlichte Inserate aufgeben. Das Model für ein Inserat heißt “Ad” und wird in der Datenbank in tx_ophiinserat_domain_model_ad gespeichert. Im Backend kann man solche Inserate dann bearbeiten und beim erstmaligen setzen von hidden auf 0 soll eben eine Benachrichtigungsmail an den Ersteller des Inserats (die E-Mail Adresse wird in $email gespeichert) mit einem Link zum Bearbeiten des Inserats geschickt werden.

Schritt 1: erstellen eines Hooks um eine Aktion nach dem Speichern durchzuführen

In der ext_localconf.php meiner Extension füge ich folgende Zeile hinzu:

  1. $GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = 'EXT:'.$_EXTKEY.'/Classes/Hook/NotificationHook.php:Ophi\OphiInserat\Hook\NotificationHook';

Möglicherweise gibt es bessere Stellen, um den Hook einzufügen, aber ich fand beim Recherchieren leider nur “processDatamap”. Nun habe ich meine Datei NotificationHook.php angelegt, und zwar unter ophi_inserat/Classes/Hook/NotificationHook.php und diese braucht die Funktion processDatamap_postProcessFieldArray. Da ich nur beim Bearbeiten meiner Domain eine Aktion durchführen will, frage ich gleich ganz am Anfang ab, ob es sich überhaupt um diesen table handelt. Zunächst sieht die Datei also so aus:

  1. <?php
  2. namespace Ophi\OphiInserat\Hook;
  3. class NotificationHook {
  4. public function processDatamap_postProcessFieldArray($status, $table, $id, &$fieldArray, &$reference){
  5. if ($table == 'tx_ophiinserat_domain_model_ad') {
  6. //insert code here
  7. }
  8. }
  9. }

Nun. Aus irgendeinem Grund sind in $fieldArray nur manche Felder zugänglich, aber da hidden praktischerweise dabei ist, habe ich mich nicht näher damit befasst, wie man auf die anderen zugreifen kann. Jetzt wird's allerdings knifflig. Wir befinden uns im Backend und damit steht quasi nichts zur Verfügung, was irgendwie nützlich oder praktisch wäre. Zunächst habe ich mir mal mein Repository geholt und anhand von $id (welche der ID des bearbeiteten Items entspricht) mein Inserat $ad gegriffen.

  1. if ($table == 'tx_ophiinserat_domain_model_ad') {
  2. if($fieldArray['hidden'] == 0){
  3. //fetch some classes
  4. $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('\TYPO3\CMS\Extbase\Object\ObjectManager');
  5. $persistenceManager = $objectManager->get('\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager');
  6. $adRepository = $objectManager->get('\Ophi\OphiInserat\Domain\Repository\AdRepository');
  7. //fetch the ad in question
  8. $ad = $adRepository->findOneByUid($id);
  9. }
  10. }

Damit habe ich nun mein $ad zur Verfügung und kann dank repository und persistenceManager auch Daten ändern. Damit nicht bei jedem Editieren wieder der Link geschickt wird, habe ich eine Spalte $emailSent definiert, die true oder false sein kann und die ich nach dem ersten Senden der E-Mail dann auf true setze. Da meine Seite zweisprachig ist, brauche ich zunächst die Texte für meine E-Mail in der richtigen Sprache. Hier fängt's an, so richtig fies zu werden.

Schritt 2: Texte in der richtigen Sprache

Ich habe extra bei stackoverflow nachgefragt und es gibt tatsächlich eine Möglichkeit, auf die gesamte geparste Übersetzungsdatei zuzugreifen. Hierfür braucht man die sogenannte LanduageFactory und über den LanguageKey ("en", "de", etc.) und den Pfad zur Übersetzungsdatei bekommt man ein Array, das in "default" alle Defaultwerte enthält und in "en" oder "de" oder welchen languageKey auch immer man angegeben hat die übersetzten Werte hat. Ich habe mir dann noch eine kleine Funktion geschrieben, um aus diesem Array den übersetzten bzw. falls dieser noch nicht existiert den default-Wert auslesen zu können:

  1. private function getTranslation($data, $languageKey, $target){
  2. if(isset($data[$languageKey][$target])){
  3. return $data[$languageKey][$target][0]['target'];
  4. } else {
  5. return $data['default'][$target][0]['target'];
  6. }
  7. }

Diese Lösung gefällt mir recht gut, hier der Ausschnitt aus der Hook Datei, wo ich mir das Array hole und die Funktion aufrufe:

  1. $languageFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('\TYPO3\CMS\Core\Localization\LocalizationFactory');
  2. $langId = $ad->getSysLanguageUid();
  3. $languageKey = $langId == 1 ? 'en' : 'de';
  4. $data = $languageFactory->getParsedData('EXT:ophi_inserat/Resources/Private/Language/locallang.xlf', $languageKey);
  5. $text1 = $this->getTranslation($data, $languageKey, 'tx_ophiinserat_domain_model_ad.email.text1');

Schritt 3: den Link generieren

Dieser Schritt hat mich bei weitem am meisten Zeit und Nerven gekostet. Offensichtlich gibt es im Typo3 Backend Kontext keinerlei Möglichkeit, einen Frontend Link zu generieren. Aus lauter Verzweiflung habe ich mir also folgendes überlegt: ich erstelle ein Plugin, das einfach nur den Link zurückgibt. Ich behandle den Aufruf wie einen Ajax Aufruf, sodass hier keine Parameter oder dergleichen mitgegeben werden, und hole mir mit file_get_contents die so generierte URL. Das klingt wie ein furchtbarer Umweg, aber Typo3 hat mir keine Wahl gelassen und irgendwie, glücklicherweise, funktioniert es so wie ich mir gedacht habe. Zunächst habe ich also in meiner ext_localconf.php ein neues Plugin hinzugefügt:

  1. \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
  2. 'Ophi.' . $_EXTKEY,
  3. 'AdFormLink',
  4. array(
  5. 'Ad' => 'generateLink',
  6. ),
  7. // non-cacheable actions
  8. array(
  9. 'Ad' => 'generateLink',
  10. )
  11. );

Dann habe ich die setup.txt konfiguriert, um das ganze als ajax Aufruf verfügbar zu machen:

  1. ajax_frontend_url < page
  2. ajax_frontend_url {
  3. typeNum = 1447752540
  4. config {
  5. disableAllHeaderCode = 1
  6. xhtml_cleaning = 0
  7. admPanel = 0
  8. }
  9. 10 = USER
  10. 10 {
  11. extensionName = OphiInserat
  12. pluginName = AdFormLink
  13. vendorName = Ophi
  14. userFunc = tx_extbase_core_bootstrap->run
  15. }
  16. }

Und damit ist die URL /index.php?type=1447752540&id=123 verfügbar, wobei 123 in dem Fall eine beliebige Typo3 Seite wäre. Im Controller in der generateLinkAction mache ich dann einfach folgendes:

  1. public function generateLinkAction(){
  2. $link = $this->uriBuilder
  3. ->setCreateAbsoluteUri(true)
  4. ->setTargetPageUid(123)
  5. ->setArguments( array('tx_ophiinserat_adform' => array('code' => '12354567') ) )
  6. ->build();
  7. die($link);
  8. }

Mehr macht das Ding nicht. Den Code kann man dann noch als Parameter mitgeben, der sollte ja für jedes Inserat anders sein, aber das war dann kein großes Ding mehr, das spar ich mir einfach mal. So, und jetzt geht's um die Wurst, ich muss im Backend ja irgendwie diese URL abfragen und das stellte sich als erstaunlich kompliziert heraus. Ich zeige hier einfach mal die notwendigen Funktionen für diese eigentlich einfache Aufgabe:

  1. /**
  2. * I have no idea why $pageId is necessary
  3. *
  4. * @param $pageId
  5. * @return string
  6. */
  7. private function getSiteUrl($pageId){
  8. $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('\TYPO3\CMS\Extbase\Object\ObjectManager');
  9. $beFunc = $objectManager->get('\TYPO3\CMS\Backend\Utility\BackendUtility');
  10. $libDiv = $objectManager->get('\TYPO3\CMS\Core\Utility\GeneralUtility');
  11. $utilityHttp = $objectManager->get('\TYPO3\CMS\Core\Utility\HttpUtility');
  12. $domain = $beFunc::firstDomainRecord($beFunc::BEgetRootLine(60));
  13. $pageRecord = $beFunc::getRecord('pages', 60);
  14. $scheme = is_array($pageRecord) && isset($pageRecord['url_scheme']) && $pageRecord['url_scheme'] == $utilityHttp::SCHEME_HTTPS ? 'https' : 'http';
  15. $siteUrl = $domain ? $scheme . '://' . $domain . '/' : $libDiv::getIndpEnv('TYPO3_SITE_URL');
  16. return $siteUrl;
  17. }
  18. /**
  19. * generate the link to the adForm in a convoluted, weird way because there seems to be no other option
  20. * @param $code
  21. * @return string
  22. */
  23. private function generateLink($code){
  24. $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('\TYPO3\CMS\Extbase\Object\ObjectManager');
  25. $libDiv = $objectManager->get('\TYPO3\CMS\Core\Utility\GeneralUtility');
  26. //fetch the base url first
  27. $siteUrl = $this->getSiteUrl($this->ajaxPageId);
  28. //now set the ajax url
  29. $url = $siteUrl.'index.php?type=1447752540&id='.$this->ajaxPageId;
  30. //set headers because otherwise it doesn't work
  31. $headers = array(
  32. 'Cookie: fe_typo_user=' . $_COOKIE['fe_typo_user']
  33. );
  34. //as far as I understand it, this is the equivalent of file_get_contents
  35. $result = $libDiv::getURL($url, false, $headers);
  36. //with luck $result should contain a valid link now.
  37. return $result;
  38. }

Und so liefert generateLink tatsächlich den im Plugin generierten Link zurück. Und damit sind wir fertig. Die komplette NotificationHook.php gibt es hier: NotificationHook

Google Maps Api: Karte wird nur grau dargestellt, verzerrte Buttons

11. November 2015

Nach dem einbinden von Google maps mit der API (v3) wurde die Map teilweise nur in grau dargestellt und die Buttons waren merkwürdig verzerrt:
karte
Stellt sich raus, der Übeltäter war Bootstrap. Das setzt img nämlich auf max-width:100% oder sowas. Die Lösung:

  1. #map-canvas img {
  2. max-width: none !important;
  3. }

Und damit gehts dann auf einmal.

Extbase Validator – Fehlermeldungen werden zweimal angezeigt

10. November 2015

Ich habe einen Validator für ein Kontaktformular geschrieben und sobald ein Feld fehlerhaft war, wurden die Fehlermeldungen doppelt ausgegeben. Der Controller hat die showAction zur Darstellung des Formulars:

  1. /**
  2. * @param Contactform $contactform
  3. */
  4. public function showAction( $contactform = null) {
  5. $this->view->assign('contactform', $contactform);
  6. $this->view->assign('timestamp', time());
  7. }

mit folgendem Template:

  1. <f:form action="create" name="contactform" objectname="contactform" object="{contactform}">
  2. <f:render partial="FormErrors" arguments="{object:contactform}"/>
  3. <f:form.textfield id="name" property="name" name="name" required="true"/>
  4. <f:form.textfield id="email" property="email" name="email" required="true"/>
  5. ....
  6. </f:form>

Dieses Formular zeigt auf die createAction, die im Controller folgendermaßen aussieht:

  1. /**
  2. * @param Contactform $contactform
  3. * @validate $contactform \Package\Extension\Domain\Validator\ContactformValidator
  4. */
  5. public function createAction(Contactform $contactform){
  6. $contactform->setPid($GLOBALS['TSFE']->id); //set pid to current page ID
  7. $this->contactformRepository->add($contactform);
  8. //redirect to thank you page...
  9. }

Der zugehörige ContactformValidator sieht so aus:

  1. <?php
  2. namespace Package\Extension\Domain\Validator;
  3. use Package\Extension\Domain\Model\Contactform;
  4. class ContactformValidator extends \TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator {
  5. public function isValid($contactform) {
  6. $isValid = true;
  7. if(!$contactform->getName()){
  8. $this->addError(
  9. $this->translateErrorMessage(
  10. 'tx_extension_domain_model_contactform.errors.required',
  11. 'extension'
  12. ), 111);
  13. $isValid = false;
  14. }
  15. if(!$contactform->getEmail()){
  16. $this->addError(
  17. $this->translateErrorMessage(
  18. 'tx_extension_domain_model_contactform.errors.required',
  19. 'extension'
  20. ), 111);
  21. $isValid = false;
  22. }
  23. return $isValid;
  24. }
  25. }

Das funktioniert soweit auch, aber jeder Fehler wird doppelt angezeigt. Ich habe lange gegoogelt (was wie immer bei Typo3 Problemen ungefähr so lustig wie eine Wurzelbehandlung war) und bin schließlich auf diesen Link gestoßen. Dort erklärt der Verfasser, dass Validatoren, die DOMAINNAMEValidator heißen, automatisch die Domain validieren. Darum war meine @validator Annotation zu viel. Ich habe sie entfernt und damit war das Problem gelöst.

Im Typo3 Backend mit Extbase ein zusätzliches Feld hinzufügen

05. November 2015

Es ist ein Wunder geschehen! Ich habe ein Typo3 Problem gegoogelt und eine Antwort gefunden, die sogar funktioniert! Ich brauchte ein zusätzliches Feld in der tt_content, um jedem Contentelement eine einzigartige ID geben zu können. Also habe ich zuerst in der Datenbank der Tabelle tt_content ein neues Feld namens “tx_kurs_id” hinzugefügt. Anschließend habe ich in der ext_tables.php meiner Extension noch folgendes hinzugefügt:

  1. $tempColumns = Array (
  2. "tx_kurs_id" => Array (
  3. "exclude" => 1,
  4. "label" => 'LLL:EXT:pgk_kurs/Resources/Private/Language/locallang_db.xml:tx_kurs_id.kursid',
  5. "config" => Array (
  6. 'type' => 'input',
  7. 'size' => 30,
  8. 'eval' => 'trim'
  9. )
  10. ),
  11. );
  12. t3lib_div::loadTCA("tt_content");
  13. t3lib_extMgm::addTCAcolumns("tt_content",$tempColumns,1);
  14. t3lib_extMgm::addToAllTCAtypes('tt_content','tx_kurs_id','','after:section_frame');

Interessant ist hier vor allem die letzte Zeile. Parameter 1 gibt die Typo3 Tabelle an, der ein Feld hinzugefügt werden soll. Der zweite Parameter ist der Name des Feldes (oben in $tempColumns definiert). Der dritte Parameter gibt an, bei welchen Inhaltselementen dieses Feld angezeigt werden soll (hatte Anfangs nur “bullets” stehen, wenn man es leer lässt gilt das Feld anscheinend für überall). Und der vierte Parameter gibt an, an welcher Stelle das Feld angezeigt werden soll (after/before). Um den Namen des Feldes rauszufinden, das man hierfür braucht, habe ich einfach im Quelltext nach der Bezeichnung des entsprechenden Textfeldes gesucht.