Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluid compilation is disabled for templates with (pagination) widgets #28

Open
neos-bot opened this issue Feb 10, 2015 · 2 comments
Open
Labels

Comments

@neos-bot
Copy link
Owner

Jira issue originally created by user @bwaidelich:

Since https://forge.typo3.org/issues/28544 Fluid compiles templates into PHP when they are parsed for the first time.
But this compilation is disabled if a ViewHelper is used, that:

  • implements ChildNodeAccessInterface and
  • does not implement CompilableInterface

This is true for all widgets by default.

This is especially bad in the case of the PaginateViewHelper that is commonly used in large templates that could benefit profoundly from being compiled.

Maybe we can fix this for all widgets, but at least we should try to implement the CompilableInterface in the PaginateViewHelper.

I just stumbled upon this issue by chance - we should consider some kind of logging for non-compilable templates.

Jira-URL: https://jira.neos.io/browse/FLOW-196

@neos-bot
Copy link
Owner Author

Comment created by @bwaidelich:

A quick update: It won't be easy to solve this for all widgets.. But in general we (Sebastian & me) think that widgets might be deprecated at some point because (apart from this issue) there are several problems with them (e.g. tight coupling to MVC layers of Flow, expensive in terms of memory & performance, ...).
We think that in most of the cases it is actually not the sub request that is needed but rather allowing 3rd party packages to reuse and adjust the output of the widget. For example the infamous pagination widget doesn't actually need its own controller. the logic could easily go into the ViewHelper class itself. But the template code for the pagination links would then have to go into the PHP code (shiver) or one would have to add it inside the ViewHelper childnodes - every time.
A nice solution could be to come up with something I would see between a regular ViewHelper and a widget allowing the user to specify a template to use:

<f:paginate objects="{invoices}" as="paginatedInvoices" partial="My.Package:Pagination">
  <-- ... -->
</f:paginate>

the partial argument would fallback to sth like "TYPO3.Fluid:Pagination"

@neos-bot
Copy link
Owner Author

Comment created by @bwaidelich:

FYI I got this to work in a custom project:

<?php
namespace Acme\SomePackage\ViewHelpers;

use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\Mvc\ActionRequest;
use TYPO3\Flow\Persistence\QueryResultInterface;
use TYPO3\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3\Fluid\Core\ViewHelper\Exception as ViewHelperException;

/****
 * A pagination ViewHelper that works similar to Fluids paginate widget, but as standard ViewHelper without sub request!
 */
class PaginateViewHelper extends AbstractViewHelper {

    /****
     * @var boolean
     */
    protected $escapeOutput = FALSE;

    /****
     * @param QueryResultInterface|\Iterator|array $objects
     * @param string $as
     * @param integer $itemsPerPage
     * @param string $ifEmpty
     * @param string $argumentName
     * @param integer $maximumNumberOfLinks
     * @param boolean $insertAbove
     * @param boolean $insertBelow
     * @return string
     * @throws ViewHelperException
     */
    public function render($objects, $as, $itemsPerPage = 30, $ifEmpty = NULL, $argumentName = 'page', $maximumNumberOfLinks = 10, $insertAbove = FALSE, $insertBelow = TRUE) {
        if (!$objects instanceof QueryResultInterface && !$objects instanceof \Iterator && !is_array($objects)) {
            throw new ViewHelperException(sprintf('The paginate ViewHelper only supports objects of QueryResultInterface, Iterator or array, given: "%s"', get_class($objects)), 1426778650);
        }
        $numberOfObjects = count($objects);
        if ($numberOfObjects === 0 && $ifEmpty !== NULL) {
            return $ifEmpty;
        }
        $numberOfPages = (integer)ceil($numberOfObjects / $itemsPerPage);
        /*** @var ActionRequest $actionRequest **/
        $actionRequest = $this->controllerContext->getRequest();
        $currentPageNumber = (integer)$actionRequest->getInternalArgument('**' . $argumentName);
        if ($currentPageNumber < 1) {
            $currentPageNumber = 1;
        } elseif ($currentPageNumber > $numberOfPages) {
            $currentPageNumber = $numberOfPages;
        }

        $content = '';
        $pagination = $this->buildPagination($currentPageNumber, $numberOfPages, $maximumNumberOfLinks);
        $paginationLinks = $this->renderPaginationLinks($pagination, $actionRequest->getControllerActionName(), $argumentName);
        if ($insertAbove) {
            $content .= $paginationLinks;
        }
        $this->templateVariableContainer->add($as, $this->limitObjects($objects, $itemsPerPage, max(0, $itemsPerPage * ($currentPageNumber - 1))));
        $content .= $this->renderChildren();
        $this->templateVariableContainer->remove($as);
        if ($insertBelow) {
            $content .= $paginationLinks;
        }

        return $content;
    }

    /****
     * @param QueryResultInterface|\Iterator|array $objects
     * @param integer $limit
     * @param integer $offset
     * @return QueryResultInterface|\Iterator|array same type as $objects parameter
     */
    protected function limitObjects($objects, $limit, $offset) {
        if ($objects instanceof QueryResultInterface) {
            $query = $objects->getQuery();
            $query->setLimit($limit);
            if ($offset > 0) {
                $query->setOffset($offset);
            }
            return $query->execute();
        }
        if ($objects instanceof \Iterator) {
            return new \LimitIterator($objects, $offset, $limit);
        }
        if (is_array($objects)) {
            return array_slice($objects, $offset, $limit);
        }
    }

