Ein Seiten-Teaser Backend-Modul mit TYPO3 und Extbase/Fluid

Ein Backend Modul in TYPO3 zu erstellen ist dank dem Extension Builder nicht schwer. Erweitert man jedoch etwas, wird die native Unterstützung etwas dünn.

Dieser Artikel setzt eine leere Extension „easy_teasers“, erstellt mit dem „Extension Builder“, voraus.

Der Usercase

Für Seiten-Teaser nutze ich die Seiteneigenschaften „title,abstract,media“.
Es wird also die Tabelle „pages“ bearbeitet um die Informationen zu pflegen.
Die Teaser lassen sich dann leicht mit dem Menü-Inhaltselement aufbereiten und sie werden in der Suche unterstützt.
Eine Übersicht über die gepflegten Teaser im Backend zu erhalten ist jedoch quasi unmöglich.
Das Backend-Modul soll eine Übersicht der Seitenteaser-Information herstellen und deren Bearbeitung unterstützen.
Zur Anordnung der Teaser wird Masonry verwendet.

easy_teaser_screen1

Screenshot des Teaser-Moduls


(Veggie Vektor Bildquelle)

Das Problem mit dem Cache

Auch wenn das Backend weitestgehen ungecached ausgespielt wird, so werden Extbase Klassen und Fluid Templates trotzdem gecached.

Während dem Ausbauen der Klassen und der Ansicht musste häufig nicht nur der komplette Cache gelöscht sondern auch das typo3temp-Verzeichnis geleert werden.

Das Problem mit masonry und requireJS

Masonry unterstützt requireJS. Das bedeutet im Umkehrschluss, dass es sich nicht auf „normalem“ Wege einbinden lässt, wenn requireJS aktiv ist … was im TYPO3 Backend der Fall ist.

Das Einbinden über

<f:be.container includeJsFiles="{0: '{f:uri.resource(path:\'JavaScript/masonry.pkg.min.js\')}'}">
...
</f:be.container>

als auch über ein normales Script-Tag,

<script src="../typo3conf/ext/easy_teasers/Resources/Public/JavaScript/masonry.pkgd.min.js"></script>

als auch über einen ViewHelper

$this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$baseUrl = '../' . \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('easy_teasers');
$this->pageRenderer->disableCompressJavascript();
$this->pageRenderer->addJsFile($baseUrl . 'Resources/Public/JavaScript/masonry.pkg.min.js');

Alles schlug fehl. Der Fehler war stets derselbe:
Uncaught Error: Mismatched anonymous define() module

Der Weg über requireJS funktionierte dann.

Die Ordnerstruktur

├── ext_emconf.php
├── ext_icon.gif
├── ext_localconf.php
├── ext_tables.php
├── Classes
│   ├── Controller
│   │   └── TeaserPageController.php
│   ├── Domain
│   │   ├── Model
│   │   │   └── TeaserPage.php
│   │   └── Repository
│   │       └── TeaserPageRepository.php
│   └── ViewHelpers
│       └── BackendLinkViewHelper.php
├── Configuration
│   └── TypoScript
│       ├── constants.txt
│       └── setup.txt
└── Resources
    ├── Private
    │   ├── Backend
    │   │   ├── Layouts
    │   │   │   └── Default.html
    │   │   ├── Partials
    │   │   │   └── TeaserPage
    │   │   │       └── Teaser.html
    │   │   └── Templates
    │   │       └── TeaserPage
    │   │           └── List.html
    │   └── Language
    │       ├── locallang_db.xlf
    └── Public
        ├── Icons
        │   └── easy_teasers.svg
        ├── Images
        │   └── noimage.svg
        ├── JavaScript
        │   ├── easy_teasers.js
        │   └── masonry.pkgd.min.js
        └── Stylesheet
            └── easy_teasers.css

Das Modul im Backend unter „Web“ verfügbar machen

ext_tables.php

<?php
if (TYPO3_MODE === 'BE') {
	/**
	 * Registers a Backend Module
	 */
	\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
		'Marwein.' . $_EXTKEY,
		'web',	 // Make module a submodule of 'web'
		'teasers',	// Submodule key
		'',						// Position
		array(
			'TeaserPage' => 'list, edit',
		),
		array(
			'access' => 'user,group',
			'icon'   => 'EXT:' . $_EXTKEY . '/ext_icon.gif',
			'labels' => 'LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/locallang_teasers.xlf',
		)
	);
}
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile($_EXTKEY, 'Configuration/TypoScript', 'Easy Teasers');

Der Controller

Der Controller wird die Listenansicht im Backend steuern.

