pt_extlist API Tutorial

Nachdem Daniel in seinem Blog einen hervorragenden Artikel zum Thema Listen-Erstellung mit pt_extlist geschrieben hat, möchte ich an dieser Stelle eine kurze Einführung in die Verwendung der pt_extlist API geben. Im Gegensatz zu Daniel's Artikel, der den Einsatz von pt_extlist aus Integratoren-Sicht mit Hilfe von TypoScript beschreibt, soll dieser Artikel verstärkt auf die Verwendung aus Programmierer-Sicht eingehen.

Voraussetzungen und Anschauungsbeispiel

Angenommen, wir möchten innerhalb eines Plugins eine Liste von Datenbankeinträgen als Liste darstellen. Weiter angenommen, wir schreiben unsere Extension mit Extbase und nutzen Controller und Actions für die einzelnen Modi der Extension. Dann könnte es innerhalb unsere Extension eine Action "listAction" geben, welche für die Darstellung der Liste zuständig ist. Ich gehe an dieser Stelle mal davon aus, dass die notwendigen Grundlagen wie das Registrieren der Action in der ext_localconf.php und in der FlexForm XML Datei bekannt sind.

In unserem Beispielprojekt soll eine Liste mit Veranstaltungen ausgegeben werden.

Konfiguration einer Liste in TypoScript

Wenn man pt_extlist als API verwenden möchte, muss man wie bei der "herkömmlichen" Verwenung auch eine Liste in TypoScript definieren bzw. konfigurieren. Wie das geht, beschreibt zum einen Daniel in seinem Blog Post sehr gut, zum anderen werden im Configuration/TypoScript-Verzeichnis der Extension zahlreiche Beispiele mitgeliefert. Wichtig für uns ist die Tatsache, dass jede Liste ihren eigenen Identifier hat. Über diesen wird auch bei der Verwendung als API die Liste angesprochen. 

Wir gehen davon aus, dass wir für unser Projekt eine Liste mit dem List-Identifier "publicEvents" konfiguriert haben. Die Konfiguration sieht folgendermaßen aus:

plugin.tx_jdavsv.settings.listConfig.publicEvents {

    backendConfig < plugin.tx_ptextlist.prototype.backend.extbase
    backendConfig {
        repositoryClassName = Tx_JdavSv_Domain_Repository_EventRepository
        sorting = dateEnd
    }
    
    
    
    fields {
    
        event {
            table = __self__
            field = __object__
        }
    
        dateStart {
            table = __self__
            field = dateStart
            isSortable = 1
        }
        
        dateEnd {
            table = __self__
            field = dateEnd
            isSortable = 1
        }
        
        title {
            table = __self__
            field = title
            isSortable = 1
        }
        
        category {
            table = __self__
            field = category
            isSortable = 0
        }
        
        accreditationNumber {
            table = __self__
            field = accreditationNumber
            isSortable = 1
        }
        
        price {
            table = __self__
            field = price
            isSortable = 1
        }
        
        maxRegistrations {
            table = __self__
            field = maxRegistrations
            isSortable = 1
        }

        isPublic {
        	table = __self__
        	field = isPublic
        }
    
    }
    
    
    
    columns {
    
        10 {
            fieldIdentifier = accreditationNumber
            columnIdentifier = accreditationNumber
            label = Akk-Nr
            sorting = accreditationNumber
        }
        
        20 {
            fieldIdentifier = dateEnd, dateStart
            columnIdentifier = dateEnd
            label = Ende
            sortingFields {
                10 {
                    field = dateStart
                    label = Beginn
                    direction = asc
                    forceDirection = 0
                }

                20 {
                    field = dateEnd
                    label = Ende
                    direction = asc
                    forceDirection = 0
                }
            }
        }
        
        30 {
            fieldIdentifier = title
            columnIdentifier = title
            label = Titel
            sorting = title
        }

        40 {
            fieldIdentifier = event
            columnIdentifier = event
            label =
            isSortable = 0
        }
    
    }

}

Im Gegensatz zu Daniel's Beispiel wird hier kein "normales" Datenbankbackend verwendet, sonder ein Extbase Backend - will heissen, die Daten werden nicht direkt aus der Datenbank gelesen, sondern über ein Extbase-Repository. Dies hat den Vorteil, dass wir an späterer Stelle direkt auf die Domänen-Modelle zugreifen können.

Der extlist-Kontext

