28. August 2013

Verwendung von Custom Views in Flow

Aufgabe: Ausgabe von Daten in verschiedenen Views (Fluid Template / JSON) unter Verwendung der selben Action-Methode.

Beispiel: Nehmen wir einmal an, wir haben eine Liste von Artikeln. Jeder Artikel beinhaltet neben Titel und Beschreibung auch ein Bild, welches als Thumbnail in einer bestimmten Größe ausgegeben werden soll. Die Darstellung soll sowohl in HTML (Fluid TemplateView) als auch in JSON (Flow JsonView) erfolgen.

Für die Darstellung des Fluid TemplateView geht man wie gewohnt vor. Innerhalb der Action-Methode werden Variablen definiert und an das Template weitergegeben. Im Template erfolgt dann die Formatierung mittels HTML. Das Rendering der Thumbnails in der passenden Größe kann über den ImageViewHelper des TYPO3.Media Package erreicht werden.

Für die Darstellung des JSON gibt es zwei Herangehensweisen: Entweder der JsonView wird innerhalb der Action-Methode konfiguriert (Flow JsonView) oder man verwendet einen CustomView und definiert dort wie das JSON aussehen soll. 

Der Nachteil bei der Verwendung des Flow JsonView ist, dass View-spezifische Aufgaben im Controller verbleiben und dieser dadurch schnell unübersichtlich wird. 
In unserem Beispiel werden z.B. Thumbnails mit einer bestimmten Größe aus den im Artikel hinterlegten Bildern erstellt und ihre Url an den JsonView übergeben. Dies ist eigentlich eine Aufgabe, die vom View erledigt werden sollte.

Die Action-Methoden sowie die beiden Views unter Verwendung des Flow JsonView sehen folgendermaßen aus (siehe dazu auch den Abschnitt zu Json View in der Flow Dokumentation):

ArticleController unter Verwendung des JsonView

namespace Networkteam\Blogexample\Controller;
 
class ArticleController extends \TYPO3\Flow\Mvc\Controller\ActionController {
 
	/**
	 * @Flow\Inject
	 * @var \Networkteam\Blogexample\Domain\Repository\ArticleRepository
	 */
	protected $articleRepository;
 
	/**
	 * @var \TYPO3\Flow\Resource\Publishing\ResourcePublisher
	 * @Flow\Inject
	 */
	protected $resourcePublisher;
 
	/**
	 * @var array
	 */
	protected $viewFormatToObjectNameMap = array('json' => 'TYPO3\Flow\Mvc\View\JsonView');
 
	/**
	 * list articles for json and html
	 */
	public function indexAction() {
		$format = $this->getControllerContext()->getRequest()->getFormat();
		$articles = $this->articleRepository->findAll();
 
		switch ($format) {
			case 'json':
				// set urls for image thumbnails of each article
				foreach($articles as $article) {
					$imgUrl = '';
					$imgMaxWidth = $imgMaxHeight = 150;
					$articleImage = $article->getImage();
 
					// scale image to given maxWith and maxHeight and resulting ratio
					if ($articleImage) {
						$imgWidth = $articleImage->getWidth() > $imgMaxWidth ? $imgMaxWidth : $articleImage->getWidth();
						$imgHeight = $imgWidth * ($imgMaxHeight / $imgMaxWidth);
						$imgUrl = $this->resourcePublisher->getPersistentResourceWebUri($articleImage->getThumbnail($imgWidth, $imgHeight, $articleImage::RATIOMODE_OUTBOUND)->getResource());
					}
 
					$customArticles[] = array(
						'title' => $article->getTitle(),
						'date' => $article->getDate(),
						'author' => $article->getAuthor(),
						'content' => $article->getContent(),
						'image' => $articleImage,
						'imgUrl' => $imgUrl
					);
				}
 
				// set vars for use in json view
				$this->view->assignMultiple(array(
					'changed' => FALSE,
					'timestamp' => time(),
					'articles' => $customArticles
				));
 
				// set variables to render. By default only the variable 'value' will be rendered
				$this->view->setVariablesToRender(array('changed', 'timestamp', 'articles'));
 
				// configure the json view
				$this->view->setConfiguration(array(
					'articles' => array(
						'_descendAll' => array(
							'_exclude' => array('image')
						)
					)
				));
				break;
 
			case 'html':
				// configure the html view here
				$this->view->assign('articles', $articles);
				break;
		}
	}
}

Resources/Private/Templates/Article/Index.html

{namespace m=TYPO3\Media\ViewHelpers}
 
<f:layout name="Default" />
 
<f:section name="Title">Index view of Article controller</f:section>
 
<f:section name="Content">
	<h1>List of articles</h1>
	<p>Some data set by the controller:</p>
	<ul>
		<f:for each="{articles}" as="article">
			<li>
				<dl>
					<dt>Title:</dt>
					<dd>{article.title}</dd>
 
					<dt>Date:</dt>
					<dd><f:format.date format="d.m.Y - H:i:s">{article.date}</f:format.date></dd>
 
					<dt>Author:</dt>
					<dd>{article.author}</dd>
 
					<dt>Content:</dt>
					<dd>{article.content}</dd>
 
					<f:if condition="{article.image}">
						<dt>Image:</dt>
						<dd><m:image image="{article.image}" maximumWidth="150" alt="{article.image.title}" /></dd>
					</f:if>
				</dl>
			</li>
		</f:for>
	</ul>
</f:section>

Der Custom View

Man sieht sehr deutlich, wie viel Code bereits in der Action-Methode steckt. Schöner geht es mit einem CustomView. Ähnlich wie beim Fluid TemplateView und JsonView können Packages eigene Views mitbringen. Der Action-Controller kann über die Instanzvariablen $defaultViewObjectName und $viewFormatToObjectNameMap so konfiguriert werden, dass er für bestimmte Formate eigene Views verwendet (Man sehe sich hierzu die Klasse \TYPO3\Flow\Mvc\Controller\ActionController genauer an).

Die eigenen Views werden im Ordner "View/ControllerName/" des Application Package abgelegt (siehe auch TYPO3 Flow Ordner Struktur).

Der Name des CustomView besteht immer aus dem Namen der Action und dem MIME-MediaType. In unserem Beispiel sieht das folgendermaßen aus:
Der ArticleController beinhaltet die customAction() Methode. Der MIME Type soll JSON sein. Der resultierende Klassennamen der CustomView ist damit CustomJson.

Soll das Ausgabeformat reiner Text sein (MIME Type TXT), müsste die View-Klasse CustomTxt heißen. Grundlage für die Namenskonvention ist der MIME MediaType. Die unterstützen Formate können in der Klasse\TYPO3\Flow\Utility\MediaTypes nachgelesen werden.

Der Vorteil bei der Verwendung des CustomView ist, dass die Action-Methode sehr sauber und übersichtlich bleibt. Alle View-spezifischen Aufgaben verbleiben im jeweiligen View.

Der Aufbau eines CustomView gestaltet sich folgendermaßen: Die View Klasse leitet von\TYPO3\Flow\Mvc\View\AbstractView ab welche wiederum das \TYPO3\Flow\Mvc\View\ViewInterface implementiert. Daher muss nur die render() Methode für den CustomView implementiert werden.

Innerhalb der render() Methode kann nun die JSON-Ausgabe als String zusammengebaut und zurückgegeben werden. Dabei stehen beispielsweise in $this->variables alle Variablen zur Verfügung, welche in der Action-Methode mit $this->view->assign() zugewiesen wurden. Des Weiteren kann auf den ControllerContext zugegriffen werden, über welchen unter anderem der Content-Type Header gesetzt werden kann.

Ebenfalls erfolgt die Generierung der Thumbnail-Url innerhalb des Views (siehe Methode articleToArray()).

Somit gestaltet sich die Action-Methode sehr sauber und aufgeräumt und die Formatierung von HTML und JSON kann einfach in den jeweiligen Views erfolgen.

ArticleController unter Verwendung des CustomView

namespace Networkteam\Blogexample\Controller;
 
class ArticleController extends \TYPO3\Flow\Mvc\Controller\ActionController {
 
	/**
	 * @Flow\Inject
	 * @var \Networkteam\Blogexample\Domain\Repository\ArticleRepository
	 */
	protected $articleRepository;
 