Classes/Controller/TeaserPageController.php

<?php
namespace Marwein\EasyTeasers\Controller;

class TeaserPageController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{

    /**
     * @var \Marwein\EasyTeasers\Domain\Repository\TeaserPageRepository
     * @inject
     */
    protected $teaserPageRepository = NULL;
    
    public function listAction()
    {
    	$id = $_GET['id'] ? intval($_GET['id']) : 0;
        $teaserPages = $this->teaserPageRepository->findByUidRecursive($id);
        $this->view->assign('teaserPages', $teaserPages);
    }

}

Das Model

Das Model besteht aus den 3 Seiteneigenschafts-Feldern. Das Model wird nur für die Anzeige der Teaser im Backend verwendet. Daher sind Setter optional und ich habe sie weggelassen.

Classes/Domain/Model/TeaserPage.php

<?php
namespace Marwein\EasyTeasers\Domain\Model;

class TeaserPage extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    /**
     * @var string
     */
    protected $title = '';
    
    /**
     * @var string
     */
    protected $abstract = '';
    
    /**
     * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference
     */
    protected $media = null;
    
    public function getTitle()
    {
        return $this->title;
    }
    
    public function getAbstract()
    {
        return $this->abstract;
    }
    
    public function getMedia()
    {
        return $this->media;
    }
}

Das Repository

Mit dem Repository werden die Teaser-Daten rekursiv aus dem Seitenbaum ausgelesen.
Anmerkung: Eine besondere Sortierung oder hierarchische Ansicht ist mit diesem Ansatz nicht möglich.

Classes/Domain/Repository/TeaserPageRepository.php

<?php
namespace Marwein\EasyTeasers\Domain\Repository;

class TeaserPageRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
    public function findByUidRecursive($pid) {
    	$queryGenerator = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance( 'TYPO3\\CMS\\Core\\Database\\QueryGenerator' );
    	// get 10 levels of the tree recursively starting at $pid
    	$pidList = $queryGenerator->getTreeList($pid, 10, 0, 1);
    	$pids = explode(',',$pidList);
    	
    	$query = $this->createQuery();
    	$query->matching(
    		$query->logicalAnd(
    			$query->in('uid', $pids),
    			// Exclude the non-page-type pages
    			$query->logicalNot($query->in('doktype', array('254','255','199')))
    		)
    	);
    	// load from no matter where
    	$query->getQuerySettings()->setRespectStoragePage(FALSE);
    	return $query->execute();
    }
}

Die Anzeige konfigurieren

Das nötige TypoScript

Die Extension hat selbst keine Tabellen. Das Model muss auf die „pages“ Tabelle verweisen bzw. „ge-mapped“ werden.

Configuration/TypoScript/constants.txt

module.tx_easyteasers_teasers {
	view {
		# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template root (BE)
		templateRootPath = EXT:easy_teasers/Resources/Private/Backend/Templates/
		# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template partials (BE)
		partialRootPath = EXT:easy_teasers/Resources/Private/Backend/Partials/
		# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template layouts (BE)
		layoutRootPath = EXT:easy_teasers/Resources/Private/Backend/Layouts/
	}
	persistence {
		# cat=module.tx_easyteasers_teasers//a; type=string; label=Default storage PID
		storagePid =
	}
}

Configuration/TypoScript/setup.txt

module.tx_easyteasers_web_easyteasersteasers {
	persistence {
		storagePid = 0
		classes {
			Marwein\EasyTeasers\Domain\Model\TeaserPage {
				mapping {
					tableName = pages
				}
			}
		}
	}
	view {
		templateRootPaths.0 = {$module.tx_easyteasers_teasers.view.templateRootPath}
		partialRootPaths.0 = {$module.tx_easyteasers_teasers.view.partialRootPath}
		layoutRootPaths.0 = {$module.tx_easyteasers_teasers.view.layoutRootPath}
	}
}

Ein ViewHelper für Links

Für die Erzeugung von Links im Backend gibt es in Fluid keine Unterstützung … daher der eigene Viewhelper.

Classes/Domain/ViewHelper/BackendLinkViewHelper.php

<?php
namespace Marwein\EasyTeasers\ViewHelpers;

use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

class BackendLinkViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper {
	protected $tagName = 'a';
	
	/**
	 * @param string $parameters
	 * @param string $returnUrl
	 */
	public function render($parameters, $returnUrl = '') {	
		$uri = \TYPO3\CMS\Backend\Utility\BackendUtility::getModuleUrl('record_edit').'&'.$parameters;
		if (!empty($returnUrl)) {
			$uri .= '&returnUrl='.rawurlencode($returnUrl);
		} else {
			$uri .= '&returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'));
		}
		$this->tag->addAttribute('href', $uri);
		$this->tag->setContent($this->renderChildren());
		$this->tag->forceClosingTag(TRUE);
		return $this->tag->render();
	}
	
}