Wie Daniel in seinem Post sehr schön beschreibt, besteht pt_extlist aus mehreren Komponenten: Liste, Header, Filter, Pager... diese einzelnen Komponenten und deren Zusammenspiel ist auf API-Ebene in einem Objekt zusammengefasst: dem sogenannten extlistContext. Mit einem einfach Aufruf bekommt man einen solchen Context für einen angegebenen List-Identifier:

/**
 * Displays all Events
 *
 * @return string The rendered list view
 */
public function listAction() {
	$extlistContext = Tx_PtExtlist_ExtlistContext_ExtlistContextFactory::getContextByCustomConfiguration(
		$this->settings['listConfig']['publicEvents'], 'publicEvents');
		
	$this->view->assign('listData', $extlistContext->getListData());
}

Der Code-Ausschnitt zeigt aber noch mehr: durch einen Getter bekommt man wie in diesem Beispiel die gerenderten Listen Daten und kann diese an die View übergeben.

Das Template

Mit der Übergabe der Daten an die View ist es noch nicht getan. Die Liste soll ja schließlich ausgegeben werden und dafür ist das entsprechende Template zuständig. Hier das Beispiel für unsere Veranstaltungs-Liste:

<div class="events-list">
	<f:for each="{listData}" as="row">
		<div class="well">
			<div class="well well-white">
				<p class="pull-right btn {row.event.value.lights}">{row.event.value.lightsText}</p>
				<small>{row.event.value.category.name}</small>
				<h2>{row.event.value.title}</h2>
			</div>
			<table>
				<tr>
					<td class="span2">
						<small><i class="icon-tag"></i> {row.event.value.category.shortcut}-{row.event.value.accreditationNumber}</small>
					</td>
					<td class="span4">
						<small><i class="icon-calendar"></i> von {row.event.value.dateStart -> f:format.date(format: 'd.m.Y')} bis {row.event.value.dateStart -> f:format.date(format: 'd.m.Y')}</small>
					</td>
					<td class="span4">
						<small><i class="icon-home"></i> {row.event.value.accommodation.name}</small>
					</td>
				</tr>
			</table>
			<table class="table table-no-bottom">
				<tr>
					<td colspan="2">
						{row.event.value.announcement}
					</td>
				</tr>
				<tr>
					<td>
						<br/>
						<f:if condition="{row.event.value.currentUserIsRegisteredForThisEvent}">
							<f:then>
								<p><strong><i class="icon-info-sign"></i> Du bist zu dieser Veranstaltung angemeldet!</strong></p>
							</f:then>
							<f:else>
								<p>&nbsp;</p>
							</f:else>
						</f:if>
					</td>
					<td>
						<p class="pull-right"><br>
							<f:link.action class="btn btn-large" controller="Event" action="show" arguments="{event: row.event.value}">Details</f:link.action>
							<f:if condition="{row.event.value.currentUserIsNotRegisteredForThisEvent}">
								<f:then>
									<f:link.action class="btn btn-large" controller="Registration" action="register" arguments="{event: row.event.value}">Anmelden</f:link.action>
								</f:then>
								<f:else>
									<f:if condition="{row.event.value.cancelRegistrationIsPossible}">
										<f:then><f:link.action class="btn btn-large btn-danger" controller="Registration" action="unregister" arguments="{event: row.event.value}">Abmelden</f:link.action></f:then>
									</f:if>
								</f:else>
							</f:if>
						</p>
					</td>
				</tr>
			</table>
		</div>
	</f:for>
</div>

Das Beispiel ist zwar etwas komplexer, aber es zeigt sehr schön, wie mit den Extbase Domänen Objekten innerhalb eines FLUID Templates gearbeitet werden kann.

Wir haben in der Action die Listen Daten mit dem Variablennamen "listData" an die View und damit an das Template übergeben. Hierüber kann mit <f:for each="{listData}" as="row"> iteriert werden.

Jede Zeile unserer Daten ist nun unter der Variable "row" im Inneren der for-Schleife verfügbar. Vorher habe ich erwähnt, dass wir mit einem Extbase Backend arbeiten. Damit kann das eigentlich Domänen-Objekt (in diesem Fall eine Veranstaltung) über den Pfad {row.event.value} abgerufen werden. Hinter "value" verbirgt sich in diesem Falle das Domänenobjekt und mit {row.event.value.title} kann die Property "title" des Domänenobjekts Event angesprochen werden.

Interessant ist vielleicht noch die Verwendung eines Inline-Viewhelpers für die Formatierung von Datumsangaben: {row.event.value.dateStart -> f:format.date(format: 'd.m.Y')}. Hier wird der Wert von dateStart direkt an den format.date ViewHelper übergeben, der als Parameter das Datumsformat übergeben kriegt, mit dem er das gegebene Datum formatieren soll.