	/**
	 * list articles
	 */
	public function customAction() {
		$articles = $this->articleRepository->findAll();
		$this->view->assign('articles', $articles);
	}
}

View/Article/CustomJson.php

namespace Networkteam\Blogexample\View\Article;
 
class CustomJson extends \TYPO3\Flow\Mvc\View\AbstractView {
 
	/**
	 * @var \TYPO3\Flow\Resource\Publishing\ResourcePublisher
	 * @Flow\Inject
	 */
	protected $resourcePublisher;
 
	/**
	 * @return string
	 */
	public function render() {
		$data = array(
			'changed' => FALSE,
			'timestamp' => time(),
			'articles' => array()
		);
 
		# set Content-Type
		$this->controllerContext->getResponse()->setHeader('Content-Type', 'application/json');
 
		# get variables form controller action
		$articles = $this->variables['articles'];
 
		foreach($articles as $article) {
			$data['articles'][] = $this->articleToArray($article, 150, 150);
		}
 
		return json_encode($data);
	}
 
	/**
	 * @param \Networkteam\Blogexample\Domain\Model\Article $article
	 * @param integer $imgMaxWidth
	 * @param integer $imgMaxHeight
	 * return array
	 */
	protected function articleToArray(\Networkteam\Blogexample\Domain\Model\Article $article,  $imgMaxWidth = NULL, $imgMaxHeight = NULL) {
		$imgUrl = '';
		$articleImage = $article->getImage();
 
		// scale image to given maxWith and maxHeight and resulting ratio
		if ($articleImage) {
			$imgWidth = $articleImage->getWidth() > $imgMaxWidth ? $imgMaxWidth : $articleImage->getWidth();
			$imgHeight = $imgWidth * ($imgMaxHeight / $imgMaxWidth);
			$imgUrl = $this->resourcePublisher->getPersistentResourceWebUri($articleImage->getThumbnail($imgWidth, $imgHeight, $articleImage::RATIOMODE_OUTBOUND)->getResource());
		}
 
		return array(
			'title' => $article->getTitle(),
			'date' => $article->getDate(),
			'author' => $article->getAuthor(),
			'content' => $article->getContent(),
			'imgUrl' => $imgUrl
		);
	}
}

Routing

Wie wird nun aber bei Aufruf der indexAction() bzw. customAction() festgelegt welcher View gerendert werden soll?

Über den @format Parameter der einzelnen Routen wird festgelegt welcher View für das Rendering der Route zuständig ist.

Dadurch, dass die Klasse View\Article\CustomJson.php vorhanden ist, wird für die customAction-Route mit @format = json dieser CustomView verwendet.

Bei der indexAction-Route mit @format = json ist innerhalb des ArticleController definiert welcher View für das Format zuständig ist: $viewFormatToObjectNameMap = array('json' => 'TYPO3\Flow\Mvc\View\JsonView');

Für beide Routen mit @format = html wird ein TYPO3\Fluid\View\TemplateView verwendet, da dies der Default ist (siehe $defaultViewObjectName in TYPO3\Flow\Mvc\Controller\ActionController).

Configuration/Routes.yaml

-
  name: 'Article list'
  uriPattern: 'blogexample/articles.{@format}'
  defaults:
    '@package':    'Networkteam.Blogexample'
    '@controller': 'Article'
    '@action':     'index'
    '@format':     'html'
 
-
  name: 'Article list with custom view'
  uriPattern: 'blogexample/custom/articles.{@format}'
  defaults:
    '@package':    'Networkteam.Blogexample'
    '@controller': 'Article'
    '@action':     'custom'
    '@format':     'html'

Beispiel Package auf GitHub

Der gesamte Code des Artikels ist noch einmal als Beispiel im folgenden Package auf Github zu finden:

https://github.com/networkteam/Networkteam.Blogexample

Fazit:

Mit CustomViews kann in TYPO3 Flow der Controller aufgeräumt werden und Logik für den View in eigene PHP-Klassen ausgelagert werden. Das sorgt für eine bessere Erweiterbarkeit  und einfachere Wartung in der Zukunft.

28. August 2013
Kai Möller

Kai Möller