Die Anzeige beschreiben

Das Stylesheet

Für das Masonry-Grid werden die nötigsten Einstellungen gemacht.

Resources/Public/Stylesheet/easy_teasers.css


.typo3-docbody {
	background-color: #ddd;
}
.grid-item { 
	width: 300px; height: auto;
	margin-bottom: 10px;
	box-shadow: 0 0 20px #666;
	background-color: #fff;
}
.grid-item b {
	font-size: 1.2em;
	display:block;
	padding:5px;
	background: #f80;
	color:#fff;
}
.grid-item p {
	padding:5px;
}
.grid-item img {
	positon:relative;
	bottom:0px;
	width:100%;
	height:auto;
}

Das JavaScript

Masonry lässt sich im Backend nur via requireJS einbinden.

Resources/Public/JavaScript/easy_teasers.js


requirejs( [
  '../typo3conf/ext/easy_teasers/Resources/Public/JavaScript/masonry.pkgd.min.js',
], function( Masonry ) {
  new Masonry( '.tx_easyteasers', {columnWidth: 300, gutter: 10});
});

Layout, Template und Partial

Stylesheet und JavaScript werden über den Be.Container ViewHelper geladen.

Resources/Private/Backend/Layouts/Default.html

{namespace m=Marwein\EasyTeasers\ViewHelpers}
<f:be.container 
  includeCssFiles="{0: '{f:uri.resource(path:\'Stylesheet/easy_teasers.css\')}'}" 
  includeJsFiles="{0: '{f:uri.resource(path:\'JavaScript/easy_teasers.js\')}'}">
	<div class="typo3-fullDoc">
		<div id="typo3-docbody">
			<div id="typo3-inner-docbody">
				<f:flashMessages />
				<f:render section="content" />
			</div>
		</div>
	</div>
</f:be.container>

Das Template für die Listenansicht arbeiten mit einem Partial für den Teaser selbst.

Resources/Private/Backend/Templates/TeaserPage/List.html

{namespace m=Marwein\EasyTeasers\ViewHelpers}

<f:layout name="Default" />

<f:section name="content">
<h1>Page teasers</h1>

<div class="tx_easyteasers" >
	<f:for each="{teaserPages}" as="teaserPage">
		<f:render partial="TeaserPage/Teaser" arguments="{teaserPage:teaserPage}" />
	</f:for>
</div>

</f:section>

Das Partial wiederum sorgt für die Bild-Anzeige und den Bearbeitungs-Link über den ViewHelper.
Der Link sorgt dafür, dass nur 3 Felder im Formular auftauchen.
Der Teaser Text („abstract“) wird beschnitten.

Resources/Private/Backend/Partials/TeaserPage/Teaser.html

{namespace m=Marwein\EasyTeasers\ViewHelpers}

<div class="grid-item">
	<m:backendLink parameters="edit[pages][{teaserPage.uid}]=edit&columnsOnly=title,_CONTROL_,_CLIPBOARD_,abstract,media&route=%2Frecord%2Fedit">
	<b>{teaserPage.title}</b>
	<f:if condition="{teaserPage.media.originalResource.publicUrl}">
		<f:then>
			<f:image src="{teaserPage.media.originalResource.publicUrl}" width="300c" height="150" />
		</f:then>
		<f:else>
			<f:image src="typo3conf/ext/easy_teasers/Resources/Public/Images/noimage.svg" width="300" height="150" />
		</f:else>
	</f:if>
	<p><f:format.crop maxCharacters="100" append=" [...]">{teaserPage.abstract}</f:format.crop>
	</p>
	</m:backendLink>
</div>

Damit ist die Extension einsatzfähig und hilft bei der übersichtlichen Bearbeitung von Seiten-Teasern, die über die Seiteneigenschaften hergestellt werden.

Durch eine Erweiterung des „Menü“-Elements lassen sich diese Teaser dann einfach aufbereiten und gewohnt flexibel konfigurieren.

In einer Ausbaustufe könnten „pages“ um weitere Teaser-Felder erweitert werden, wie auch unter ‚Die Extbase Extension „news“ um ein Feld erweitern‚ für „News“ beschrieben. Felder aus der Praxis sind bspw „teaser link“, „teaser link text“, „teaser title“ oder „teaser_layout“

One comment

Add Comment

Required fields are marked *. Your email address will not be published.