Weitere Komponenten aus dem Extlist-Kontext

Listen Daten sind nicht das einzige Objekt, welches sich aus dem Extlist-Kontext beziehen lässt. Wie bereits gesagt, lassen sich alle Komponenten dort einfach ansprechen. Hier ein weiteres Beispiel einer Liste, in der neben den Spalten noch Filter konfiguriert sind, die zusammen mit dem Pager an die View übergeben werden:

/**
 * Displays all Events
 *
 * @return string The rendered list view
 */
public function listAction() {

	if (!$this->feUser->getIsAdmin()) {
		$this->forward('myEventsList');
	}

	$extlistContextForEventAdminList = Tx_PtExtlist_ExtlistContext_ExtlistContextFactory::getContextByCustomConfiguration(
		$this->settings['listConfig']['adminEvents'], 'adminEvents');

	$extlistContextForRegistrationsTeamerList = Tx_PtExtlist_ExtlistContext_ExtlistContextFactory::getContextByCustomConfiguration(
		$this->settings['listConfig']['registrationsTeamer'],
		'registrationsTeamer'
	);
	$registrationsByEventFilterForTeamerList = 
		$extlistContextForRegistrationsTeamerList->getFilterBoxCollection()
		->getFilterboxByFilterboxIdentifier('filterbox1')
		->getFilterByFilterIdentifier('registrationsByEventFilter');

	$extlistContextForRegistrationsParticipantsList = Tx_PtExtlist_ExtlistContext_ExtlistContextFactory::getContextByCustomConfiguration(
		$this->settings['listConfig']['registrationsParticipants'],
		'registrationsParticipants'
	);
	$registrationsByEventFilterForParticipantsList = 
		$extlistContextForRegistrationsParticipantsList->getFilterBoxCollection()
		->getFilterboxByFilterboxIdentifier('filterbox1')
		->getFilterByFilterIdentifier('registrationsByEventFilter');
		
	$this->view->assign('listData', $extlistContextForEventAdminList->getListData());
	$this->view->assign('listCaptions', 
		$extlistContextForEventAdminList->getRendererChain()
			->renderCaptions($extlistContextForEventAdminList->getList()->getListHeader())
	);
	$this->view->assign('listHeader', $extlistContextForEventAdminList->getList()->getListHeader());
	$this->view->assign('registrationsByEventFilterForTeamerList', $registrationsByEventFilterForTeamerList);
	$this->view->assign('registrationsByEventFilterForParticipantsList', $registrationsByEventFilterForParticipantsList);
	$this->view->assign('pager', $extlistContextForEventAdminList->getPager());
	$this->view->assign('pagerCollection', $extlistContextForEventAdminList->getPagerCollection());
}

Verwendung von Filtern im Template

Wie Filterobjekte an das Template übergeben werden können, wurde oben gezeigt. Jetzt ist es noch spannend zu wissen, wie diese Filter im Template selbst verwendet werden. Dazu soll folgendes FLUID Template etwas Anschauung liefern:

<f:form class="well form-inline" controller="RegistrationAdmin" action="list">
	<strong>Filter</strong>
	<table>
		<tr>
			<td>
				<span>Teilnehmername:</span><br>
				<f:form.textfield class="span3" id="{userFilter.filterIdentifier}" property="filterValue"
								  value="{userFilter.filterValue}"
								  name="{extlist:namespace.FormElementName(object:'{userFilter}' property:'filterValue')}"
								  size="{userFilter.filterConfig.settings.size}"
								  maxlength="{userFilter.filterConfig.settings.maxLength}"/>
				&nbsp;
			</td>
			<td>
				<span>Veranstaltung:</span><br>
				<ptextbase:form.select class="span4" id="{eventFilter.filterIdentifier}" property="filterValue"
								  value="{eventFilter.filterValue}"
								  name="{extlist:namespace.FormElementName(object:'{eventFilter}' property:'filterValue')}"
								  options="{eventFilter.events}"
								  optionValueField="uid"
								  optionLabelField="fullName"
								  emptyOption="[Keine Veranstaltung gewählt]"
						/>
				&nbsp;
			</td>
			<td>
				<span>von:</span><br>
				<f:form.textfield class="span2" id="{dateFilter.filterIdentifier}From" property="filterValueFrom"
								  value="{dateFilter.filterValueFrom ->f:format.date(format: 'd.m.Y')}"
								  name="{extlist:namespace.FormElementName(object:'{dateFilter}' property:'filterValueFrom')}" />
				&nbsp;
			</td>
			<td>
				<span>bis:</span><br>
				<f:form.textfield class="span2" id="{dateFilter.filterIdentifier}to" property="filterValueTo"
								  value="{dateFilter.filterValueTo -> f:format.date(format:'d.m.Y')}"
								  name="{extlist:namespace.FormElementName(object:'{dateFilter}' property:'filterValueTo')}" />
				&nbsp;
			</td>
			<td>
				<span>&nbsp;</span><br>
				<f:form.submit class="btn btn-primary" value="Filter anwenden"/>
				&nbsp;
				<f:link.action class="btn btn-primary" action="list" arguments="{restFilters: 1}"><i
						class="icon-white icon-remove"></i> Filter zurücksetzen
				</f:link.action>
			</td>
		</tr>
	</table>
</f:form>

In diesem Beispiel wurden 3 Filter übergeben: userFilter, eventFilter und dateFilter. Der erste Filter ist ein einfacher String-Filter, der mit Hilfe der Eingabe eines Namens die Anmeldungen nach dem damit registrierten Benutzer bzw. dessen Namen filtert. Der zweite Filter filtert die Anmeldungen nach einer Veranstaltung, die im Filter ausgewählt werden kann. Der dritte Filter filtert Anmeldungen nach einer Zeitspanne, die mit einem "von-Wert" und einem "bis-Wert" angegeben wird.

Interessant ist an dieser Stelle die Benennung der Filter-Werte im Formularelement. Diese muss - damit die Komponenten automatisch mit den enstprechenden Werten gefüttert werden - einem bestimmten Schema folgen. Um die Arbeit hier zu erleichtern gibt es einen speziellen ViewHelper, der den passenden Namensraum setzt, den extlist:namespace.FormElementName-Viewhelper. Dieser macht nichts anderes, als dass er für ein gegebenes Objekt ermittelt, wie dessen Namensraum innerhalb der GET/POST-Variablen lautet und den Formularfeld-Namen entsprechend setzt. Beispiel: name="{extlist:namespace.FormElementName(object:'{dateFilter}' property:'filterValueFrom')}" wird im HTML-Formular später zu  name="tx_jdavsv_pi1[registrationsAdmin][filters][registrationAdminFilters][dateFilter][filterValueFrom]"

Wie man sieht, nimmt einem der ViewHelper hier viel Schreibarbeit ab und sorgt außerdem dafür, dass auch wenn sich der Namensraum des Filters einmal ändern sollte, nicht jedes Mal das Template angepasst werden muss.

Sortierung

Eine kleine Besonderheit bei der Benutzung des Kontexts weißt die Sortierung von Listen auf. Wenn diese bei einer Liste möglich sein soll, muss der entsprechende Controller eine Sort-Action besitzen, die wie folgt aussehen kann:

/**
 * Sort action for event admin list
 */
public function sortAction() {
	$extlistContextForEventAdminList = Tx_PtExtlist_ExtlistContext_ExtlistContextFactory::getContextByCustomConfiguration(
		$this->settings['listConfig']['adminEvents'], 'adminEvents');
	$extlistContextForEventAdminList->getDataBackend()->getSorter()->reset();
	$this->forward('list');
}

Der Grund hierfür liegt in der Implementierung der Sortierung in pt_extlist begründet. Anders als beim Pager oder beim Filter muss bei einer Sortierung nach einer neuen Spalte zunächst die bisherige Sortierung zurückgesetzt werden, weil es auch möglich ist, dass nach mehreren Spalten sortiert werden kann. Dies ist aber meistens nicht das gewünschte Verhalten so dass obige Action vorhanden sein muss.

Fazit und Ausblick

Mit diesem Tutorial hoffe ich einen Einblick in die pt_extlist API-Verwendung geben zu haben. Es ist hoffentlich klar geworden, wie einfach die Verwendung von pt_extlist Komponenten auf Code-Ebene ist und wie schnell sich damit die Darstellung von Listen in Extbase-Projekten realisieren lässt.

Wer mehr über die Verwendung von extlist-Kontext erfahren möchte, dem sei ein Blick in den Code der YAG-Gallery empfohlen. Hier setzen wir diese Möglichkeit häufig ein und haben sie ursprünglich sogar dafür entwickelt.

 

Viel Spaß beim Ausprobieren!


 
Inhalt © Michael Knoll 2009-2017  •  Powered by TYPO3  •  TypoScript Blogging by Fabrizio Branca  •  TYPO3 Photo Gallery Management by yag  •  Impressum