We think the decision to remove TYPO3 widgets (i.e. a ViewHelper with its own controller) is great. Having your own controller for a ViewHelper has always felt a bit strange. Still, we like the idea of a PageBrowser-ViewHelper in most cases. This keeps the controller slim (concept of a slim controller in Extbase).
In the following we will show you how you can build your own Paginate-ViewHelper for arrays or query results (result from an Extbase repository) in TYPO3 11 (and already in 10).
In the example, the new ViewHelper is located under Classes/ViewHelpers/Pagination/PaginateViewHelper.php:
<?php
declare(strict_types = 1);
namespace In2code\Lux\ViewHelpers\Pagination;
use Closure;
use In2code\Lux\Exception\NotPaginatableException;
use TYPO3\CMS\Core\Pagination\ArrayPaginator;
use TYPO3\CMS\Core\Pagination\PaginationInterface;
use TYPO3\CMS\Core\Pagination\PaginatorInterface;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* PaginateViewHelper
*/
class PaginateViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
/**
* @return void
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('objects', 'mixed', 'array or queryresult', true);
$this->registerArgument('as', 'string', 'new variable name', true);
$this->registerArgument('itemsPerPage', 'int', 'items per page', false, 10);
$this->registerArgument('name', 'string', 'unique identification - will take "as" as fallback', false, '');
}
/**
* @param array $arguments
* @param Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
* @return string
* @throws NotPaginatableException
*/
public static function renderStatic(
array $arguments,
Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
if ($arguments['objects'] === null) {
return $renderChildrenClosure();
}
$templateVariableContainer = $renderingContext->getVariableProvider();
$templateVariableContainer->add($arguments['as'], [
'pagination' => self::getPagination($arguments, $renderingContext),
'paginator' => self::getPaginator($arguments, $renderingContext),
'name' => self::getName($arguments)
]);
$output = $renderChildrenClosure();
$templateVariableContainer->remove($arguments['as']);
return $output;
}
/**
* @param array $arguments
* @param RenderingContextInterface $renderingContext
* @return PaginationInterface
* @throws NotPaginatableException
*/
protected static function getPagination(
array $arguments,
RenderingContextInterface $renderingContext
): PaginationInterface {
$paginator = self::getPaginator($arguments, $renderingContext);
return GeneralUtility::makeInstance(SimplePagination::class, $paginator);
}
/**
* @param array $arguments
* @param RenderingContextInterface $renderingContext
* @return PaginatorInterface
* @throws NotPaginatableException
*/
protected static function getPaginator(
array $arguments,
RenderingContextInterface $renderingContext
): PaginatorInterface {
if (is_array($arguments['objects'])) {
$paginatorClass = ArrayPaginator::class;
} elseif (is_a($arguments['objects'], QueryResultInterface::class)) {
$paginatorClass = QueryResultPaginator::class;
} else {
throw new NotPaginatableException('Given object is not supported for pagination', 1634132847);
}
return GeneralUtility::makeInstance(
$paginatorClass,
$arguments['objects'],
self::getPageNumber($arguments, $renderingContext),
$arguments['itemsPerPage']
);
}
/**
* @param array $arguments
* @param RenderingContextInterface $renderingContext
* @return int
*/
protected static function getPageNumber(array $arguments, RenderingContextInterface $renderingContext): int
{
$extensionName = $renderingContext->getControllerContext()->getRequest()->getControllerExtensionName();
$pluginName = $renderingContext->getControllerContext()->getRequest()->getPluginName();
$extensionService = GeneralUtility::makeInstance(ExtensionService::class);
$pluginNamespace = $extensionService->getPluginNamespace($extensionName, $pluginName);
$variables = GeneralUtility::_GP($pluginNamespace);
if ($variables !== null) {
if (!empty($variables[self::getName($arguments)]['currentPage'])) {
return (int)$variables[self::getName($arguments)]['currentPage'];
}
}
return 1;
}
/**
* @param array $arguments
* @return string
*/
protected static function getName(array $arguments): string
{
return $arguments['name'] ?: $arguments['as'];
}
}
To build the links in the fluid, we use a second ViewHelper Classes / ViewHelpers / Pagination / UriViewHelper.php:
<?php
declare(strict_types = 1);
namespace In2code\Lux\ViewHelpers\Pagination;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* UriViewHelper
*/
class UriViewHelper extends AbstractTagBasedViewHelper
{
/**
* Initialize arguments
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('name', 'string', 'identifier important if more widgets on same page', false, 'widget');
$this->registerArgument('arguments', 'array', 'Arguments', false, []);
}
/**
* Build an uri to current action with &tx_ext_plugin[currentPage]=2
*
* @return string The rendered uri
*/
public function render(): string
{
$uriBuilder = $this->renderingContext->getControllerContext()->getUriBuilder();
$extensionName = $this->renderingContext->getControllerContext()->getRequest()->getControllerExtensionName();
$pluginName = $this->renderingContext->getControllerContext()->getRequest()->getPluginName();
$extensionService = GeneralUtility::makeInstance(ExtensionService::class);
$pluginNamespace = $extensionService->getPluginNamespace($extensionName, $pluginName);
$argumentPrefix = $pluginNamespace . '[' . $this->arguments['name'] . ']';
$arguments = $this->hasArgument('arguments') ? $this->arguments['arguments'] : [];
if ($this->hasArgument('action')) {
$arguments['action'] = $this->arguments['action'];
}
if ($this->hasArgument('format') && $this->arguments['format'] !== '') {
$arguments['format'] = $this->arguments['format'];
}
$uriBuilder->reset()
->setArguments([$argumentPrefix => $arguments])
->setAddQueryString(true)
->setArgumentsToBeExcludedFromQueryString([$argumentPrefix, 'cHash']);
$addQueryStringMethod = $this->arguments['addQueryStringMethod'] ?? null;
if (is_string($addQueryStringMethod)) {
$uriBuilder->setAddQueryStringMethod($addQueryStringMethod);
}
return $uriBuilder->build();
}
}
So you can use it almost as usual in the fluid:
<lux:pagination.paginate objects="{downloads}" as="downloadsPaginator" itemsPerPage="15">
<f:for each="{downloadsPaginator.paginator.paginatedItems}" as="download">
<p>{download.title}</p>
</f:for>
<f:alias map="{pagination:downloadsPaginator.pagination, paginator:downloadsPaginator.paginator, name:downloadsPaginator.name}">
<f:render partial="Miscellaneous/Pagination" arguments="{_all}" />
</f:alias>
</lux:pagination.paginate>
The partial with the actual page browser could then look like this or something similar:
<f:if condition="{paginator.numberOfPages} > 1">
<nav aria-label="pagebrowser">
<ul class="f3-widget-paginator pagination">
<f:if condition="{pagination.previousPageNumber} && {pagination.previousPageNumber} >= {pagination.firstPageNumber}">
<li class="previous">
<a href="{lux:pagination.uri(arguments:'{currentPage:pagination.previousPageNumber}',name:name)}" title="previous" class="page-link">
<
</a>
</li>
</f:if>
<f:if condition="{pagination.hasLessPages}">
<li class="page-item">…</li>
</f:if>
<f:for each="{pagination.allPageNumbers}" as="page">
<f:if condition="{page} == {paginator.currentPageNumber}">
<f:then>
<li class="page-item current active">
<span class="page-link">{page}</span>
</li>
</f:then>
<f:else>
<li class="page-item">
<a href="{lux:pagination.uri(arguments:'{currentPage:page}',name:name)}" class="page-link">{page}</a>
</li>
</f:else>
</f:if>
</f:for>
<f:if condition="{pagination.hasMorePages}">
<li class="page-item">…</li>
</f:if>
<f:if condition="{pagination.nextPageNumber} && {pagination.nextPageNumber} <= {pagination.lastPageNumber}">
<li class="next">
<a href="{lux:pagination.uri(arguments:'{currentPage:pagination.nextPageNumber}',name:name)}" title="next" class="page-link">
>
</a>
</li>
</f:if>
</ul>
</nav>
</f:if>
Tip: All information about the new paginator and pagination classes in TYPO3 can be found in the documentation