    /****
     * @param integer $currentPageNumber
     * @param integer $numberOfPages
     * @param integer $maximumNumberOfLinks
     * @return array in the format array('currentPageNumber' => 2, 'numberOfPages' => 123, 'previousPage' => array(), 'nextPage' => array(...), 'pages' => array(array('number' => 1, 'isCurrent' => TRUE), ...))
     */
    protected function buildPagination($currentPageNumber, $numberOfPages, $maximumNumberOfLinks = NULL) {
        if ($currentPageNumber < 1) {
            $currentPageNumber = 1;
        } elseif ($currentPageNumber > $numberOfPages) {
            $currentPageNumber = $numberOfPages;
        }
        if ($maximumNumberOfLinks === NULL || $maximumNumberOfLinks > $numberOfPages) {
            $maximumNumberOfLinks = $numberOfPages;
        }
        $pagination = array(
            'currentPageNumber' => $currentPageNumber,
            'numberOfPages' => $numberOfPages,
        );
        if ($currentPageNumber > 1) {
            $pagination['previousPage'] = $this->createPageLink($currentPageNumber - 1, $currentPageNumber, 'previous');
        } else {
            $pagination['previousPage'] = NULL;
        }
        if ($currentPageNumber < $numberOfPages) {
            $pagination['nextPage'] = $this->createPageLink($currentPageNumber <ins> 1, $currentPageNumber, 'next');
        } else {
            $pagination['nextPage'] = NULL;
        }

        $delta = floor($maximumNumberOfLinks / 2);
        $displayRangeStart = $currentPageNumber - $delta;
        $displayRangeEnd = $currentPageNumber </ins> $delta <ins> ($maximumNumberOfLinks % 2 === 0 ? 1 : 0);
        if ($displayRangeStart < 1) {
            $displayRangeEnd -= $displayRangeStart - 1;
        }
        if ($displayRangeEnd > $numberOfPages) {
            $displayRangeStart -= ($displayRangeEnd - $numberOfPages);
        }
        $displayRangeStart = (integer)max($displayRangeStart, 1);
        $displayRangeEnd = (integer)min($displayRangeEnd, $numberOfPages);

        $pages = array();
        if ($displayRangeStart > 1) {
            $pages[] = $this->createPageLink(1);
            if ($displayRangeStart > 2) {
                $pages[] = $this->createPageLink(NULL);
            }
        }
        if ($numberOfPages > 1) {
            for ($pageNumber = $displayRangeStart; $pageNumber <= $displayRangeEnd; $pageNumber</ins><ins>) {
                $pages[] = $this->createPageLink($pageNumber, $currentPageNumber);
            }
        }
        if ($displayRangeEnd < $numberOfPages) {
            if ($displayRangeEnd </ins> 1 < $numberOfPages) {
                $pages[] = $this->createPageLink(NULL);
            }
            $pages[] = $this->createPageLink($numberOfPages);
        }
        $pagination['pages'] = $pages;

        return $pagination;
    }

    /****
     * @param array $pagination
     * @param string $actionName
     * @param string $pageArgumentName
     * @return string
     */
    public function renderPaginationLinks(array $pagination, $actionName, $pageArgumentName) {
        $arguments = array(
            'pagination' => $pagination,
            'actionName' => $actionName,
            'pageArgumentName' => $pageArgumentName,
        );
        if ($this->templateVariableContainer->exists('settings')) {
            $arguments['settings'] = $this->templateVariableContainer->get('settings');
        }
        return $this->viewHelperVariableContainer->getView()->renderPartial('Pagination', NULL, $arguments);
    }

    /****
     * @param integer $pageNumber NULL => '...'
     * @param integer $currentPageNumber
     * @param string $label
     * @return array
     */
    protected function createPageLink($pageNumber, $currentPageNumber = NULL, $label = NULL) {
        $pageLink = array();
        if ($pageNumber === NULL) {
            $pageLink['isEllipsis'] = TRUE;
            $pageLink['label'] = '...';
        } else {
            $pageLink['label'] = $pageNumber;
        }
        if ($label !== NULL) {
            $pageLink['label'] = $label;
        }
        $pageLink['arguments'] = $pageNumber > 1 ? array('**' . $this->arguments['argumentName'] => $pageNumber) : NULL;
        $pageLink['isCurrent'] = $currentPageNumber !== NULL && $pageNumber === $currentPageNumber;
        $pageLink['isLinked'] = $pageNumber !== NULL && $pageNumber !== $currentPageNumber;
        return $pageLink;
    }
}

Currently it's hard-coded to a partial Pagination which could look like:

<div class="pagination pagination-centered">
    <ul>
        <f:if condition="{pagination.previousPage}">
            <li class="previous">
                <f:link.action action="{actionName}" addQueryString="true" arguments="{pagination.previousPage.arguments}">«</f:link.action>
            </li>
        </f:if>
        <f:for each="{pagination.pages}" as="page">
            <f:if condition="{page.isLinked}">
                <f:then>
                    <li>
                        <f:link.action action="{actionName}" addQueryString="true" arguments="{page.arguments}">{page.label}</f:link.action>
                    </li>
                </f:then>
                <f:else>
                    <li class="{f:if(condition: page.isCurrent, then: 'active', else: 'disabled')}">
                        <span>{page.label}</span>
                    </li>
                </f:else>
            </f:if>
        </f:for>
        <f:if condition="{pagination.nextPage}">
            <li class="next">
                <f:link.action action="{actionName}" addQueryString="true" arguments="{pagination.nextPage.arguments}">»</f:link.action>
            </li>
        </f:if>
    </ul>
</div>

@neos-bot neos-bot added the bug label Sep 20, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant