conteneurisation de l'appli

This commit is contained in:
Rudy
2023-01-07 22:07:15 +01:00
parent b79e8502b1
commit b21f2bc04b
8725 changed files with 1271447 additions and 676 deletions

21
vendor/knplabs/knp-components/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2011 KnpLabs
The MIT license, reference http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,73 @@
{
"name": "knplabs/knp-components",
"type": "library",
"description": "Knplabs component library",
"keywords": [
"components",
"paginator",
"pager",
"knp",
"knplabs"
],
"homepage": "http://github.com/KnpLabs/knp-components",
"license": "MIT",
"authors": [
{
"name": "KnpLabs Team",
"homepage": "https://knplabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://github.com/KnpLabs/knp-components/contributors"
}
],
"require": {
"php": "^7.4 || ^8.0",
"symfony/event-dispatcher-contracts": "^2.0 || ^3.0",
"symfony/http-foundation": "^4.4 || ^5.4 || ^6.0"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"doctrine/mongodb-odm": "^2.0",
"doctrine/orm": "^2.7",
"doctrine/phpcr-odm": "^1.2",
"jackalope/jackalope-doctrine-dbal": "^1.2",
"phpunit/phpunit": "^9.5",
"propel/propel1": "^1.7",
"ruflin/elastica": "^7.0",
"solarium/solarium": "^6.0",
"symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
"symfony/property-access": "^4.4 || ^5.4 || ^6.0"
},
"suggest": {
"doctrine/common": "to allow usage pagination with Doctrine ArrayCollection",
"doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB",
"doctrine/orm": "to allow usage pagination with Doctrine ORM",
"doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR",
"propel/propel1": "to allow usage pagination with Propel ORM",
"ruflin/elastica": "to allow usage pagination with ElasticSearch Client",
"solarium/solarium": "to allow usage pagination with Solarium Client",
"symfony/property-access": "To allow sorting arrays"
},
"conflict": {
"doctrine/dbal": "<2.10"
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Knp\\Component\\": "src/Knp/Component"
}
},
"autoload-dev": {
"psr-4": {
"Test\\": "tests/Test"
}
},
"scripts": {
"test": "phpunit"
}
}

View File

@ -0,0 +1,12 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#2 \\$stringPattern of class Doctrine\\\\ORM\\\\Query\\\\AST\\\\LikeExpression constructor expects Doctrine\\\\ORM\\\\Query\\\\AST\\\\InputParameter, Doctrine\\\\ORM\\\\Query\\\\AST\\\\Literal given\\.$#"
count: 1
path: src/Knp/Component/Pager/Event/Subscriber/Filtration/Doctrine/ORM/Query/WhereWalker.php
-
message: "#^Instanceof between Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalExpression and Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalPrimary will always evaluate to false\\.$#"
count: 1
path: src/Knp/Component/Pager/Event/Subscriber/Filtration/Doctrine/ORM/Query/WhereWalker.php

View File

@ -0,0 +1,24 @@
<?php
namespace Knp\Component\Pager\Event;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Specific Event class for paginator
*/
final class AfterEvent extends Event
{
private PaginationInterface $pagination;
public function __construct(PaginationInterface $paginationView)
{
$this->pagination = $paginationView;
}
public function getPaginationView(): PaginationInterface
{
return $this->pagination;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Knp\Component\Pager\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Specific Event class for paginator
*/
final class BeforeEvent extends Event
{
private EventDispatcherInterface $eventDispatcher;
private ?Request $request;
public function __construct(EventDispatcherInterface $eventDispatcher, ?Request $request)
{
$this->eventDispatcher = $eventDispatcher;
$this->request = $request;
}
public function getEventDispatcher(): EventDispatcherInterface
{
return $this->eventDispatcher;
}
public function getRequest(): Request
{
return $this->request ?? Request::createFromGlobals();
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Knp\Component\Pager\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Specific Event class for paginator
*/
final class ItemsEvent extends Event
{
/**
* A target being paginated
*
* @var mixed
*/
public $target;
/**
* List of options
*/
public array $options;
/**
* Items result
*
* @var mixed
*/
public $items;
/**
* Count result
*/
public int $count;
private int $offset;
private int $limit;
private array $customPaginationParams = [];
public function __construct(int $offset, int $limit)
{
$this->offset = $offset;
$this->limit = $limit;
}
public function setCustomPaginationParameter($name, $value): void
{
$this->customPaginationParams[$name] = $value;
}
public function getCustomPaginationParameters(): array
{
return $this->customPaginationParams;
}
public function unsetCustomPaginationParameter($name): void
{
if (isset($this->customPaginationParams[$name])) {
unset($this->customPaginationParams[$name]);
}
}
public function getLimit(): int
{
return $this->limit;
}
public function getOffset(): int
{
return $this->offset;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Knp\Component\Pager\Event;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Specific Event class for paginator
*/
final class PaginationEvent extends Event
{
/**
* A target being paginated
*
* @var mixed
*/
public $target;
/**
* List of options
*/
public array $options;
private PaginationInterface $pagination;
public function setPagination(PaginationInterface $pagination): void
{
$this->pagination = $pagination;
}
public function getPagination(): PaginationInterface
{
return $this->pagination;
}
}

View File

@ -0,0 +1,261 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query;
use Doctrine\DBAL\Types\Types as Type;
use Doctrine\ORM\Query\AST\Functions\LowerFunction;
use Doctrine\ORM\Query\AST\ComparisonExpression;
use Doctrine\ORM\Query\AST\ConditionalExpression;
use Doctrine\ORM\Query\AST\ConditionalFactor;
use Doctrine\ORM\Query\AST\ConditionalPrimary;
use Doctrine\ORM\Query\AST\ConditionalTerm;
use Doctrine\ORM\Query\AST\LikeExpression;
use Doctrine\ORM\Query\AST\Literal;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\WhereClause;
use Doctrine\ORM\Query\TreeWalkerAdapter;
/**
* Where Query TreeWalker for Filtration functionality
* in doctrine paginator
*/
class WhereWalker extends TreeWalkerAdapter
{
/**
* Filter key columns hint name
*/
public const HINT_PAGINATOR_FILTER_COLUMNS = 'knp_paginator.filter.columns';
/**
* Filter value hint name
*/
public const HINT_PAGINATOR_FILTER_VALUE = 'knp_paginator.filter.value';
/**
* Filter strings in a case insensitive way
*/
const HINT_PAGINATOR_FILTER_CASE_INSENSITIVE = 'knp_paginator.filter.case_insensitive';
/**
* Walks down a SelectStatement AST node, modifying it to
* filter the query like requested by url
*
* @param SelectStatement $AST
* @return void|string
*/
public function walkSelectStatement(SelectStatement $AST)
{
$query = $this->_getQuery();
$queriedValue = $query->getHint(self::HINT_PAGINATOR_FILTER_VALUE);
$columns = $query->getHint(self::HINT_PAGINATOR_FILTER_COLUMNS);
$filterCaseInsensitive = $query->getHint(self::HINT_PAGINATOR_FILTER_CASE_INSENSITIVE);
$components = $this->_getQueryComponents();
$filterExpressions = [];
$expressions = [];
foreach ($columns as $column) {
$alias = false;
$parts = explode('.', $column, 2);
$field = end($parts);
if (2 <= count($parts)) {
$alias = trim(reset($parts));
if (!array_key_exists($alias, $components)) {
throw new \UnexpectedValueException("There is no component aliased by [{$alias}] in the given Query");
}
$meta = $components[$alias];
if (!$meta['metadata']->hasField($field)) {
throw new \UnexpectedValueException("There is no such field [{$field}] in the given Query component, aliased by [$alias]");
}
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field);
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
} else {
if (!array_key_exists($field, $components)) {
throw new \UnexpectedValueException("There is no component field [{$field}] in the given Query");
}
$pathExpression = $components[$field]['resultVariable'];
}
$expression = new ConditionalPrimary();
if (isset($meta) && $meta['metadata']->getTypeOfField($field) === 'boolean') {
if (in_array(strtolower($queriedValue), ['true', 'false'])) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue));
} elseif (is_numeric($queriedValue)) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue == '1' ? 'true' : 'false'));
} else {
continue;
}
unset($meta);
} elseif (is_numeric($queriedValue)
&& (
!isset($meta)
|| in_array(
$meta['metadata']->getTypeOfField($field),
[
Type::SMALLINT,
Type::INTEGER,
Type::BIGINT,
Type::FLOAT,
Type::DECIMAL,
]
)
)
) {
$expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::NUMERIC, $queriedValue));
} else {
$likePathExpression = $pathExpression;
$likeQueriedValue = $queriedValue;
if ($filterCaseInsensitive) {
$lower = new LowerFunction('lower');
$lower->stringPrimary = $pathExpression;
$likePathExpression = $lower;
$likeQueriedValue = strtolower($queriedValue);
}
$expression->simpleConditionalExpression = new LikeExpression($likePathExpression, new Literal(Literal::STRING, $likeQueriedValue));
}
$filterExpressions[] = $expression->simpleConditionalExpression;
$expressions[] = $expression;
}
if (count($expressions) > 1) {
$conditionalPrimary = new ConditionalExpression($expressions);
} elseif (count($expressions) > 0) {
$conditionalPrimary = reset($expressions);
} else {
return;
}
if ($AST->whereClause) {
if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) {
if (!$this->termContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
array_unshift(
$AST->whereClause->conditionalExpression->conditionalFactors,
$this->createPrimaryFromNode($conditionalPrimary)
);
}
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) {
if (!$this->primaryContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
$AST->whereClause->conditionalExpression = new ConditionalTerm([
$this->createPrimaryFromNode($conditionalPrimary),
$AST->whereClause->conditionalExpression,
]);
}
} elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression) {
if (!$this->expressionContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) {
$previousPrimary = new ConditionalPrimary();
$previousPrimary->conditionalExpression = $AST->whereClause->conditionalExpression;
$AST->whereClause->conditionalExpression = new ConditionalTerm([
$this->createPrimaryFromNode($conditionalPrimary),
$previousPrimary,
]);
}
}
} else {
$AST->whereClause = new WhereClause(
$conditionalPrimary
);
}
}
/**
* @param ConditionalExpression $node
* @param Node[] $filterExpressions
* @return bool
*/
private function expressionContainsFilter(ConditionalExpression $node, $filterExpressions): bool
{
foreach ($node->conditionalTerms as $conditionalTerm) {
if ($conditionalTerm instanceof ConditionalTerm && $this->termContainsFilter($conditionalTerm, $filterExpressions)) {
return true;
} elseif ($conditionalTerm instanceof ConditionalPrimary && $this->primaryContainsFilter($conditionalTerm, $filterExpressions)) {
return true;
}
}
return false;
}
/**
* @param ConditionalTerm $node
* @param Node[] $filterExpressions
* @return bool
*/
private function termContainsFilter(ConditionalTerm $node, $filterExpressions): bool
{
foreach ($node->conditionalFactors as $conditionalFactor) {
if ($conditionalFactor instanceof ConditionalFactor) {
if ($this->factorContainsFilter($conditionalFactor, $filterExpressions)) {
return true;
}
} elseif ($conditionalFactor instanceof ConditionalPrimary) {
if ($this->primaryContainsFilter($conditionalFactor, $filterExpressions)) {
return true;
}
}
}
return false;
}
/**
* @param ConditionalFactor $node
* @param Node[] $filterExpressions
* @return bool
*/
private function factorContainsFilter(ConditionalFactor $node, $filterExpressions): bool
{
if ($node->conditionalPrimary instanceof ConditionalPrimary && $node->not === false) {
return $this->primaryContainsFilter($node->conditionalPrimary, $filterExpressions);
}
return false;
}
/**
* @param ConditionalPrimary $node
* @param Node[] $filterExpressions
* @return bool
*/
private function primaryContainsFilter(ConditionalPrimary $node, $filterExpressions): bool
{
if ($node->isSimpleConditionalExpression() && ($node->simpleConditionalExpression instanceof LikeExpression || $node->simpleConditionalExpression instanceof ComparisonExpression)) {
return $this->isExpressionInFilterExpressions($node->simpleConditionalExpression, $filterExpressions);
}
if ($node->isConditionalExpression()) {
return $this->expressionContainsFilter($node->conditionalExpression, $filterExpressions);
}
return false;
}
/**
* @param Node $node
* @param Node[] $filterExpressions
* @return bool
*/
private function isExpressionInFilterExpressions(Node $node, $filterExpressions): bool
{
foreach ($filterExpressions as $filterExpression) {
if ((string) $filterExpression === (string) $node) {
return true;
}
}
return false;
}
/**
* @param ConditionalPrimary|ConditionalExpression $node
* @return ConditionalPrimary
*/
private function createPrimaryFromNode($node): ConditionalPrimary
{
if ($node instanceof ConditionalPrimary) {
$conditionalPrimary = $node;
} else {
$conditionalPrimary = new ConditionalPrimary();
$conditionalPrimary->conditionalExpression = $node;
}
return $conditionalPrimary;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM;
use Doctrine\ORM\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\Event\Subscriber\Filtration\Doctrine\ORM\Query\WhereWalker;
use Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\Query\Helper as QueryHelper;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class QuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(?Request $request)
{
$this->request = $request ?? Request::createFromGlobals();
}
public function items(ItemsEvent $event): void
{
if ($event->target instanceof Query) {
$filterValue = $this->getQueryParameter($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]);
if (null === $filterValue || (empty($filterValue) && $filterValue !== '0')) {
return;
}
$filterName = $this->getQueryParameter($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME]);
if (!empty($filterName)) {
$columns = $filterName;
} elseif (!empty($event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS])) {
$columns = $event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS];
} else {
return;
}
$value = $this->getQueryParameter($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]);
if (false !== strpos($value, '*')) {
$value = str_replace('*', '%', $value);
}
if (is_string($columns) && false !== strpos($columns, ',')) {
$columns = explode(',', $columns);
}
$columns = (array) $columns;
if (isset($event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) {
foreach ($columns as $column) {
if (!in_array($column, $event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot filter by: [{$column}] this field is not in allow list");
}
}
}
$event->target
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_VALUE, $value)
->setHint(WhereWalker::HINT_PAGINATOR_FILTER_COLUMNS, $columns);
QueryHelper::addCustomTreeWalker($event->target, WhereWalker::class);
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
private function getQueryParameter(string $name): ?string
{
return $this->request->query->get($name);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Filtration;
use Knp\Component\Pager\Event\BeforeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FiltrationSubscriber implements EventSubscriberInterface
{
/**
* Lazy-load state tracker
*/
private bool $isLoaded = false;
public function before(BeforeEvent $event): void
{
// Do not lazy-load more than once
if ($this->isLoaded) {
return;
}
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = $event->getEventDispatcher();
// hook all standard filtration subscribers
$dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber($event->getRequest()));
$dispatcher->addSubscriber(new PropelQuerySubscriber($event->getRequest()));
$this->isLoaded = true;
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.before' => ['before', 1],
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Filtration;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class PropelQuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(?Request $request)
{
$this->request = $request ?? Request::createFromGlobals();
}
public function items(ItemsEvent $event): void
{
$query = $event->target;
if ($query instanceof \ModelCriteria) {
if (!$this->request->query->has($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME])) {
return;
}
if ($this->request->query->has($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME])) {
$columns = $this->request->query->get($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME]);
} elseif (!empty($event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS])) {
$columns = $event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS];
} else {
return;
}
if (is_string($columns) && false !== strpos($columns, ',')) {
$columns = explode(',', $columns);
}
$columns = (array) $columns;
if (isset($event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) {
foreach ($columns as $column) {
if (!in_array($column, $event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot filter by: [{$column}] this field is not in allow list");
}
}
}
$value = $this->request->query->get($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]);
$criteria = \Criteria::EQUAL;
if (false !== strpos($value, '*')) {
$value = str_replace('*', '%', $value);
$criteria = \Criteria::LIKE;
}
foreach ($columns as $column) {
if (false !== strpos($column, '.')) {
$query->where($column.$criteria.'?', $value);
} else {
$query->{'filterBy'.$column}($value, $criteria);
}
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate;
use ArrayObject;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ArraySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (is_array($event->target)) {
$event->count = count($event->target);
$event->items = array_slice(
$event->target,
$event->getOffset(),
$event->getLimit()
);
$event->stopPropagation();
} elseif ($event->target instanceof ArrayObject) {
$event->count = $event->target->count();
$event->items = new ArrayObject(array_slice(
$event->target->getArrayCopy(),
$event->getOffset(),
$event->getLimit()
));
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', -1/* other data arrays should be analized first*/],
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Callback;
/**
* Callback pagination.
*
* @author Piotr Pelczar <me@athlan.pl>
*/
class CallbackPagination
{
/**
* @var callable
*/
private $count;
/**
* @var callable
*/
private $items;
public function __construct(callable $count, callable $items)
{
$this->count = $count;
$this->items = $items;
}
public function getPaginationCount(): int
{
return call_user_func($this->count);
}
public function getPaginationItems(int $offset, int $limit): array
{
return call_user_func($this->items, $offset, $limit);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Callback;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Callback pagination.
*
* @author Piotr Pelczar <me@athlan.pl>
*/
class CallbackSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof CallbackPagination) {
$event->count = $event->target->getPaginationCount();
if($event->count > 0) {
$event->items = $event->target->getPaginationItems($event->getOffset(), $event->getLimit());
}
else {
$event->items = [];
}
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine;
use Doctrine\Common\Collections\Collection;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CollectionSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof Collection) {
$event->count = $event->target->count();
$event->items = $event->target->slice(
$event->getOffset(),
$event->getLimit()
);
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine;
use Doctrine\DBAL\Query\QueryBuilder;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* DBALQueryBuilderSubscriber.php
*
* @author Vladimir Chub <v@chub.com.ua>
*/
class DBALQueryBuilderSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof QueryBuilder) {
$target = $event->target;
// count results
$qb = clone $target;
//reset count orderBy since it can break query and slow it down
$qb
->resetQueryPart('orderBy')
;
// get the query
$sql = $qb->getSQL();
$qb
->resetQueryParts()
->select('count(*) as cnt')
->from('(' . $sql . ')', 'dbal_count_tbl')
;
$compat = $qb->execute();
$event->count = method_exists($compat, 'fetchColumn') ? $compat->fetchColumn(0) : $compat->fetchOne();
// if there is results
$event->items = [];
if ($event->count) {
$qb = clone $target;
$qb
->setFirstResult($event->getOffset())
->setMaxResults($event->getLimit())
;
$event->items = $qb
->execute()
->fetchAll()
;
}
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 10 /*make sure to transform before any further modifications*/],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ODM\MongoDB;
use Doctrine\ODM\MongoDB\Query\Builder;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class QueryBuilderSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof Builder) {
// change target into query
$event->target = $event->target->getQuery($event->options[PaginatorInterface::ODM_QUERY_OPTIONS] ?? []);
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/],
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ODM\MongoDB;
use Doctrine\ODM\MongoDB\Query\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class QuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof Query) {
// items
$type = $event->target->getType();
if ($type !== Query::TYPE_FIND) {
throw new \UnexpectedValueException('ODM query must be a FIND type query');
}
static $reflectionProperty;
if (is_null($reflectionProperty)) {
$reflectionClass = new \ReflectionClass('Doctrine\ODM\MongoDB\Query\Query');
$reflectionProperty = $reflectionClass->getProperty('query');
$reflectionProperty->setAccessible(true);
}
$queryOptions = $reflectionProperty->getValue($event->target);
$resultCount = clone $event->target;
$queryOptions['type'] = Query::TYPE_COUNT;
$reflectionProperty->setValue($resultCount, $queryOptions);
$event->count = $resultCount->execute();
$queryOptions = $reflectionProperty->getValue($event->target);
$queryOptions['type'] = Query::TYPE_FIND;
$queryOptions['limit'] = $event->getLimit();
$queryOptions['skip'] = $event->getOffset();
$resultQuery = clone $event->target;
$reflectionProperty->setValue($resultQuery, $queryOptions);
$cursor = $resultQuery->execute();
$event->items = [];
// iterator_to_array for GridFS results in 1 item
foreach ($cursor as $item) {
$event->items[] = $item;
}
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ODM\PHPCR;
use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class QueryBuilderSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (!$event->target instanceof QueryBuilder) {
return;
}
$event->target = $event->target->getQuery();
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/],
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ODM\PHPCR;
use Doctrine\ODM\PHPCR\Query\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class QuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (!$event->target instanceof Query) {
return;
}
$queryCount = clone $event->target;
$event->count = $queryCount->execute(null, Query::HYDRATE_PHPCR)->getRows()->count();
$query = $event->target;
$query->setMaxResults($event->getLimit());
$query->setFirstResult($event->getOffset());
$event->items = $query->execute();
$event->stopPropagation();
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\Query;
use Doctrine\ORM\Query;
/**
* ORM Query helper for cloning
* and hint processing
*/
class Helper
{
/**
* Clones the given $query and copies all used
* parameters and hints
*/
public static function cloneQuery(Query $query): Query
{
$clonedQuery = clone $query;
$clonedQuery->setParameters(clone $query->getParameters());
// attach hints
foreach ($query->getHints() as $name => $hint) {
$clonedQuery->setHint($name, $hint);
}
return $clonedQuery;
}
/**
* Add a custom TreeWalker $walker class name to
* be included in the CustomTreeWalker hint list
* of the given $query
*/
public static function addCustomTreeWalker(Query $query, string $walker): void
{
$customTreeWalkers = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
if ($customTreeWalkers !== false && is_array($customTreeWalkers)) {
$customTreeWalkers = array_merge($customTreeWalkers, [$walker]);
} else {
$customTreeWalkers = [$walker];
}
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $customTreeWalkers);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM;
use Doctrine\ORM\QueryBuilder;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class QueryBuilderSubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof QueryBuilder) {
// change target into query
$event->target = $event->target->getQuery();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/],
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\CountWalker;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class QuerySubscriber implements EventSubscriberInterface
{
public const HINT_COUNT = 'knp_paginator.count';
public const HINT_FETCH_JOIN_COLLECTION = 'knp_paginator.fetch_join_collection';
public function items(ItemsEvent $event): void
{
if (!$event->target instanceof Query) {
return;
}
$event->stopPropagation();
$useOutputWalkers = false;
if (isset($event->options['wrap-queries'])) {
$useOutputWalkers = $event->options['wrap-queries'];
}
$event->target
->setFirstResult($event->getOffset())
->setMaxResults($event->getLimit())
->setHint(CountWalker::HINT_DISTINCT, $event->options['distinct'])
;
$fetchJoinCollection = true;
if ($event->target->hasHint(self::HINT_FETCH_JOIN_COLLECTION)) {
$fetchJoinCollection = $event->target->getHint(self::HINT_FETCH_JOIN_COLLECTION);
} else if (isset($event->options['distinct'])) {
$fetchJoinCollection = $event->options['distinct'];
}
$paginator = new Paginator($event->target, $fetchJoinCollection);
$paginator->setUseOutputWalkers($useOutputWalkers);
if (($count = $event->target->getHint(self::HINT_COUNT)) !== false) {
$event->count = (int) $count;
} else {
$event->count = count($paginator);
}
$event->items = iterator_to_array($paginator);
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate;
use Elastica\Query;
use Elastica\SearchableInterface;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Elastica query pagination.
*
*/
class ElasticaQuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (is_array($event->target) && 2 === count($event->target) && reset($event->target) instanceof SearchableInterface && end($event->target) instanceof Query) {
[$searchable, $query] = $event->target;
$query->setFrom($event->getOffset());
$query->setSize($event->getLimit());
$results = $searchable->search($query);
$event->count = $results->getTotalHits();
if ($results->hasAggregations()) {
$event->setCustomPaginationParameter('aggregations', $results->getAggregations());
}
$event->setCustomPaginationParameter('resultSet', $results);
$event->items = $results->getResults();
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0], /* triggers before a standard array subscriber*/
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate;
use Knp\Component\Pager\Event\BeforeEvent;
use Knp\Component\Pager\Event\PaginationEvent;
use Knp\Component\Pager\Pagination\SlidingPagination;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PaginationSubscriber implements EventSubscriberInterface
{
/**
* Lazy-load state tracker
*/
private bool $isLoaded = false;
public function pagination(PaginationEvent $event): void
{
$event->setPagination(new SlidingPagination);
$event->stopPropagation();
}
public function before(BeforeEvent $event): void
{
// Do not lazy-load more than once
if ($this->isLoaded) {
return;
}
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = $event->getEventDispatcher();
// hook all standard subscribers
$dispatcher->addSubscriber(new ArraySubscriber);
$dispatcher->addSubscriber(new Callback\CallbackSubscriber);
$dispatcher->addSubscriber(new Doctrine\ORM\QueryBuilderSubscriber);
$dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber);
$dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QueryBuilderSubscriber);
$dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QuerySubscriber);
$dispatcher->addSubscriber(new Doctrine\ODM\PHPCR\QueryBuilderSubscriber);
$dispatcher->addSubscriber(new Doctrine\ODM\PHPCR\QuerySubscriber);
$dispatcher->addSubscriber(new Doctrine\CollectionSubscriber);
$dispatcher->addSubscriber(new Doctrine\DBALQueryBuilderSubscriber);
$dispatcher->addSubscriber(new PropelQuerySubscriber);
$dispatcher->addSubscriber(new SolariumQuerySubscriber());
$dispatcher->addSubscriber(new ElasticaQuerySubscriber());
$this->isLoaded = true;
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.before' => ['before', 0],
'knp_pager.pagination' => ['pagination', 0],
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use ModelCriteria;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PropelQuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if ($event->target instanceof ModelCriteria) {
// process count
$countQuery = clone $event->target;
$countQuery
->limit(0)
->offset(0)
;
if ($event->options[PaginatorInterface::DISTINCT]) {
$countQuery->distinct();
}
$event->count = intval($countQuery->count());
// process items
$result = null;
if ($event->count) {
$resultQuery = clone $event->target;
if ($event->options[PaginatorInterface::DISTINCT]) {
$resultQuery->distinct();
}
$resultQuery
->offset($event->getOffset())
->limit($event->getLimit())
;
$result = $resultQuery->find();
} else {
$result = []; // count is 0
}
$event->items = $result;
$event->stopPropagation();
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0],
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Paginate;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Solarium query pagination.
*
* @author Paweł Jędrzejewski <pjedrzejewski@diweb.pl>
*/
class SolariumQuerySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (is_array($event->target) && 2 == count($event->target)) {
$values = array_values($event->target);
[$client, $query] = $values;
if ($client instanceof \Solarium\Client && $query instanceof \Solarium\QueryType\Select\Query\Query) {
$query->setStart($event->getOffset())->setRows($event->getLimit());
$solrResult = $client->select($query);
$event->items = iterator_to_array($solrResult->getIterator());
$event->count = $solrResult->getNumFound();
$event->setCustomPaginationParameter('result', $solrResult);
$event->stopPropagation();
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 0], /* triggers before a standard array subscriber*/
];
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class ArraySubscriber implements EventSubscriberInterface
{
/**
* @var string the field used to sort current object array list
*/
private string $currentSortingField;
/**
* @var string the sorting direction
*/
private string $sortDirection;
private ?PropertyAccessorInterface $propertyAccessor;
private Request $request;
public function __construct(Request $request = null, PropertyAccessorInterface $accessor = null)
{
if (!$accessor && class_exists(PropertyAccess::class)) {
$accessor = PropertyAccess::createPropertyAccessorBuilder()->enableMagicCall()->getPropertyAccessor();
}
$this->propertyAccessor = $accessor;
// check needed because $request must be nullable, being the second parameter (with the first one nullable)
if (null === $request) {
throw new \InvalidArgumentException('Request must be initialized.');
}
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
if (!is_array($event->target) || null === $sortField || !$this->request->query->has($sortField)) {
return;
}
$event->setCustomPaginationParameter('sorted', true);
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($this->request->query->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot sort by: [{$this->request->query->get($sortField)}] this field is not in allow list.");
}
$sortFunction = $event->options['sortFunction'] ?? [$this, 'proxySortFunction'];
$sortField = $this->request->query->get($sortField);
// compatibility layer
if ($sortField[0] === '.') {
$sortField = substr($sortField, 1);
}
call_user_func_array($sortFunction, [
&$event->target,
$sortField,
$this->getSortDirection($event->options),
]);
}
private function getSortDirection(array $options): string
{
if (!$this->request->query->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) {
return 'desc';
}
$direction = $this->request->query->get($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]);
if (strtolower($direction) === 'asc') {
return 'asc';
}
return 'desc';
}
private function proxySortFunction(&$target, $sortField, $sortDirection): bool
{
$this->currentSortingField = $sortField;
$this->sortDirection = $sortDirection;
return usort($target, [$this, 'sortFunction']);
}
/**
* @param mixed $object1 first object to compare
* @param mixed $object2 second object to compare
*
* @return int
*/
private function sortFunction($object1, $object2): int
{
if (null === $this->propertyAccessor) {
throw new \UnexpectedValueException('You need symfony/property-access component to use this sorting function');
}
if (!$this->propertyAccessor->isReadable($object1, $this->currentSortingField) || !$this->propertyAccessor->isReadable($object2, $this->currentSortingField)) {
return 0;
}
try {
$fieldValue1 = $this->propertyAccessor->getValue($object1, $this->currentSortingField);
} catch (UnexpectedTypeException $e) {
return -1 * $this->getSortCoefficient();
}
try {
$fieldValue2 = $this->propertyAccessor->getValue($object2, $this->currentSortingField);
} catch (UnexpectedTypeException $e) {
return 1 * $this->getSortCoefficient();
}
if (is_string($fieldValue1)) {
$fieldValue1 = mb_strtolower($fieldValue1);
}
if (is_string($fieldValue2)) {
$fieldValue2 = mb_strtolower($fieldValue2);
}
if ($fieldValue1 === $fieldValue2) {
return 0;
}
return ($fieldValue1 > $fieldValue2 ? 1 : -1) * $this->getSortCoefficient();
}
private function getSortCoefficient(): int
{
return $this->sortDirection === 'asc' ? 1 : -1;
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1],
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable\Doctrine\ODM\MongoDB;
use Doctrine\ODM\MongoDB\Query\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class QuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
if ($event->target instanceof Query) {
$event->setCustomPaginationParameter('sorted', true);
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
$sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME];
if (null !== $sortField && $this->request->query->has($sortField)) {
$field = $this->request->query->get($sortField);
$dir = null !== $sortDir && strtolower($this->request->query->get($sortDir)) === 'asc' ? 1 : -1;
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
if (!in_array($field, $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot sort by: [{$field}] this field is not in allow list.");
}
}
static $reflectionProperty;
if (is_null($reflectionProperty)) {
$reflectionClass = new \ReflectionClass(Query::class);
$reflectionProperty = $reflectionClass->getProperty('query');
$reflectionProperty->setAccessible(true);
}
$queryOptions = $reflectionProperty->getValue($event->target);
// handle multi sort
$sortFields = explode('+', $field);
$sortOption = [];
foreach ($sortFields as $sortField) {
$sortOption[$sortField] = $dir;
}
$queryOptions['sort'] = $sortOption;
$reflectionProperty->setValue($event->target, $queryOptions);
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1],
];
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable\Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\OrderByClause,
Doctrine\ORM\Query\AST\OrderByItem,
Doctrine\ORM\Query\AST\PathExpression,
Doctrine\ORM\Query\AST\SelectStatement,
Doctrine\ORM\Query\TreeWalkerAdapter;
/**
* OrderBy Query TreeWalker for Sortable functionality
* in doctrine paginator
*/
class OrderByWalker extends TreeWalkerAdapter
{
/**
* Sort key alias hint name
*/
public const HINT_PAGINATOR_SORT_ALIAS = 'knp_paginator.sort.alias';
/**
* Sort key field hint name
*/
public const HINT_PAGINATOR_SORT_FIELD = 'knp_paginator.sort.field';
/**
* Sort direction hint name
*/
public const HINT_PAGINATOR_SORT_DIRECTION = 'knp_paginator.sort.direction';
/**
* Walks down a SelectStatement AST node, modifying it to
* sort the query like requested by url
*/
public function walkSelectStatement(SelectStatement $AST): string
{
$query = $this->_getQuery();
$fields = (array)$query->getHint(self::HINT_PAGINATOR_SORT_FIELD);
$aliases = (array)$query->getHint(self::HINT_PAGINATOR_SORT_ALIAS);
$components = $this->_getQueryComponents();
foreach ($fields as $index => $field) {
if (!$field) {
continue;
}
$alias = $aliases[$index];
if ($alias !== false) {
if (!array_key_exists($alias, $components)) {
throw new \UnexpectedValueException("There is no component aliased by [{$alias}] in the given Query");
}
$meta = $components[$alias];
if (!$meta['metadata']->hasField($field)) {
throw new \UnexpectedValueException("There is no such field [{$field}] in the given Query component, aliased by [$alias]");
}
} else {
if (!array_key_exists($field, $components)) {
throw new \UnexpectedValueException("There is no component field [{$field}] in the given Query");
}
}
$direction = $query->getHint(self::HINT_PAGINATOR_SORT_DIRECTION);
if ($alias !== false) {
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field);
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
} else {
$pathExpression = $field;
}
$orderByItem = new OrderByItem($pathExpression);
$orderByItem->type = $direction;
if ($AST->orderByClause) {
$set = false;
foreach ($AST->orderByClause->orderByItems as $item) {
if ($item->expression instanceof PathExpression) {
if ($item->expression->identificationVariable === $alias && $item->expression->field === $field) {
$item->type = $direction;
$set = true;
break;
}
}
}
if (!$set) {
array_unshift($AST->orderByClause->orderByItems, $orderByItem);
}
} else {
$AST->orderByClause = new OrderByClause([$orderByItem]);
}
}
return '';
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable\Doctrine\ORM;
use Doctrine\ORM\Query;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\Query\Helper as QueryHelper;
use Knp\Component\Pager\Event\Subscriber\Sortable\Doctrine\ORM\Query\OrderByWalker;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class QuerySubscriber implements EventSubscriberInterface
{
/**
* @var Request
*/
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
if ($event->target instanceof Query) {
$event->setCustomPaginationParameter('sorted', true);
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
$sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME];
if (null !== $sortField && $this->request->query->has($sortField)) {
$dir = null !== $sortDir && $this->request->query->has($sortDir) && strtolower($this->request->query->get($sortDir)) === 'asc' ? 'asc' : 'desc';
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
if (!in_array($this->request->query->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot sort by: [{$this->request->query->get($sortField)}] this field is not in allow list.");
}
}
$sortFieldParameterNames = $this->request->query->get($sortField);
$fields = [];
$aliases = [];
if (!is_string($sortFieldParameterNames)) {
throw new \UnexpectedValueException('Cannot sort with array parameter.');
}
foreach (explode('+', $sortFieldParameterNames) as $sortFieldParameterName) {
$parts = explode('.', $sortFieldParameterName, 2);
// We have to prepend the field. Otherwise OrderByWalker will add
// the order-by items in the wrong order
array_unshift($fields, end($parts));
array_unshift($aliases, 2 <= count($parts) ? reset($parts) : false);
}
$event->target
->setHint(OrderByWalker::HINT_PAGINATOR_SORT_DIRECTION, $dir)
->setHint(OrderByWalker::HINT_PAGINATOR_SORT_FIELD, $fields)
->setHint(OrderByWalker::HINT_PAGINATOR_SORT_ALIAS, $aliases)
;
QueryHelper::addCustomTreeWalker($event->target, OrderByWalker::class);
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1],
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable;
use Elastica\Query;
use Elastica\SearchableInterface;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class ElasticaQuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
if (is_array($event->target) && 2 === count($event->target) && reset($event->target) instanceof SearchableInterface && end($event->target) instanceof Query) {
[$searchable, $query] = $event->target;
$event->setCustomPaginationParameter('sorted', true);
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
$sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME];
if (null !== $sortField && $this->request->query->has($sortField)) {
$field = $this->request->query->get($sortField);
$dir = null !== $sortDir && $this->request->query->has($sortDir) && strtolower($this->request->query->get($sortDir)) === 'asc' ? 'asc' : 'desc';
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($field, $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException(sprintf('Cannot sort by: [%s] this field is not in allow list.', $field));
}
$query->setSort([
$field => ['order' => $dir],
]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1],
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class PropelQuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
$query = $event->target;
if ($query instanceof \ModelCriteria) {
$event->setCustomPaginationParameter('sorted', true);
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
$sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME];
if (null !== $sortField && $this->request->query->has($sortField)) {
$part = $this->request->query->get($sortField);
$direction = (null !== $sortDir && $this->request->query->has($sortDir) && strtolower($this->request->query->get($sortDir)) === 'asc')
? 'asc' : 'desc';
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
if (!in_array($this->request->query->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot sort by: [{$this->request->query->get($sortField)}] this field is not in allow list.");
}
}
$query->orderBy($part, $direction);
}
}
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1],
];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable;
use Knp\Component\Pager\Event\ItemsEvent;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Solarium query sorting
*
* @author Marek Kalnik <marekk@theodo.fr>
*/
class SolariumQuerySubscriber implements EventSubscriberInterface
{
private Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function items(ItemsEvent $event): void
{
// Check if the result has already been sorted by an other sort subscriber
$customPaginationParameters = $event->getCustomPaginationParameters();
if (!empty($customPaginationParameters['sorted']) ) {
return;
}
if (is_array($event->target) && 2 === count($event->target)) {
$values = array_values($event->target);
[$client, $query] = $values;
if ($client instanceof \Solarium\Client && $query instanceof \Solarium\QueryType\Select\Query\Query) {
$event->setCustomPaginationParameter('sorted', true);
$sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME];
if (null !== $sortField && $this->request->query->has($sortField)) {
if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
if (!in_array($this->request->query->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) {
throw new \UnexpectedValueException("Cannot sort by: [{$this->request->query->get($sortField)}] this field is not in allow list.");
}
}
$query->addSort($this->request->query->get($sortField), $this->getSortDirection($event));
}
}
}
}
public static function getSubscribedEvents(): array
{
return [
// trigger before the pagination subscriber
'knp_pager.items' => ['items', 1],
];
}
private function getSortDirection($event): string
{
$sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME];
return null !== $sortDir && $this->request->query->has($sortDir) &&
strtolower($this->request->query->get($sortDir)) === 'asc' ? 'asc' : 'desc';
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Knp\Component\Pager\Event\Subscriber\Sortable;
use Knp\Component\Pager\Event\BeforeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SortableSubscriber implements EventSubscriberInterface
{
/**
* Lazy-load state tracker
*/
private bool $isLoaded = false;
public function before(BeforeEvent $event): void
{
// Do not lazy-load more than once
if ($this->isLoaded) {
return;
}
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = $event->getEventDispatcher();
// hook all standard sortable subscribers
$request = $event->getRequest();
$dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber($request));
$dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QuerySubscriber($request));
$dispatcher->addSubscriber(new ElasticaQuerySubscriber($request));
$dispatcher->addSubscriber(new PropelQuerySubscriber($request));
$dispatcher->addSubscriber(new SolariumQuerySubscriber($request));
$dispatcher->addSubscriber(new ArraySubscriber($request));
$this->isLoaded = true;
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.before' => ['before', 1],
];
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Knp\Component\Pager\Exception;
use OutOfRangeException;
final class PageLimitInvalidException extends OutOfRangeException
{
public static function create(int $limit): self
{
return new self(
sprintf('Invalid page limit. Limit: %d: $limit must be positive non-zero integer', $limit)
);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Knp\Component\Pager\Exception;
use OutOfRangeException;
final class PageNumberInvalidException extends OutOfRangeException
{
public static function create(int $page): self
{
return new self(
sprintf('Invalid page number. Page: %d: $page must be positive non-zero integer', $page)
);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Knp\Component\Pager\Exception;
use OutOfRangeException;
use Throwable;
class PageNumberOutOfRangeException extends OutOfRangeException
{
private int $maxPageNumber;
public function __construct(?string $message, int $maxPageNumber, ?Throwable $previousException = null)
{
parent::__construct($message, 0, $previousException);
$this->maxPageNumber = $maxPageNumber;
}
public function getMaxPageNumber(): int
{
return $this->maxPageNumber;
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace Knp\Component\Pager\Pagination;
use Iterator;
abstract class AbstractPagination implements Iterator, PaginationInterface
{
protected ?int $currentPageNumber = null;
protected ?int $numItemsPerPage = null;
protected iterable $items = [];
protected ?int $totalCount = null;
protected ?array $paginatorOptions = null;
protected ?array $customParameters = null;
public function rewind(): void
{
reset($this->items);
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->items);
}
/**
* @return bool|float|int|string|null
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->items);
}
public function next(): void
{
next($this->items);
}
public function valid(): bool
{
return key($this->items) !== null;
}
public function count(): int
{
return count($this->items);
}
public function setCustomParameters(array $parameters): void
{
$this->customParameters = $parameters;
}
/**
* @return mixed|null
*/
public function getCustomParameter(string $name)
{
return $this->customParameters[$name] ?? null;
}
public function setCurrentPageNumber(int $pageNumber): void
{
$this->currentPageNumber = $pageNumber;
}
public function getCurrentPageNumber(): int
{
return $this->currentPageNumber;
}
public function setItemNumberPerPage(int $numItemsPerPage): void
{
$this->numItemsPerPage = $numItemsPerPage;
}
public function getItemNumberPerPage(): int
{
return $this->numItemsPerPage;
}
public function setTotalItemCount(int $numTotal): void
{
$this->totalCount = $numTotal;
}
public function getTotalItemCount(): int
{
return $this->totalCount;
}
public function setPaginatorOptions(array $options): void
{
$this->paginatorOptions = $options;
}
/**
* @return mixed|null
*/
public function getPaginatorOption(string $name)
{
return $this->paginatorOptions[$name] ?? null;
}
public function setItems(iterable $items): void
{
$this->items = $items;
}
public function getItems(): iterable
{
return $this->items;
}
/**
* @param string|int|float|bool|null $offset
*/
public function offsetExists($offset): bool
{
if ($this->items instanceof \ArrayIterator) {
return array_key_exists($offset, iterator_to_array($this->items));
}
return array_key_exists($offset, $this->items);
}
/**
* @param string|int|float|bool|null $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->items[$offset];
}
/**
* @param string|int|float|bool|null $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
if (null === $offset) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
/**
* @param string|int|float|bool|null $offset
*/
public function offsetUnset($offset): void
{
unset($this->items[$offset]);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Knp\Component\Pager\Pagination;
use ArrayAccess, Countable, Traversable;
/**
* Pagination interface strictly defines
* the methods - paginator will use to populate the
* pagination data
*/
interface PaginationInterface extends Countable, Traversable, ArrayAccess
{
public function setCurrentPageNumber(int $pageNumber): void;
/**
* Get currently used page number
*/
public function getCurrentPageNumber(): int;
public function setItemNumberPerPage(int $numItemsPerPage): void;
/**
* Get number of items per page
*/
public function getItemNumberPerPage(): int;
public function setTotalItemCount(int $numTotal): void;
/**
* Get total item number available
*/
public function getTotalItemCount(): int;
public function setItems(iterable $items): void;
/**
* Get current items
*/
public function getItems(): iterable;
/**
* @param array $options
*/
public function setPaginatorOptions(array $options): void;
/**
* Get pagination alias
*
* @return mixed
*/
public function getPaginatorOption(string $name);
/**
* @param array $parameters
*/
public function setCustomParameters(array $parameters): void;
/**
* Return custom parameter
*
* @return mixed
*/
public function getCustomParameter(string $name);
}

View File

@ -0,0 +1,94 @@
<?php
namespace Knp\Component\Pager\Pagination;
use Closure;
/**
* @todo: find a way to avoid exposing private member setters
*
* Sliding pagination
*/
final class SlidingPagination extends AbstractPagination
{
/**
* Pagination page range
*/
private int $range = 5;
/**
* Closure which is executed to render pagination
*/
public ?Closure $renderer = null;
public function setPageRange(int $range): void
{
$this->range = abs($range);
}
/**
* Renders the pagination
*/
public function __toString(): string
{
$data = $this->getPaginationData();
$output = 'override in order to render a template';
if ($this->renderer instanceof Closure) {
$output = call_user_func($this->renderer, $data);
}
return $output;
}
public function getPaginationData(): array
{
$pageCount = (int) ceil($this->totalCount / $this->numItemsPerPage);
$current = $this->currentPageNumber;
if ($this->range > $pageCount) {
$this->range = $pageCount;
}
$delta = ceil($this->range / 2);
if ($current - $delta > $pageCount - $this->range) {
$pages = range($pageCount - $this->range + 1, $pageCount);
} else {
if ($current - $delta < 0) {
$delta = $current;
}
$offset = $current - $delta;
$pages = range($offset + 1, $offset + $this->range);
}
$viewData = [
'last' => $pageCount,
'current' => $current,
'numItemsPerPage' => $this->numItemsPerPage,
'first' => 1,
'pageCount' => $pageCount,
'totalCount' => $this->totalCount,
];
$viewData = array_merge($viewData, $this->paginatorOptions, $this->customParameters);
if ($current - 1 > 0) {
$viewData['previous'] = $current - 1;
}
if ($current + 1 <= $pageCount) {
$viewData['next'] = $current + 1;
}
$viewData['pagesInRange'] = $pages;
$viewData['firstPageInRange'] = min($pages);
$viewData['lastPageInRange'] = max($pages);
if ($this->getItems() !== null) {
$viewData['currentItemCount'] = $this->count();
$viewData['firstItemNumber'] = (($current - 1) * $this->numItemsPerPage) + 1;
$viewData['lastItemNumber'] = $viewData['firstItemNumber'] + $viewData['currentItemCount'] - 1;
}
return $viewData;
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace Knp\Component\Pager;
use Knp\Component\Pager\Exception\PageLimitInvalidException;
use Knp\Component\Pager\Exception\PageNumberInvalidException;
use Knp\Component\Pager\Exception\PageNumberOutOfRangeException;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Paginator uses event dispatcher to trigger pagination
* lifecycle events. Subscribers are expected to paginate
* wanted target and finally it generates pagination view
* which is only the result of paginator
*/
final class Paginator implements PaginatorInterface
{
private EventDispatcherInterface $eventDispatcher;
/**
* Default options of paginator
*
* @var array<string, scalar>
*/
private array $defaultOptions = [
self::PAGE_PARAMETER_NAME => 'page',
self::SORT_FIELD_PARAMETER_NAME => 'sort',
self::SORT_DIRECTION_PARAMETER_NAME => 'direction',
self::FILTER_FIELD_PARAMETER_NAME => 'filterParam',
self::FILTER_VALUE_PARAMETER_NAME => 'filterValue',
self::DISTINCT => true,
self::PAGE_OUT_OF_RANGE => self::PAGE_OUT_OF_RANGE_IGNORE,
self::DEFAULT_LIMIT => self::DEFAULT_LIMIT_VALUE,
];
private ?RequestStack $requestStack;
public function __construct(EventDispatcherInterface $eventDispatcher, RequestStack $requestStack = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->requestStack = $requestStack;
}
/**
* Override the default paginator options
* to be reused for paginations
*/
public function setDefaultPaginatorOptions(array $options): void
{
$this->defaultOptions = \array_merge($this->defaultOptions, $options);
}
public function paginate($target, int $page = 1, int $limit = null, array $options = []): PaginationInterface
{
if ($page <= 0) {
throw PageNumberInvalidException::create($page);
}
$limit = $limit ?? $this->defaultOptions[self::DEFAULT_LIMIT];
if ($limit <= 0) {
throw PageLimitInvalidException::create($limit);
}
$offset = ($page - 1) * $limit;
$options = \array_merge($this->defaultOptions, $options);
// normalize default sort field
if (isset($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]) && is_array($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME])) {
$options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME] = implode('+', $options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]);
}
$request = null === $this->requestStack ? Request::createFromGlobals() : $this->requestStack->getCurrentRequest();
// default sort field and direction are set based on options (if available)
if (isset($options[self::DEFAULT_SORT_FIELD_NAME]) && !$request->query->has($options[self::SORT_FIELD_PARAMETER_NAME])) {
$request->query->set($options[self::SORT_FIELD_PARAMETER_NAME], $options[self::DEFAULT_SORT_FIELD_NAME]);
if (!$request->query->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) {
$request->query->set($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME], $options[PaginatorInterface::DEFAULT_SORT_DIRECTION] ?? 'asc');
}
}
// before pagination start
$beforeEvent = new Event\BeforeEvent($this->eventDispatcher, $request);
$this->eventDispatcher->dispatch($beforeEvent, 'knp_pager.before');
// items
$itemsEvent = new Event\ItemsEvent($offset, $limit);
$itemsEvent->options = &$options;
$itemsEvent->target = &$target;
$this->eventDispatcher->dispatch($itemsEvent, 'knp_pager.items');
if (!$itemsEvent->isPropagationStopped()) {
throw new \RuntimeException('One of listeners must count and slice given target');
}
if ($page > ceil($itemsEvent->count / $limit)) {
$pageOutOfRangeOption = $options[PaginatorInterface::PAGE_OUT_OF_RANGE] ?? $this->defaultOptions[PaginatorInterface::PAGE_OUT_OF_RANGE];
if ($pageOutOfRangeOption === PaginatorInterface::PAGE_OUT_OF_RANGE_FIX && $itemsEvent->count > 0) {
// replace page number out of range with max page
return $this->paginate($target, (int) ceil($itemsEvent->count / $limit), $limit, $options);
}
if ($pageOutOfRangeOption === self::PAGE_OUT_OF_RANGE_THROW_EXCEPTION && $page > 1) {
throw new PageNumberOutOfRangeException(
sprintf('Page number: %d is out of range.', $page),
(int) ceil($itemsEvent->count / $limit)
);
}
}
// pagination initialization event
$paginationEvent = new Event\PaginationEvent;
$paginationEvent->target = &$target;
$paginationEvent->options = &$options;
$this->eventDispatcher->dispatch($paginationEvent, 'knp_pager.pagination');
if (!$paginationEvent->isPropagationStopped()) {
throw new \RuntimeException('One of listeners must create pagination view');
}
// pagination class can be different, with different rendering methods
$paginationView = $paginationEvent->getPagination();
$paginationView->setCustomParameters($itemsEvent->getCustomPaginationParameters());
$paginationView->setCurrentPageNumber($page);
$paginationView->setItemNumberPerPage($limit);
$paginationView->setTotalItemCount($itemsEvent->count);
$paginationView->setPaginatorOptions($options);
$paginationView->setItems($itemsEvent->items);
// after
$afterEvent = new Event\AfterEvent($paginationView);
$this->eventDispatcher->dispatch($afterEvent, 'knp_pager.after');
return $paginationView;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Knp\Component\Pager;
use Knp\Component\Pager\Pagination\PaginationInterface;
/**
* PaginatorInterface
*/
interface PaginatorInterface
{
public const DEFAULT_SORT_FIELD_NAME = 'defaultSortFieldName';
public const DEFAULT_SORT_DIRECTION = 'defaultSortDirection';
public const DEFAULT_FILTER_FIELDS = 'defaultFilterFields';
public const SORT_FIELD_PARAMETER_NAME = 'sortFieldParameterName';
public const SORT_FIELD_ALLOW_LIST = 'sortFieldAllowList';
public const SORT_DIRECTION_PARAMETER_NAME = 'sortDirectionParameterName';
public const PAGE_PARAMETER_NAME = 'pageParameterName';
public const FILTER_FIELD_PARAMETER_NAME = 'filterFieldParameterName';
public const FILTER_VALUE_PARAMETER_NAME = 'filterValueParameterName';
public const FILTER_FIELD_ALLOW_LIST = 'filterFieldAllowList';
public const DISTINCT = 'distinct';
public const PAGE_OUT_OF_RANGE = 'pageOutOfRange';
public const DEFAULT_LIMIT = 'defaultLimit';
public const ODM_QUERY_OPTIONS = 'odmQueryOptions';
public const PAGE_OUT_OF_RANGE_IGNORE = 'ignore'; // do nothing (default)
public const PAGE_OUT_OF_RANGE_FIX = 'fix'; // replace page number out of range with max page
public const PAGE_OUT_OF_RANGE_THROW_EXCEPTION = 'throwException'; // throw PageNumberOutOfRangeException
public const DEFAULT_LIMIT_VALUE = 10;
/**
* Paginates anything (depending on event listeners)
* into Pagination object, which is a view targeted
* pagination object (might be aggregated helper object)
* responsible for the pagination result representation
*
* @param mixed $target anything what needs to be paginated
* @param int $page page number, starting from 1
* @param int|null $limit number of items per page
* @param array $options less used options:
* bool $distinct default true for distinction of results
* string $alias pagination alias, default none
* array $sortFieldAllowList sortable allow list for target fields being paginated
*/
public function paginate($target, int $page = 1, int $limit = null, array $options = []): PaginationInterface;
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2011 Knplabs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,330 @@
# Intro to KnpPaginatorBundle
Friendly Symfony paginator to paginate everything
[![Build Status](https://github.com/KnpLabs/KnpPaginatorBundle/workflows/Build/badge.svg)](https://github.com/KnpLabs/KnpPaginatorBundle/actions)
Generally this bundle is based on [Knp Pager component][knp_component_pager]. This
component introduces a different way of pagination handling. You can read more about the
internal logic on the given documentation link.
[![knpbundles.com](http://knpbundles.com/KnpLabs/KnpPaginatorBundle/badge-short)](http://knpbundles.com/KnpLabs/KnpPaginatorBundle)
**Note:** Keep **knp-components** in sync with this bundle. If you want to use
older version of KnpPaginatorBundle - use **v3.0** or **v4.X** tags in the repository which is
suitable to paginate **ODM MongoDB** and **ORM 2.0** queries
## Latest updates
For notes about the latest changes please read [`CHANGELOG`](https://github.com/KnpLabs/KnpPaginatorBundle/blob/master/CHANGELOG.md),
for required changes in your code please read [`UPGRADE`](https://github.com/KnpLabs/KnpPaginatorBundle/blob/master/docs/upgrade.md)
chapter of the documentation.
## Requirements:
- Knp Pager component `>=2.0`.
- KnpPaginatorBundle's master is compatible with Symfony `>=4.4` versions.
- Twig `>=2.0` version is required if you use the Twig templating engine.
## Features:
- Does not require initializing specific adapters.
- Can be customized in any way needed, etc.: pagination view, event subscribers.
- Possibility to add custom filtering, sorting functionality depending on request parameters.
- Separation of concerns, paginator is responsible for generating the pagination view only,
pagination view - for representation purposes.
**Note:** using multiple paginators requires setting the **alias** in order to keep non
conflicting parameters.
## More detailed documentation:
- Creating [custom pagination subscribers][doc_custom_pagination_subscriber]
- [Extending pagination](#) class (todo, may require some refactoring)
- [Customizing view][doc_templates] templates and arguments
## Installation and configuration:
### Pretty simple with [Composer](http://packagist.org), run
```sh
composer require knplabs/knp-paginator-bundle
```
### Add PaginatorBundle to your application kernel
If you don't use flex (you should), you need to manually enable bundle:
```php
// app/AppKernel.php
public function registerBundles()
{
return [
// ...
new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
// ...
];
}
```
<a name="configuration"></a>
### Configuration example
You can configure default query parameter names and templates
#### YAML:
```yaml
knp_paginator:
page_range: 5 # number of links shown in the pagination menu (e.g: you have 10 pages, a page_range of 3, on the 5th page you'll see links to page 4, 5, 6)
default_options:
page_name: page # page query parameter name
sort_field_name: sort # sort field query parameter name
sort_direction_name: direction # sort direction query parameter name
distinct: true # ensure distinct results, useful when ORM queries are using GROUP BY statements
filter_field_name: filterField # filter field query parameter name
filter_value_name: filterValue # filter value query parameter name
template:
pagination: '@KnpPaginator/Pagination/sliding.html.twig' # sliding pagination controls template
sortable: '@KnpPaginator/Pagination/sortable_link.html.twig' # sort link template
filtration: '@KnpPaginator/Pagination/filtration.html.twig' # filters template
```
#### PHP:
```php
// config/packages/paginator.php
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $configurator): void
{
$configurator->extension('knp_paginator', [
'page_range' => 5, // number of links shown in the pagination menu (e.g: you have 10 pages, a page_range of 3, on the 5th page you'll see links
'default_options' => [
'page_name' => 'page', // page query parameter name
'sort_field_name' => 'sort', // sort field query parameter name
'sort_direction_name' => 'direction', // sort direction query parameter name
'distinct' => true, // ensure distinct results, useful when ORM queries are using GROUP BY statements
'filter_field_name' => 'filterField', // filter field query parameter name
'filter_value_name' => 'filterValue' // filter value query parameter name
],
'template' => [
'pagination' => '@KnpPaginator/Pagination/sliding.html.twig', // sliding pagination controls template
'sortable' => '@KnpPaginator/Pagination/sortable_link.html.twig', // sort link template
'filtration' => '@KnpPaginator/Pagination/filtration.html.twig' // filters template
]
]);
};
```
#### Additional pagination templates
That could be used out of the box in `knp_paginator.template.pagination` key:
* `@KnpPaginator/Pagination/sliding.html.twig` (by default)
* `@KnpPaginator/Pagination/bootstrap_v5_pagination.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v4_pagination.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v3_pagination.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_pagination.html.twig`
* `@KnpPaginator/Pagination/foundation_v6_pagination.html.twig`
* `@KnpPaginator/Pagination/foundation_v5_pagination.html.twig`
* `@KnpPaginator/Pagination/bulma_pagination.html.twig`
* `@KnpPaginator/Pagination/semantic_ui_pagination.html.twig`
* `@KnpPaginator/Pagination/materialize_pagination.html.twig`
* `@KnpPaginator/Pagination/tailwindcss_pagination.html.twig`
* `@KnpPaginator/Pagination/uikit_v3_pagination.html.twig`
#### Additional sortable templates
That could be used out of the box in `knp_paginator.template.sortable` key:
* `@KnpPaginator/Pagination/sortable_link.html.twig` (by default)
* `@KnpPaginator/Pagination/bootstrap_v5_bi_sortable_link.html.twig`
* `@KnpPaginator/Pagination/bootstrap_v5_fa_sortable_link.html.twig`
* `@KnpPaginator/Pagination/bootstrap_v5_md_sortable_link.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v3_sortable_link.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v4_font_awesome_sortable_link.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v4_material_design_icons_sortable_link.html.twig`
* `@KnpPaginator/Pagination/semantic_ui_sortable_link.html.twig`
* `@KnpPaginator/Pagination/uikit_v3_sortable.html.twig`
#### Additional filtration templates
That could be used out of the box in `knp_paginator.template.filtration` key:
* `@KnpPaginator/Pagination/filtration.html.twig` (by default)
* `@KnpPaginator/Pagination/bootstrap_v5_filtration.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v4_filtration.html.twig`
## Usage examples:
### Controller
Currently paginator can paginate:
- `array`
- `Doctrine\ORM\Query`
- `Doctrine\ORM\QueryBuilder`
- `Doctrine\ODM\MongoDB\Query\Query`
- `Doctrine\ODM\MongoDB\Query\Builder`
- `Doctrine\ODM\PHPCR\Query\Query`
- `Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder`
- `Doctrine\Common\Collection\ArrayCollection` - any Doctrine relation collection including
- `ModelCriteria` - Propel ORM query
- array with `Solarium_Client` and `Solarium_Query_Select` as elements
```php
// App\Controller\ArticleController.php
public function listAction(EntityManagerInterface $em, PaginatorInterface $paginator, Request $request)
{
$dql = "SELECT a FROM AcmeMainBundle:Article a";
$query = $em->createQuery($dql);
$pagination = $paginator->paginate(
$query, /* query NOT result */
$request->query->getInt('page', 1), /*page number*/
10 /*limit per page*/
);
// parameters to template
return $this->render('article/list.html.twig', ['pagination' => $pagination]);
}
```
### View
```twig
{# total items count #}
<div class="count">
{{ pagination.getTotalItemCount }}
</div>
<table>
<tr>
{# sorting of properties based on query components #}
<th>{{ knp_pagination_sortable(pagination, 'Id', 'a.id') }}</th>
<th{% if pagination.isSorted('a.Title') %} class="sorted"{% endif %}>
{{ knp_pagination_sortable(pagination, 'Title', 'a.title') }}
</th>
<th{% if pagination.isSorted(['a.date', 'a.time']) %} class="sorted"{% endif %}>
{{ knp_pagination_sortable(pagination, 'Release', ['a.date', 'a.time']) }}
</th>
</tr>
{# table body #}
{% for article in pagination %}
<tr {% if loop.index is odd %}class="color"{% endif %}>
<td>{{ article.id }}</td>
<td>{{ article.title }}</td>
<td>{{ article.date | date('Y-m-d') }}, {{ article.time | date('H:i:s') }}</td>
</tr>
{% endfor %}
</table>
{# display navigation #}
<div class="navigation">
{{ knp_pagination_render(pagination) }}
</div>
```
### Translation in view
For translating the following text:
* `%foo% name` with translation key `table_header_name`. The translation is in the domain `messages`.
* `{0} No author|{1} Author|[2,Inf] Authors` with translation key `table_header_author`. The translation is in the domain `messages`.
translationCount and translationParameters can be combined.
```twig
<table>
<tr>
{# sorting of properties based on query components #}
<th>{{ knp_pagination_sortable(pagination, 'Id'|trans({foo:'bar'},'messages'), 'a.id' )|raw }}</th>
<th{% if pagination.isSorted('a.Title') %} class="sorted"{% endif %}>{{ knp_pagination_sortable(pagination, 'Title', 'a.title')|raw }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Author'|trans({}, 'messages'), 'a.author' )|raw }}</th>
</tr>
<!-- Content of the table -->
</table>
```
### Adding translation files
You can also override translations by creating a translation file in the following name format: `domain.locale.format`.
So, to create a translation file for this bundle you need to create for instance `KnpPaginatorBundle.tr.yaml` file under `project_root/translations/`
and add your translations there:
```yaml
label_previous: "Önceki"
label_next: "Sonraki"
filter_searchword: "Arama kelimesi"
```
If you set default translation for configuration accordingly:
```yaml
framework:
default_locale: tr
```
Symfony will pick it automatically.
### Dependency Injection
You can automatically inject a paginator service into another service by using the `knp_paginator.injectable` DIC tag.
The tag takes one optional argument `paginator`, which is the ID of the paginator service that should be injected.
It defaults to `knp_paginator`.
The class that receives the KnpPaginator service must implement `Knp\Bundle\PaginatorBundle\Definition\PaginatorAwareInterface`.
If you're too lazy you can also just extend the `Knp\Bundle\PaginatorBundle\Definition\PaginatorAware` base class.
> **⚠ Warning** using `PaginatorAwareInterface` is discouraged, and could be removed in a future version. You should not rely on setter
> injection, but only on proper constructor injection. Using Symfony built-in autowiring mechanism is the suggested way to go.
#### Lazy service
The `knp_paginator` service will be created lazily if the package `symfony/proxy-manager-bridge` is installed.
For more information about lazy services, consult the [Symfony documentation on dependency injection](https://symfony.com/doc/current/service_container/lazy_services.html).
###### XML configuration example
```xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="my_bundle.paginator_aware.class">MyBundle\Repository\PaginatorAwareRepository</parameter>
</parameters>
<services>
<service id="my_bundle.paginator_aware" class="my_bundle.paginator_aware.class">
<tag name="knp_paginator.injectable" paginator="knp_paginator" />
</service>
</services>
</container>
```
[knp_component_pager]: https://github.com/KnpLabs/knp-components/blob/master/docs/pager/intro.md "Knp Pager component introduction"
[doc_custom_pagination_subscriber]: https://github.com/KnpLabs/KnpPaginatorBundle/tree/master/docs/custom_pagination_subscribers.md "Custom pagination subscribers"
[doc_templates]: https://github.com/KnpLabs/KnpPaginatorBundle/tree/master/docs/templates.md "Customizing Pagination templates"
## Troubleshooting
- Make sure the translator is activated in your Symfony config:
```yaml
framework:
translator: { fallbacks: ['%locale%'] }
```
- If your locale is not available, create your own translation file in
`translations/KnpPaginatorBundle.en.yml` (substitute "en" for your own language code if needed).
Then add these lines:
```yaml
label_next: Next
label_previous: Previous
```
## Maintainers
Please read [this post](https://knplabs.com/en/blog/news-for-our-foss-projects-maintenance) first.
This library is maintained by the following people (alphabetically sorted) :
- @garak
- @polc

View File

@ -0,0 +1,52 @@
{
"name": "knplabs/knp-paginator-bundle",
"type": "symfony-bundle",
"description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features",
"keywords": ["pager", "paginator", "pagination", "symfony", "bundle", "knp", "knplabs"],
"homepage": "http://github.com/KnpLabs/KnpPaginatorBundle",
"license": "MIT",
"authors": [
{
"name": "KnpLabs Team",
"homepage": "http://knplabs.com"
},
{
"name": "Symfony Community",
"homepage": "http://github.com/KnpLabs/KnpPaginatorBundle/contributors"
}
],
"require": {
"php": "^7.3 || ^8.0",
"knplabs/knp-components": "^2.4 || ^3.0",
"symfony/config": "^4.4 || ^5.4 || ^6.0",
"symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0",
"symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0",
"symfony/http-foundation": "^4.4 || ^5.4 || ^6.0",
"symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
"symfony/routing": "^4.4 || ^5.4 || ^6.0",
"symfony/translation": "^4.4 || ^5.4 || ^6.0",
"twig/twig": "^2.0 || ^3.0"
},
"require-dev": {
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^8.5 || ^9.5",
"symfony/expression-language": "^4.4 || ^5.4 || ^6.0",
"symfony/templating": "^4.4 || ^5.4 || ^6.0"
},
"autoload": {
"psr-4": {
"Knp\\Bundle\\PaginatorBundle\\": "src"
}
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
}
}

View File

@ -0,0 +1,50 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="knp_paginator" class="Knp\Component\Pager\Paginator" public="true" lazy="true">
<argument type="service" id="event_dispatcher" />
<argument type="service" id="request_stack" />
<tag name="proxy" interface="Knp\Component\Pager\PaginatorInterface" />
</service>
<service id="Knp\Component\Pager\PaginatorInterface" alias="knp_paginator" />
<service id="knp_paginator.subscriber.paginate" class="Knp\Component\Pager\Event\Subscriber\Paginate\PaginationSubscriber">
<tag name="kernel.event_subscriber" />
</service>
<service id="knp_paginator.subscriber.sortable" class="Knp\Component\Pager\Event\Subscriber\Sortable\SortableSubscriber">
<tag name="kernel.event_subscriber" />
</service>
<service id="knp_paginator.subscriber.filtration" class="Knp\Component\Pager\Event\Subscriber\Filtration\FiltrationSubscriber">
<tag name="kernel.event_subscriber" />
</service>
<service id="knp_paginator.subscriber.sliding_pagination" class="Knp\Bundle\PaginatorBundle\Subscriber\SlidingPaginationSubscriber">
<argument type="collection">
<argument key="defaultPaginationTemplate">%knp_paginator.template.pagination%</argument>
<argument key="defaultSortableTemplate">%knp_paginator.template.sortable%</argument>
<argument key="defaultFiltrationTemplate">%knp_paginator.template.filtration%</argument>
<argument key="defaultPageRange">%knp_paginator.page_range%</argument>
<argument key="defaultPageLimit">%knp_paginator.page_limit%</argument>
</argument>
<tag name="kernel.event_subscriber" />
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
</service>
<service id="knp_paginator.helper.processor" class="Knp\Bundle\PaginatorBundle\Helper\Processor">
<argument type="service" id="router" />
<argument type="service" id="translator" />
</service>
<service id="knp_paginator.twig.extension.pagination" class="Knp\Bundle\PaginatorBundle\Twig\Extension\PaginationExtension">
<argument type="service" id="knp_paginator.helper.processor" />
<tag name="twig.extension" />
</service>
</services>
</container>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="knp_paginator.templating.helper.pagination.class">Knp\Bundle\PaginatorBundle\Templating\PaginationHelper</parameter>
</parameters>
<services>
<service id="knp_paginator.templating.helper.pagination" class="%knp_paginator.templating.helper.pagination.class%">
<argument type="service" id="knp_paginator.helper.processor" />
<argument type="service" id="templating.engine.php" />
<tag name="templating.helper" alias="knp_pagination" />
</service>
</services>
</container>

View File

@ -0,0 +1,145 @@
# Creating custom subscriber
Let's say we want to paginate a directory content, which might be quite interesting.
And when we have such a handy **Finder** component in symfony, it's easily achievable.
## Prepare environment
I will assume we you just installed [Symfony demo](https://github.com/symfony/demo)
and you install [KnpPaginatorBundle](https://github.com/knplabs/KnpPaginatorBundle).
Follow the installation guide on these repositories, it's very easy to set up.
## Create subscriber
Next, let's extend our subscriber.
Create a file named **src/Subscriber/PaginateDirectorySubscriber.php**
``` php
<?php
// file: src/Subscriber/PaginateDirectorySubscriber.php
// requires Symfony\Component\Finder\Finder
namespace App\Subscriber;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Finder\Finder;
final class PaginateDirectorySubscriber implements EventSubscriberInterface
{
public function items(ItemsEvent $event): void
{
if (!is_string($event->target) || !is_dir($event->target)) {
return;
}
$finder = new Finder();
$finder
->files()
->depth('< 4') // 3 levels
->in($event->target)
;
$iterator = $finder->getIterator();
$files = iterator_to_array($iterator);
$event->count = count($files);
$event->items = array_slice($files, $event->getOffset(), $event->getLimit());
$event->stopPropagation();
}
public static function getSubscribedEvents(): array
{
return [
'knp_pager.items' => ['items', 1/* increased priority to override any internal */]
];
}
}
```
Class above is the simple event subscriber, which listens to **knp_pager.items** event.
Creates a finder and looks in this directory for files. To be more specific it will look
for the **files** in the directory being paginated, max in 3 level depth.
## Register subscriber as service
Next we need to tell **knp_paginator** about our new fancy subscriber which we intend
to use in pagination. It is also very simple, add few line to your service config file
(usually **config/services.xml**)
``` xml
<?xml version="1.0" ?>
<!-- file: config/services.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<service id="acme.directory.subscriber" class="App\Subscriber\PaginateDirectorySubscriber">
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
```
## Controller action
Finally, we are done with configuration, now let's create actual controller action.
Modify controller: **src/Controller/DemoController.php**
And add the following action, which paginates the previous directory
``` php
<?php
use ...
/**
* @Route("/test", name="_demo_test")
*/
public function test(KnpPaginatorInterface $paginator, Request $request): Response
{
$pagination = $paginator->paginate(__DIR__.'/../', $request->query->getInt('page', 1), 10);
return $this->render('demo/test.html.twig', ['pagination' => $pagination]);
}
```
## Template
And the last thing is the template, create: **templates/demo/test.html.twig**
``` html
{% extends "layout.html.twig" %}
{% block title "My demo" %}
{% block content %}
<h1>Demo</h1>
<table>
<tr>
{# sorting of properties based on query components #}
<th>base name</th>
<th>path</th>
</tr>
{# table body #}
{% for file in pagination %}
<tr{% if loop.index is odd %} class="color"{% endif %}>
<td>{{ file.baseName }}</td>
<td>{{ file.path }}</td>
</tr>
{% endfor %}
</table>
{# display navigation #}
<div id="navigation">
{{ knp_pagination_render(pagination) }}
</div>
{% endblock %}
```
Do not forget to reload the cache: **php bin/console cache:clear**
You should find some files paginated if you open the url: **http://baseurl/index.php/demo/test**

View File

@ -0,0 +1,32 @@
# Manual counting
**Note:** this documentation concerns more advanced usage of ORM
query pagination. Paginator cannot support two **FROM** components
or composite identifiers, because it cannot predict the total count
in the database.
The solution to that is simple and direct, you can provide the **count**
manually through the hint on the query.
## Usage example
``` php
<?php
$paginator = new Paginator();
$count = $entityManager
->createQuery('SELECT COUNT(c) FROM Entity\CompositeKey c')
->getSingleScalarResult()
;
$query = $entityManager
->createQuery('SELECT c FROM Entity\CompositeKey c')
->setHint('knp_paginator.count', $count)
;
$pagination = $paginator->paginate($query, 1, 10, ['distinct' => false]);
```
Distinction in this case also cannot be determined by paginator. It will take direct result
of the query and limit with offset. In some cases you may need to use **GROUP BY**

View File

@ -0,0 +1,34 @@
# Configuring paginator
There's an easy way to configure paginator - just add a `knp_paginator` entry to your configuration and change default values.
Location of your configuration file depends on your Symfony versions: most common places are `config/packages/knp_paginator.yaml`
for recent versions of Symfony and `app/config.yml` for older versions. If you can't find a configuration file, you can create it.
## Default options
``` yaml
knp_paginator:
page_range: 5 # default page range used in pagination control
page_limit: 100 # page limit for pagination control; to disable set this field to ~ (null)
convert_exception: false # convert paginator exception (e.g. non-positive page and/or limit) into 404 error
default_options:
page_name: page # page query parameter name
sort_field_name: sort # sort field query parameter name; to disable sorting set this field to ~ (null)
sort_direction_name: direction # sort direction query parameter name
distinct: true # ensure distinct results, useful when ORM queries are using GROUP BY statements
page_out_of_range: ignore # if page number exceeds the last page. Options: 'fix'(return last page); 'throwException'
default_limit: 10 # default number of items per page
template:
pagination: @KnpPaginator/Pagination/sliding.html.twig # sliding pagination controls template
sortable: @KnpPaginator/Pagination/sortable_link.html.twig # sort link template
```
There are a few additional pagination templates, that could be used out of the box in `knp_paginator.template.pagination` key:
* `@KnpPaginator/Pagination/sliding.html.twig` (by default)
* `@KnpPaginator/Pagination/twitter_bootstrap_v4_pagination.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_v3_pagination.html.twig`
* `@KnpPaginator/Pagination/twitter_bootstrap_pagination.html.twig`
* `@KnpPaginator/Pagination/foundation_v6_pagination.html.twig`
* `@KnpPaginator/Pagination/foundation_v5_pagination.html.twig`
* `@KnpPaginator/Pagination/bulma_pagination.html.twig`

View File

@ -0,0 +1,216 @@
# Templates
This document will describe how pagination can be rendered, extended and used in
templates. For now there's only a sliding pagination supported, so all documentation
will reference it.
## Overriding default pagination template
There are few ways to override templates
### Configuration
This way it will override it globally for all default pagination rendering.
You can override templates in [configuration of
paginator](http://github.com/KnpLabs/KnpPaginatorBundle/blob/master/README.md#configuration)
Or place this parameter in **config/packages/knp_paginator.yaml**
knp_paginator.template.pagination: my_pagination.html.twig
Same for sorting link template:
knp_paginator.template.sortable: my_sortable.html.twig
### Directly in pagination
``` php
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate($target, $page);
$pagination->setTemplate('my_pagination.html.twig');
$pagination->setSortableTemplate('my_sortable.html.twig');
```
or in view
``` html
{% do pagination.setTemplate('my_pagination.html.twig') %}
```
### In render method
```twig
{{ knp_pagination_render(pagination, 'my_pagination.html.twig') }}
```
or by specifying path to your custom template that is located under your project's `templates` directory:
```twig
{{ knp_pagination_sortable(pagination, 'date', 'c.publishedAt', {}, {}, 'KnpPaginator/Pagination/bootstrap_v4_sortable_link.html.twig') }}
{{ knp_pagination_render(pagination, 'KnpPaginator/Pagination/bootstrap_v4_pagination.html.twig') }}
```
## Other useful parameters
By default when render method is triggered, pagination renders the template
with standard arguments provided:
- pagination parameters, like pages in range, current page and so on..
- route - which is used to generate page, sorting urls
- request query, which contains all GET request parameters
- extra pagination template parameters
Except from pagination parameters, others can be modified or adapted to some
use cases. Usually it's possible, you might need setting a route if default is not
matched correctly (because of rendering in sub requests). Or adding additional
query or view parameters.
### Setting a route and query parameters to use for pagination urls
``` php
<?php
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate($target, $page);
$pagination->setUsedRoute('blog_articles');
```
In case if route requires additional parameters
``` php
<?php
$pagination->setParam('category', 'news');
```
This would set additional query parameter
### Additional pagination template parameters
If you need custom parameters in pagination template, use:
``` php
<?php
// set an array of custom parameters
$pagination->setCustomParameters([
'align' => 'center', # center|right (for template: twitter_bootstrap_v4_pagination and foundation_v6_pagination)
'size' => 'large', # small|large (for template: twitter_bootstrap_v4_pagination)
'style' => 'bottom',
'span_class' => 'whatever',
]);
```
### You can also change the page range
Default page range is 5 pages in sliding pagination. Doing it in controller:
``` php
<?php
$pagination->setPageRange(7);
```
In template:
``` php
{% do pagination.setPageRange(7) %}
```
### Setting a page limit
The default page limit is unlimited, but if for some reason you want to limit
the number of pages you can do so. Doing it in controller:
``` php
<?php
$pagination->setPageLimit(25);
```
In template:
``` php
{% do pagination.setPageLimit(25) %}
```
## Choose the sorting direction
The `knp_pagination_sortable()` template automatically switches the sorting direction but sometimes you need to propose that your users select the sorting direction.
You can add an array at the end of `knp_pagination_sortable()` to choose the direction.
``` html
{{ knp_pagination_sortable(pagination, 'Title A-Z', 'a.title', {}, {'direction': 'asc'}) }}
{{ knp_pagination_sortable(pagination, 'Title Z-A', 'a.title', {}, {'direction': 'desc'}) }}
```
(Assuming you use the default configuration value of sort_direction_name)
<a name="query-parameters"></a>
## Query parameters
If you need to change query parameters for paginator or use multiple paginators for the same page.
You can customize these parameter names through [configuration](http://github.com/KnpLabs/KnpPaginatorBundle/blob/master/README.md#configuration)
or manually with paginator options.
``` php
<?php // controller
// will change "page" query parameter into "section" and sort direction "direction" into "dir"
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$query, // target to paginate
$this->get('request')->query->getInt('section', 1), // page parameter, now section
10, // limit per page
['pageParameterName' => 'section', 'sortDirectionParameterName' => 'dir']
);
```
Or even in Twig:
```jinja
{{ knp_pagination_render(
pagination,
'@KnpPaginator/Pagination/twitter_bootstrap_v4_pagination.html.twig',
{
'queryParam1': 'param1 value',
'queryParam2': 'param2 value'
},
{
'viewParam1': 'param1 value',
'viewParam2': 'param2 value'
},
) }}
```
## Filter your query
Only include this lines and enjoy the pagination :
``` html
{{ knp_pagination_filter(pagination, {
'entity.name': 'Name',
}) }}
```
## Customize rendering
### Bulma
You can configure the position, the size, and make the buttons rounded or not:
- `align`: `'left'`, `'center'`, or `'right'`. By default align is not modified
- `size`: `'small'`, `'medium'`, or `'large'`. By default, size is not modified
- `rounded`: `true` or `false`. By default it's `false`
In your controller:
```php
$pagination->setCustomParameters([
'align' => 'center',
'size' => 'large',
'rounded' => true,
]);
```
or in the view:
```twig
{{ knp_pagination_render(pagination, null, {}, {
'align': 'center',
'size': 'large',
'rounded': true,
}) }}
```

View File

@ -0,0 +1,34 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Definition;
use Knp\Component\Pager\PaginatorInterface;
/**
* This is a base class that can be extended if you're too lazy to implement PaginatorAwareInterface yourself.
*/
abstract class AbstractPaginatorAware implements PaginatorAwareInterface
{
/**
* @var PaginatorInterface
*/
private $paginator;
/**
* Sets the KnpPaginator instance.
*/
public function setPaginator(PaginatorInterface $paginator): PaginatorAwareInterface
{
$this->paginator = $paginator;
return $this;
}
/**
* Returns the KnpPaginator instance.
*/
public function getPaginator(): PaginatorInterface
{
return $this->paginator;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Definition;
@\trigger_error(sprintf('The %s class is deprecated since knplabs/knp-paginator-bundle 5.x. Use %s instead.', PaginatorAware::class, AbstractPaginatorAware::class), \E_USER_DEPRECATED);
/**
* Class PaginatorAware.
*
* This is a base class that can be extended if you're too lazy to implement PaginatorAwareInterface yourself.
*
* @deprecated since knplabs/knp-paginator-bundle 5.x
*/
abstract class PaginatorAware extends AbstractPaginatorAware
{
}

View File

@ -0,0 +1,19 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Definition;
use Knp\Component\Pager\PaginatorInterface;
/**
* Interface PaginatorAwareInterface.
*
* PaginatorAwareInterface can be implemented by classes that depend on a KnpPaginator service.
* You should avoid this solution: use autowiring instead.
*/
interface PaginatorAwareInterface
{
/**
* Sets the KnpPaginator instance.
*/
public function setPaginator(PaginatorInterface $paginator): self;
}

View File

@ -0,0 +1,58 @@
<?php
namespace Knp\Bundle\PaginatorBundle\DependencyInjection\Compiler;
use Knp\Bundle\PaginatorBundle\Definition\PaginatorAwareInterface;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class PaginatorAwarePass.
*
* This compiler scans for the 'knp_paginator.injectable' tag and injects the Paginator service.
*/
final class PaginatorAwarePass implements CompilerPassInterface
{
/**
* @var string
*/
public const PAGINATOR_AWARE_TAG = 'knp_paginator.injectable';
/**
* @var string
*/
public const PAGINATOR_AWARE_INTERFACE = PaginatorAwareInterface::class;
/**
* Populates all tagged services with the paginator service.
*
* @throws \InvalidArgumentException
* @throws InvalidDefinitionException
*/
public function process(ContainerBuilder $container): void
{
$defaultAttributes = ['paginator' => 'knp_paginator'];
foreach ($container->findTaggedServiceIds(self::PAGINATOR_AWARE_TAG) as $id => [$attributes]) {
$definition = $container->getDefinition($id);
if (null === $class = $definition->getClass()) {
throw new \InvalidArgumentException(\sprintf('Service "%s" not found.', $id));
}
/** @var class-string $class */
$refClass = new \ReflectionClass($class);
if (!$refClass->implementsInterface(self::PAGINATOR_AWARE_INTERFACE)) {
throw new \InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, self::PAGINATOR_AWARE_INTERFACE));
}
$attributes = \array_merge($defaultAttributes, $attributes);
if (!$container->has($attributes['paginator'])) {
throw new InvalidDefinitionException(\sprintf('Paginator service "%s" for tag "%s" on service "%s" could not be found.', $attributes['paginator'], self::PAGINATOR_AWARE_TAG, $id));
}
$definition->addMethodCall('setPaginator', [new Reference($attributes['paginator'])]);
$container->setDefinition($id, $definition);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Knp\Bundle\PaginatorBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
final class PaginatorConfigurationPass implements CompilerPassInterface
{
/**
* Populate the listener service ids.
*/
public function process(ContainerBuilder $container): void
{
// use main symfony dispatcher
if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) {
return;
}
$listeners = $container->findTaggedServiceIds('knp_paginator.listener');
$subscribers = $container->findTaggedServiceIds('knp_paginator.subscriber');
foreach ($listeners as $serviceId => $tags) {
@\trigger_error('Using "knp_paginator.listener" tag is deprecated, use "kernel.event_listener" instead.', \E_USER_DEPRECATED);
}
foreach ($subscribers as $serviceId => $tags) {
@\trigger_error('Using "knp_paginator.subscriber" tag is deprecated, use "kernel.event_subscriber" instead.', \E_USER_DEPRECATED);
}
if (\count($listeners) > 0 || \count($subscribers) > 0) {
$pass = new RegisterListenersPass('event_dispatcher', 'knp_paginator.listener', 'knp_paginator.subscriber');
$pass->process($container);
}
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Knp\Bundle\PaginatorBundle\DependencyInjection;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('knp_paginator');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->addDefaultsIfNotSet()
->children()
->arrayNode('default_options')
->addDefaultsIfNotSet()
->children()
->scalarNode('sort_field_name')->defaultValue('sort')->end()
->scalarNode('sort_direction_name')->defaultValue('direction')->end()
->scalarNode('filter_field_name')->defaultValue('filterField')->end()
->scalarNode('filter_value_name')->defaultValue('filterValue')->end()
->scalarNode('page_name')->defaultValue('page')->end()
->booleanNode('distinct')->defaultTrue()->end()
->scalarNode('page_out_of_range')->defaultValue(PaginatorInterface::PAGE_OUT_OF_RANGE_IGNORE)->end()
->scalarNode('default_limit')->defaultValue(PaginatorInterface::DEFAULT_LIMIT_VALUE)->end()
->end()
->end()
->arrayNode('template')
->addDefaultsIfNotSet()
->children()
->scalarNode('pagination')
->defaultValue('@KnpPaginator/Pagination/sliding.html.twig')
->end()
->scalarNode('filtration')
->defaultValue('@KnpPaginator/Pagination/filtration.html.twig')
->end()
->scalarNode('sortable')
->defaultValue('@KnpPaginator/Pagination/sortable_link.html.twig')
->end()
->end()
->end()
->scalarNode('page_range')
->defaultValue(5)
->end()
->integerNode('page_limit')
->defaultNull()
->end()
->booleanNode('convert_exception')
->defaultFalse()
->end()
->end()
;
return $treeBuilder;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Knp\Bundle\PaginatorBundle\DependencyInjection;
use Knp\Bundle\PaginatorBundle\EventListener\ExceptionListener;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
final class KnpPaginatorExtension extends Extension
{
/**
* Build the extension services.
*
* @param array<string, array<string, mixed>> $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$processor = new Processor();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
$loader->load('paginator.xml');
if ($container->hasParameter('templating.engines')) {
/** @var array<string> $engines */
$engines = $container->getParameter('templating.engines');
if (\in_array('php', $engines, true)) {
$loader->load('templating_php.xml');
}
}
$configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs);
$container->setParameter('knp_paginator.template.pagination', $config['template']['pagination']);
$container->setParameter('knp_paginator.template.filtration', $config['template']['filtration']);
$container->setParameter('knp_paginator.template.sortable', $config['template']['sortable']);
$container->setParameter('knp_paginator.page_range', $config['page_range']);
$container->setParameter('knp_paginator.page_limit', $config['page_limit']);
$paginatorDef = $container->getDefinition('knp_paginator');
$paginatorDef->addMethodCall('setDefaultPaginatorOptions', [[
'pageParameterName' => $config['default_options']['page_name'],
'sortFieldParameterName' => $config['default_options']['sort_field_name'],
'sortDirectionParameterName' => $config['default_options']['sort_direction_name'],
'filterFieldParameterName' => $config['default_options']['filter_field_name'],
'filterValueParameterName' => $config['default_options']['filter_value_name'],
'distinct' => $config['default_options']['distinct'],
'pageOutOfRange' => $config['default_options']['page_out_of_range'],
'defaultLimit' => $config['default_options']['default_limit'],
]]);
if ($config['convert_exception']) {
$definition = new Definition(ExceptionListener::class);
$definition->addTag('kernel.event_listener', ['event' => 'kernel.exception']);
$container->setDefinition(ExceptionListener::class, $definition);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Knp\Bundle\PaginatorBundle\EventListener;
use OutOfRangeException;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Intercept OutOfRangeException and throw http-related exceptions instead.
*/
final class ExceptionListener
{
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if ($exception instanceof OutOfRangeException) {
$event->setThrowable(new NotFoundHttpException('Not Found.', $exception));
}
}
}

View File

@ -0,0 +1,205 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Helper;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPaginationInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Pagination data processor.
*
* Common data processor for all templating engines
*
* @author Rafał Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
*/
final class Processor
{
/**
* @var UrlGeneratorInterface
*/
private $router;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(UrlGeneratorInterface $router, TranslatorInterface $translator)
{
$this->router = $router;
$this->translator = $translator;
}
/**
* Generates pagination template data.
*
* @param SlidingPaginationInterface<mixed> $pagination
* @param array<string, mixed> $queryParams
* @param array<string, mixed> $viewParams
*
* @return array<string, mixed>
*/
public function render(SlidingPaginationInterface $pagination, array $queryParams = [], array $viewParams = []): array
{
$data = $pagination->getPaginationData();
$data['route'] = $pagination->getRoute();
$data['query'] = \array_merge($pagination->getParams(), $queryParams);
return \array_merge(
$pagination->getPaginatorOptions() ?? [], // options given to paginator when paginated
$pagination->getCustomParameters() ?? [], // all custom parameters for view
$viewParams, // additional custom parameters for view
$data // merging base route parameters last, to avoid broke of integrity
);
}
/**
* Create a sort url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key examples: "article.title" or "['article.title', 'article.subtitle']"
*
* @param SlidingPaginationInterface<mixed> $pagination
* @param string|array<string, mixed> $title
* @param string|array<string, mixed> $key
* @param array<string, mixed> $options
* @param array<string, mixed> $params
*
* @return array<string, mixed>
*/
public function sortable(SlidingPaginationInterface $pagination, $title, $key, array $options = [], array $params = []): array
{
$options = \array_merge([
'absolute' => UrlGeneratorInterface::ABSOLUTE_PATH,
'translationParameters' => [],
'translationDomain' => null,
'translationCount' => null,
], $options);
$hasFixedDirection = null !== $pagination->getPaginatorOption('sortDirectionParameterName')
&& isset($params[$pagination->getPaginatorOption('sortDirectionParameterName')])
;
$params = \array_merge($pagination->getParams(), $params);
$direction = $options['defaultDirection'] ?? 'asc';
if (null !== $pagination->getPaginatorOption('sortDirectionParameterName')) {
if (isset($params[$pagination->getPaginatorOption('sortDirectionParameterName')])) {
$direction = $params[$pagination->getPaginatorOption('sortDirectionParameterName')];
} elseif (isset($options[$pagination->getPaginatorOption('sortDirectionParameterName')])) {
$direction = $options[$pagination->getPaginatorOption('sortDirectionParameterName')];
}
}
$sorted = $pagination->isSorted($key, $params);
if ($sorted) {
if (!$hasFixedDirection) {
$direction = 'asc' === \strtolower($direction) ? 'desc' : 'asc';
}
$class = 'asc' === $direction ? 'desc' : 'asc';
} else {
$class = 'sortable';
}
if (isset($options['class'])) {
$options['class'] .= ' '.$class;
} else {
$options['class'] = $class;
}
if (\is_array($title) && \array_key_exists($direction, $title)) {
$title = $title[$direction];
}
if (\is_array($key)) {
$key = \implode('+', $key);
}
$params = \array_merge(
$params,
[
$pagination->getPaginatorOption('sortFieldParameterName') => $key,
$pagination->getPaginatorOption('sortDirectionParameterName') => $direction,
$pagination->getPaginatorOption('pageParameterName') => 1, // reset to 1 on sort
]
);
$options['href'] = $this->router->generate($pagination->getRoute(), $params, $options['absolute']);
if (null !== $options['translationDomain']) {
if (null === $options['translationCount']) {
$translationParameters = $options['translationParameters'];
} else {
$translationParameters = $options['translationParameters'] + ['%count%' => $options['translationCount']];
}
$title = $this->translator->trans($title, $translationParameters, $options['translationDomain']);
}
if (!isset($options['title'])) {
$options['title'] = $title;
}
unset($options['absolute'], $options['translationParameters'], $options['translationDomain'], $options['translationCount']);
return \array_merge(
$pagination->getPaginatorOptions() ?? [],
$pagination->getCustomParameters() ?? [],
\compact('options', 'title', 'direction', 'sorted', 'key')
);
}
/**
* Create a filter url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key example: "article.title"
*
* @param SlidingPaginationInterface<mixed> $pagination
* @param array<string, mixed> $fields
* @param array<string, mixed> $options
* @param array<string, mixed> $params
*
* @return array<string, mixed>
*/
public function filter(SlidingPaginationInterface $pagination, array $fields, array $options = [], array $params = []): array
{
$options = \array_merge([
'absolute' => UrlGeneratorInterface::ABSOLUTE_PATH,
'translationParameters' => [],
'translationDomain' => null,
'button' => 'Filter',
], $options);
$params = \array_merge($pagination->getParams(), $params);
$params[$pagination->getPaginatorOption('pageParameterName')] = 1; // reset to 1 on filter
$filterFieldName = $pagination->getPaginatorOption('filterFieldParameterName');
$filterValueName = $pagination->getPaginatorOption('filterValueParameterName');
$selectedField = $params[$filterFieldName] ?? null;
$selectedValue = $params[$filterValueName] ?? null;
$action = $this->router->generate($pagination->getRoute(), $params, $options['absolute']);
foreach ($fields as $field => $title) {
$fields[$field] = $this->translator->trans($title, $options['translationParameters'], $options['translationDomain']);
}
$options['button'] = $this->translator->trans($options['button'], $options['translationParameters'], $options['translationDomain']);
unset($options['absolute'], $options['translationDomain'], $options['translationParameters']);
return \array_merge(
$pagination->getPaginatorOptions() ?? [],
$pagination->getCustomParameters() ?? [],
\compact('fields', 'action', 'filterFieldName', 'filterValueName', 'selectedField', 'selectedValue', 'options')
);
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* (c) Thibault Duplessis <thibault.duplessis@gmail.com>.
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Knp\Bundle\PaginatorBundle;
use Knp\Bundle\PaginatorBundle\DependencyInjection\Compiler\PaginatorAwarePass;
use Knp\Bundle\PaginatorBundle\DependencyInjection\Compiler\PaginatorConfigurationPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class KnpPaginatorBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(new PaginatorConfigurationPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new PaginatorAwarePass(), PassConfig::TYPE_BEFORE_REMOVING);
}
public function getPath(): string
{
return \dirname(__DIR__);
}
}

View File

@ -0,0 +1,245 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Pagination;
use Knp\Component\Pager\Pagination\AbstractPagination;
final class SlidingPagination extends AbstractPagination implements SlidingPaginationInterface
{
/** @var string|null */
private $route;
/** @var array<string, mixed> */
private $params;
/** @var int */
private $pageRange = 5;
/** @var int|null */
private $pageLimit = null;
/** @var string|null */
private $template;
/** @var string|null */
private $sortableTemplate;
/** @var string|null */
private $filtrationTemplate;
/**
* @param array<string, mixed> $params
*/
public function __construct(array $params)
{
$this->params = $params;
}
public function setUsedRoute(?string $route): void
{
$this->route = $route;
}
public function getRoute(): ?string
{
return $this->route;
}
public function setSortableTemplate(string $template): void
{
$this->sortableTemplate = $template;
}
public function getSortableTemplate(): ?string
{
return $this->sortableTemplate;
}
public function setFiltrationTemplate(string $template): void
{
$this->filtrationTemplate = $template;
}
public function getFiltrationTemplate(): ?string
{
return $this->filtrationTemplate;
}
/**
* @param mixed $value
*/
public function setParam(string $name, $value): void
{
$this->params[$name] = $value;
}
public function getParams(): array
{
return $this->params;
}
public function setTemplate(string $template): void
{
$this->template = $template;
}
public function getTemplate(): ?string
{
return $this->template;
}
public function setPageRange(int $range): void
{
$this->pageRange = \abs($range);
}
public function setPageLimit(?int $limit): void
{
$this->pageLimit = $limit;
}
/**
* Get url query with all parameters.
*
* @param array<string, mixed> $additionalQueryParams
*
* @return array<string, mixed> - list of query parameters
*/
public function getQuery(array $additionalQueryParams = []): array
{
return \array_merge($this->params, $additionalQueryParams);
}
/**
* @param string[]|string|null $key
* @param array<string, mixed> $params
*/
public function isSorted($key = null, array $params = []): bool
{
$params = \array_merge($this->params, $params);
if (null === $key) {
return isset($params[$this->getPaginatorOption('sortFieldParameterName')]);
}
if (\is_array($key)) {
$key = \implode('+', $key);
}
return isset($params[$this->getPaginatorOption('sortFieldParameterName')]) && $params[$this->getPaginatorOption('sortFieldParameterName')] === $key;
}
public function getPage(): ?int
{
return $this->params[$this->getPaginatorOption('pageParameterName')] ?? null;
}
public function getSort(): ?string
{
return $this->params[$this->getPaginatorOption('sortFieldParameterName')] ?? null;
}
public function getDirection(): ?string
{
return $this->params[$this->getPaginatorOption('sortDirectionParameterName')] ?? null;
}
public function getPaginationData(): array
{
$pageCount = $this->getPageCount();
$current = $this->currentPageNumber;
if ($pageCount < $current) {
$this->currentPageNumber = $current = $pageCount;
}
if ($this->pageRange > $pageCount) {
$this->pageRange = $pageCount;
}
$delta = \ceil($this->pageRange / 2);
if ($current - $delta > $pageCount - $this->pageRange) {
$pages = \range($pageCount - $this->pageRange + 1, $pageCount);
} else {
if ($current - $delta < 0) {
$delta = $current;
}
$offset = $current - $delta;
$pages = \range($offset + 1, $offset + $this->pageRange);
}
$proximity = \floor($this->pageRange / 2);
$startPage = $current - $proximity;
$endPage = $current + $proximity;
if ($startPage < 1) {
$endPage = \min($endPage + (1 - $startPage), $pageCount);
$startPage = 1;
}
if ($endPage > $pageCount) {
$startPage = \max($startPage - ($endPage - $pageCount), 1);
$endPage = $pageCount;
}
$viewData = [
'last' => $pageCount,
'current' => $current,
'numItemsPerPage' => $this->numItemsPerPage,
'first' => 1,
'pageCount' => $pageCount,
'totalCount' => $this->totalCount,
'pageRange' => $this->pageRange,
'startPage' => $startPage,
'endPage' => $endPage,
];
if ($current > 1) {
$viewData['previous'] = $current - 1;
}
if ($current < $pageCount) {
$viewData['next'] = $current + 1;
}
$viewData['pagesInRange'] = $pages;
$viewData['firstPageInRange'] = \min($pages);
$viewData['lastPageInRange'] = \max($pages);
if (null !== $this->getItems()) {
$viewData['currentItemCount'] = $this->count();
$viewData['firstItemNumber'] = 0;
$viewData['lastItemNumber'] = 0;
if ($viewData['totalCount'] > 0) {
$viewData['firstItemNumber'] = (($current - 1) * $this->numItemsPerPage) + 1;
$viewData['lastItemNumber'] = $viewData['firstItemNumber'] + $viewData['currentItemCount'] - 1;
}
}
return $viewData;
}
public function getPageCount(): int
{
$count = (int) \ceil($this->totalCount / $this->numItemsPerPage);
if (null !== $this->pageLimit) {
return \min($count, $this->pageLimit);
}
return $count;
}
public function getPaginatorOptions(): ?array
{
return $this->paginatorOptions;
}
public function getCustomParameters(): ?array
{
return $this->customParameters;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Pagination;
use Knp\Component\Pager\Pagination\PaginationInterface;
interface SlidingPaginationInterface extends PaginationInterface
{
public function getRoute(): ?string;
/**
* @return array<string, mixed>
*/
public function getParams(): array;
/**
* @param string[]|string|null $key
* @param array<string, mixed> $params
*/
public function isSorted($key = null, array $params = []): bool;
/**
* @return array<string, mixed>
*/
public function getPaginationData(): array;
/**
* @return array<string, mixed>
*/
public function getPaginatorOptions(): ?array;
/**
* @return array<string, mixed>
*/
public function getCustomParameters(): ?array;
}

View File

@ -0,0 +1,96 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Subscriber;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
use Knp\Component\Pager\Event\PaginationEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
final class SlidingPaginationSubscriber implements EventSubscriberInterface
{
/** @var string */
private $route;
/** @var array<string, mixed> */
private $params = [];
/** @var array<string, mixed> */
private $options;
/**
* @param array<string, mixed> $options
*/
public function __construct(array $options)
{
$this->options = $options;
}
public function onKernelRequest(RequestEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
$this->route = $request->attributes->get('_route');
$this->params = \array_replace($request->query->all(), $request->attributes->get('_route_params', []));
foreach ($this->params as $key => $param) {
if (\strpos($key, '_') === 0) {
unset($this->params[$key]);
}
}
}
public function pagination(PaginationEvent $event): void
{
// default sort field and order
$eventOptions = $event->options;
if (isset($eventOptions['defaultSortFieldName']) && !isset($this->params[$eventOptions['sortFieldParameterName']])) {
$this->params[$eventOptions['sortFieldParameterName']] = $eventOptions['defaultSortFieldName'];
}
if (isset($eventOptions['defaultSortDirection']) && !isset($this->params[$eventOptions['sortDirectionParameterName']])) {
$this->params[$eventOptions['sortDirectionParameterName']] = $eventOptions['defaultSortDirection'];
}
// remove default sort params from pagination links
if (isset($eventOptions['removeDefaultSortParams']) && true === $eventOptions['removeDefaultSortParams']) {
$defaultSortFieldName = $eventOptions['defaultSortFieldName'];
$sortFieldParameterName = $this->params[$eventOptions['sortFieldParameterName']];
$isFieldEqual = $defaultSortFieldName === $sortFieldParameterName;
$defaultSortDirection = $eventOptions['defaultSortDirection'];
$sortDirectionParameterName = $this->params[$eventOptions['sortDirectionParameterName']];
$isDirectionEqual = $defaultSortDirection === $sortDirectionParameterName;
if (isset($defaultSortFieldName, $sortFieldParameterName, $defaultSortDirection, $sortDirectionParameterName) && $isFieldEqual && $isDirectionEqual) {
unset($this->params[$eventOptions['sortFieldParameterName']], $this->params[$eventOptions['sortDirectionParameterName']]);
}
}
$pagination = new SlidingPagination($this->params);
$pagination->setUsedRoute($this->route);
$pagination->setTemplate($this->options['defaultPaginationTemplate']);
$pagination->setSortableTemplate($this->options['defaultSortableTemplate']);
$pagination->setFiltrationTemplate($this->options['defaultFiltrationTemplate']);
$pagination->setPageRange($this->options['defaultPageRange']);
$pagination->setPageLimit($this->options['defaultPageLimit']);
$event->setPagination($pagination);
$event->stopPropagation();
}
/**
* @return array<string, array<int, int|string>>
*/
public static function getSubscribedEvents(): array
{
return [
'knp_pager.pagination' => ['pagination', 1],
];
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Templating;
use Knp\Bundle\PaginatorBundle\Helper\Processor;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPaginationInterface;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Templating\PhpEngine;
/**
* Pagination PHP helper.
*
* Basically provides access to KnpPaginator from PHP templates
*
* @author Rafa³ Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
*/
final class PaginationHelper extends Helper
{
/**
* @var PhpEngine
*/
protected $templating;
/**
* @var Processor
*/
protected $processor;
public function __construct(Processor $processor, PhpEngine $templating)
{
$this->processor = $processor;
$this->templating = $templating;
}
/**
* Renders the pagination template.
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param array<string, mixed> $queryParams
* @param array<string, mixed> $viewParams
*/
public function render(SlidingPaginationInterface $pagination, ?string $template = null, array $queryParams = [], array $viewParams = []): string
{
return $this->templating->render(
$template ?: $pagination->getTemplate(),
$this->processor->render($pagination, $queryParams, $viewParams)
);
}
/**
* Create a sort url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key example: "article.title"
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param string|array<string, mixed> $key
* @param array<string, mixed> $options
* @param array<string, mixed> $params
*/
public function sortable(SlidingPaginationInterface $pagination, string $title, $key, array $options = [], array $params = [], ?string $template = null): string
{
return $this->templating->render(
$template ?: $pagination->getSortableTemplate(),
$this->processor->sortable($pagination, $title, $key, $options, $params)
);
}
/**
* Create a filter url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key example: "article.title"
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param array<string, mixed> $fields
* @param array<string, mixed> $options
* @param array<string, mixed> $params
*/
public function filter(SlidingPaginationInterface $pagination, array $fields, array $options = [], array $params = [], ?string $template = null): string
{
return $this->templating->render(
$template ?: $pagination->getFiltrationTemplate(),
$this->processor->filter($pagination, $fields, $options, $params)
);
}
/**
* Get helper name.
*/
public function getName(): string
{
return 'knp_pagination';
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Knp\Bundle\PaginatorBundle\Twig\Extension;
use Knp\Bundle\PaginatorBundle\Helper\Processor;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPaginationInterface;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class PaginationExtension extends AbstractExtension
{
/**
* @var Processor
*/
private $processor;
public function __construct(Processor $processor)
{
$this->processor = $processor;
}
public function getFunctions(): array
{
return [
new TwigFunction('knp_pagination_render', [$this, 'render'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFunction('knp_pagination_sortable', [$this, 'sortable'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFunction('knp_pagination_filter', [$this, 'filter'], ['is_safe' => ['html'], 'needs_environment' => true]),
];
}
/**
* Renders the pagination template.
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param array<string, mixed> $queryParams
* @param array<string, mixed> $viewParams
*/
public function render(Environment $env, SlidingPaginationInterface $pagination, ?string $template = null, ?array $queryParams = [], ?array $viewParams = []): string
{
return $env->render(
$template ?: $pagination->getTemplate(),
$this->processor->render($pagination, $queryParams ?? [], $viewParams ?? [])
);
}
/**
* Create a sort url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key example: "article.title"
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param string|array<string, mixed> $key
* @param array<string, mixed> $options
* @param array<string, mixed> $params
*/
public function sortable(Environment $env, SlidingPaginationInterface $pagination, string $title, $key, array $options = [], array $params = [], ?string $template = null): string
{
return $env->render(
$template ?: $pagination->getSortableTemplate(),
$this->processor->sortable($pagination, $title, $key, $options, $params)
);
}
/**
* Create a filter url for the field named $title
* and identified by $key which consists of
* alias and field. $options holds all link
* parameters like "alt, class" and so on.
*
* $key example: "article.title"
*
* @param \Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination<mixed> $pagination
* @param array<string, mixed> $fields
* @param array<string, mixed> $options
* @param array<string, mixed>|null $params
*/
public function filter(Environment $env, SlidingPaginationInterface $pagination, array $fields, ?array $options = [], ?array $params = [], ?string $template = null): string
{
return $env->render(
$template ?: $pagination->getFiltrationTemplate(),
$this->processor->filter($pagination, $fields, $options ?? [], $params ?? [])
);
}
}

View File

@ -0,0 +1,23 @@
{#
/**
* @file
* Bootstrap v5 with Bootstrap Icons Sorting control implementation.
*
* Install Icon Set: https://icons.getbootstrap.com/#install
* Overview: https://icons.getbootstrap.com/
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="float-end">
{% if sorted %}
{% if direction == 'desc' %}
<i class="bi bi-sort-down"></i>
{% else %}
<i class="bi bi-sort-up"></i>
{% endif %}
{% else %}
<i class="bi bi-filter-left"></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,20 @@
{#
/**
* @file
* Bootstrap v5 with Font Awesome Sorting control implementation.
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="float-end">
{% if sorted %}
{% if direction == 'desc' %}
<i class="fa fa-sort-down"></i>
{% else %}
<i class="fa fa-sort-up"></i>
{% endif %}
{% else %}
<i class="fa fa-sort"></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,24 @@
{#
/**
* @file
* Bootstrap v5 Filter control implementation.
*
* View that can be used with the filter module
*/
#}
<form method="get" action="{{ action }}" enctype="application/x-www-form-urlencoded">
<div class="input-group mb-3">
{% if fields|length > 1 %}
<select class="form-select" name="{{ filterFieldName }}">
{% for field, label in fields %}
<option value="{{ field }}"{% if selectedField == field %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% else %}
<input type="hidden" name="{{ filterFieldName }}" value="{{ fields|keys|first }}"/>
{% endif %}
<input class="form-control" type="search" value="{{ selectedValue }}" name="{{ filterValueName }}"
placeholder="{{ 'filter_searchword'|trans({}, 'KnpPaginatorBundle') }}"/>
<button class="btn btn-primary">{{ options.button }}</button>
</div>
</form>

View File

@ -0,0 +1,23 @@
{#
/**
* @file
* Bootstrap v5 with Material Design Icons Sorting control implementation.
*
* Install Icon Set: https://google.github.io/material-design-icons/
* Overview: https://material.io/resources/icons/?style=baseline
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="float-end">
{% if sorted %}
{% if direction == 'desc' %}
<i class="material-icons">expand_more</i>
{% else %}
<i class="material-icons">expand_less</i>
{% endif %}
{% else %}
<i class="material-icons">unfold_more</i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,11 @@
{#
/**
* @file
* Bootstrap v5 Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Bootstrap CSS Framework
* https://getbootstrap.com/docs/5.0/components/pagination/
*/
#}
{% extends '@KnpPaginator/Pagination/twitter_bootstrap_v4_pagination.html.twig' %}

View File

@ -0,0 +1,69 @@
{# bulma Sliding pagination control implementation #}
{% set position = position|default('left') %}
{% set rounded = rounded|default(false) %}
{% set size = size|default(null) %}
{% set classes = ['pagination'] %}
{% if position != 'left' %}{% set classes = classes|merge(['is-' ~ position]) %}{% endif %}
{% if rounded %}{% set classes = classes|merge(['is-rounded']) %}{% endif %}
{% if size != null %}{% set classes = classes|merge(['is-' ~ size]) %}{% endif %}
{% if pageCount > 1 %}
<nav class="{{ classes|join(' ') }}" role="navigation" aria-label="pagination">
{% if previous is defined %}
<a rel="prev" class="pagination-previous" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
{% else %}
<a class="pagination-previous" disabled>{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
{% endif %}
{% if next is defined %}
<a rel="next" class="pagination-next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}</a>
{% else %}
<a class="pagination-next" disabled>{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}</a>
{% endif %}
<ul class="pagination-list">
<li>
{% if current == first %}
<a class="pagination-link is-current" aria-label="Page {{ current }}" aria-current="page" href="{{ path(route, query|merge({(pageParameterName): first})) }}">1</a>
{% else %}
<a class="pagination-link" href="{{ path(route, query|merge({(pageParameterName): first})) }}">1</a>
{% endif %}
</li>
{% if pagesInRange[0] - first >= 2 %}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{% endif %}
{% for page in pagesInRange %}
{% if first != page and page != last %}
<li>
{% if page == current %}
<a class="pagination-link is-current" aria-label="Page {{ current }}" aria-current="page" href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
{% else %}
<a class="pagination-link" aria-label="Goto page {{ page }}" href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
{% if last - pagesInRange[pagesInRange|length - 1] >= 2 %}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{% endif %}
<li>
{% if current == last %}
<a class="pagination-link is-current" aria-label="Page {{ current }}" aria-current="page" href="{{ path(route, query|merge({(pageParameterName): last})) }}">{{ last }}</a>
{% else %}
<a class="pagination-link" href="{{ path(route, query|merge({(pageParameterName): last})) }}">{{ last }}</a>
{% endif %}
</li>
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,17 @@
<form method="get" action="{{ action }}" enctype="application/x-www-form-urlencoded">
{% if fields|length > 1 %}
<select name="{{ filterFieldName }}">
{% for field, label in fields %}
<option value="{{ field }}"{% if selectedField == field %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% else %}
<input type="hidden" name="{{ filterFieldName }}" value="{{ fields|keys|first }}" />
{% endif %}
<input type="text" value="{{ selectedValue }}" name="{{ filterValueName }}" />
<button>{{ options.button }}</button>
</form>

View File

@ -0,0 +1,97 @@
{#
/**
* @file
* Foundation 5 Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Foundation 5 CSS Toolkit
* https://get.foundation/sites/docs-v5/components/pagination.html
*
* @author Vincent Loy <vincent.loy1@gmail.com>
*
* This view have been ported from twitter bootstrap v3 pagination control implementation
* from:
* @author Pablo Díez <pablodip@gmail.com>
* @author Jan Sorgalla <jsorgalla@gmail.com>
* @author Artem Ponomarenko <imenem@inbox.ru>
* @author Artem Zabelin <artjomzabelin@gmail.com>
*/
#}
{% if pageCount > 1 %}
<ul class="pagination">
{% if previous is defined %}
<li class="arrow">
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&laquo; {{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
</li>
{% else %}
<li class="arrow unavailable">
<a>
&laquo; {{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}
</a>
</li>
{% endif %}
{% if startPage > 1 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="unavailable">
<a>&hellip;</a>
</li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">
{{ page }}
</a>
</li>
{% else %}
<li class="current">
<a>{{ page }}</a>
</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="unavailable">
<a>&hellip;</a>
</li>
{% else %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">
{{ pageCount -1 }}
</a>
</li>
{% endif %}
{% endif %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li class="arrow">
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">
{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }} &nbsp;&raquo;
</a>
</li>
{% else %}
<li class="arrow unavailable">
<a>
{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }} &nbsp;&raquo;
</a>
</li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,74 @@
{% if pageCount > 1 %}
<nav aria-label="Pagination">
{% set classAlign = (align is defined) ? " text-#{align}" : '' %}
<ul class="pagination{{ classAlign }}">
{% if previous is defined %}
<li class="pagination-previous">
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">
{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}
</a>
</li>
{% else %}
<li class="pagination-previous disabled">
{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}
</li>
{% endif %}
{% if startPage > 1 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="ellipsis"></li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">
{{ page }}
</a>
</li>
{% else %}
<li class="current">{{ page }}</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="ellipsis"></li>
{% else %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">
{{ pageCount -1 }}
</a>
</li>
{% endif %}
{% endif %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li class="pagination-next">
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">
{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}
</a>
</li>
{% else %}
<li class="pagination-next disabled">
{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,83 @@
{#
/**
* @file
* Materialize pagination control implementation.
*
* View that can be used with the pagination module
* from the Materialize CSS
* https://materializecss.com/pagination.html
*
* @author Leonardo Bressan Motyczka <leomoty@gmail.com>
*/
#}
{% if pageCount > 1 %}
<ul class="pagination">
{% if first is defined and current != first %}
<li class="waves-effect">
<a href="{{ path(route, query|merge({(pageParameterName): first})) }}">
<i class="material-icons">first_page</i>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#!">
<i class="material-icons">first_page</i>
</a>
</li>
{% endif %}
{% if previous is defined %}
<li class="waves-effect">
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">
<i class="material-icons">chevron_left</i>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#!">
<i class="material-icons">chevron_left</i>
</a>
</li>
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li class="waves-effect">
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</li>
{% else %}
<li class="active">
<a href="#!">{{ page }}</a>
</li>
{% endif %}
{% endfor %}
{% if next is defined %}
<li class="waves-effect">
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">
<i class="material-icons">chevron_right</i>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#!">
<i class="material-icons">chevron_right</i>
</a>
</li>
{% endif %}
{% if last is defined and current != last %}
<li class="waves-effect">
<a href="{{ path(route, query|merge({(pageParameterName): last})) }}">
<i class="material-icons">last_page</i>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#!">
<i class="material-icons">last_page</i>
</a>
</li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,46 @@
{#
/**
* @file
* Semantic UI Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Semantic UI CSS Toolkit
* https://semantic-ui.com/collections/menu.html#pagination
*
* @author Valerian Dorcy <valerian.dorcy@gmail.com>
*/
#}
<div class="ui pagination menu">
{% if first is defined and current != first %}
<a class="icon item" href="{{ path(route, query|merge({(pageParameterName): first})) }}">
<i class="angle double left icon"></i>
</a>
{% endif %}
{% if previous is defined %}
<a rel="prev" class="item icon" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">
<i class="angle left icon"></i>
</a>
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<a class="item" href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
{% else %}
<span class="active item">{{ page }}</span>
{% endif %}
{% endfor %}
{% if next is defined %}
<a rel="next" class="icon item" href="{{ path(route, query|merge({(pageParameterName): next})) }}">
<i class="angle right icon"></i>
</a>
{% endif %}
{% if last is defined and current != last %}
<a class="icon item" href="{{ path(route, query|merge({(pageParameterName): last})) }}">
<i class="angle right double icon"></i>
</a>
{% endif %}
</div>

View File

@ -0,0 +1,14 @@
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="right floated">
{% if sorted %}
{% if direction == 'desc' %}
<i class="sort down icon"></i>
{% else %}
<i class="sort up icon"></i>
{% endif %}
{% else %}
<i class="sort icon"></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,38 @@
{# default Sliding pagination control implementation #}
{% if pageCount > 1 %}
<div class="pagination">
{% if first is defined and current != first %}
<span class="first">
<a href="{{ path(route, query|merge({(pageParameterName): first})) }}">&lt;&lt;</a>
</span>
{% endif %}
{% if previous is defined %}
<span class="previous">
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&lt;</a>
</span>
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<span class="page">
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</span>
{% else %}
<span class="current">{{ page }}</span>
{% endif %}
{% endfor %}
{% if next is defined %}
<span class="next">
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">&gt;</a>
</span>
{% endif %}
{% if last is defined and current != last %}
<span class="last">
<a href="{{ path(route, query|merge({(pageParameterName): last})) }}">&gt;&gt;</a>
</span>
{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1 @@
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>{{ title }}</a>

View File

@ -0,0 +1,40 @@
{# tailwindcss Sliding pagination control implementation #}
{% if pageCount > 1 %}
<div class="inline-block">
<div class="flex items-baseline flex-row border border-gray-400 rounded-sm w-auto">
{% if first is defined and current != first %}
<span class="bg-white text-blue-600 px-3 py-2 text-lg border-r border-gray-400 font-bold">
<a href="{{ path(route, query|merge({(pageParameterName): first})) }}">&lt;&lt;</a>
</span>
{% endif %}
{% if previous is defined %}
<span class="bg-white text-blue-600 px-3 text-lg py-2 border-r border-gray-400">
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&lt;</a>
</span>
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<span class="bg-white text-blue-600 px-3 py-2 text-lg border-r border-gray-400">
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</span>
{% else %}
<span class="bg-blue-600 text-white px-3 py-2 text-lg font-bold">{{ page }}</span>
{% endif %}
{% endfor %}
{% if next is defined %}
<span class="bg-white text-blue-600 px-3 py-2 text-lg border-r border-gray-400">
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">&gt;</a>
</span>
{% endif %}
{% if last is defined and current != last %}
<span class="bg-white text-blue-600 px-3 py-2 text-lg border-gray-400 font-bold">
<a href="{{ path(route, query|merge({(pageParameterName): last})) }}">&gt;&gt;</a>
</span>
{% endif %}
</div>
</div>
{% endif %}

View File

@ -0,0 +1,89 @@
{#
/**
* @file
* Twitter Bootstrap Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Twitter Bootstrap CSS Toolkit
* https://getbootstrap.com/2.3.2/components.html#pagination
*
* This view has been ported from Pagerfanta progect
* https://github.com/whiteoctober/Pagerfanta/
* https://github.com/whiteoctober/Pagerfanta/blob/master/src/Pagerfanta/View/TwitterBootstrapView.php
*
* @author Pablo Díez <pablodip@gmail.com>
* @author Jan Sorgalla <jsorgalla@gmail.com>
* @author Artem Ponomarenko <imenem@inbox.ru>
*/
#}
{% if pageCount > 1 %}
<div class="pagination">
<ul>
{% if previous is defined %}
<li>
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
</li>
{% else %}
<li class="disabled">
<span>&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</span>
</li>
{% endif %}
{% if startPage > 1 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="disabled">
<span>&hellip;</span>
</li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</li>
{% else %}
<li class="active">
<span>{{ page }}</span>
</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="disabled">
<span>&hellip;</span>
</li>
{% else %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">{{ pageCount -1 }}</a>
</li>
{% endif %}
{% endif %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li>
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</a>
</li>
{% else %}
<li class="disabled">
<span>{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</span>
</li>
{% endif %}
</ul>
</div>
{% endif %}

View File

@ -0,0 +1,84 @@
{#
/**
* @file
* Twitter Bootstrap v3 Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Twitter Bootstrap CSS Toolkit
* https://getbootstrap.com/docs/3.4/components/#pagination
*
* @author Pablo Díez <pablodip@gmail.com>
* @author Jan Sorgalla <jsorgalla@gmail.com>
* @author Artem Ponomarenko <imenem@inbox.ru>
* @author Artem Zabelin <artjomzabelin@gmail.com>
*/
#}
{% if pageCount > 1 %}
<ul class="pagination justify-content-center">
{% if previous is defined %}
<li class="page-item">
<a class="page-link" rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&laquo;&nbsp;{{ 'label_previous'|trans([], 'KnpPaginatorBundle') }}</a>
</li>
{% else %}
<li class="disabled page-item">
<a class="page-link">&laquo;&nbsp;{{ 'label_previous'|trans([], 'KnpPaginatorBundle') }}</a>
</li>
{% endif %}
{% if startPage > 1 %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="disabled page-item">
<a class="page-link">&hellip;</a>
</li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</li>
{% else %}
<li class="active">
<a class="page-link">{{ page }}</a>
</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="disabled page-item">
<a class="page-link">&hellip;</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">{{ pageCount -1 }}</a>
</li>
{% endif %}
{% endif %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li class="page-item">
<a class="page-link" rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">{{ 'label_next'|trans([], 'KnpPaginatorBundle') }}&nbsp;&raquo;</a>
</li>
{% else %}
<li class="disabled page-item">
<a class="page-link">{{ 'label_next'|trans([], 'KnpPaginatorBundle') }}&nbsp;&raquo;</a>
</li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,22 @@
{#
/**
* @file
* Twitter Bootstrap v3 Sorting control implementation.
*
* @author Afolabi Olayinka <folabiolayinka@gmail.com>
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %} style="color: #000;">
<span class="pull-right">
{% if sorted %}
{% if direction == 'desc' %}
<i class="glyphicon glyphicon-sort-by-attributes-alt"></i>
{% else %}
<i class="glyphicon glyphicon-sort-by-attributes"></i>
{% endif %}
{% else %}
<i class="glyphicon glyphicon-sort" style="color: #d2d6de;"></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,23 @@
<form class="form-inline" method="get" action="{{ action }}" enctype="application/x-www-form-urlencoded">
<div class="form-group">
{% if fields|length > 1 %}
<select class="form-control" name="{{ filterFieldName }}">
{% for field, label in fields %}
<option value="{{ field }}"{% if selectedField == field %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% else %}
<input type="hidden" name="{{ filterFieldName }}" value="{{ fields|keys|first }}" />
{% endif %}
<input class="form-control" type="search" value="{{ selectedValue }}" name="{{ filterValueName }}" placeholder="{{ 'filter_searchword'|trans({}, 'KnpPaginatorBundle') }}" />
</div>
<div class="form-group">
<button class="btn btn-primary">{{ options.button }}</button>
</div>
</form>

View File

@ -0,0 +1,22 @@
{#
/**
* @file
* Twitter Bootstrap - Font Awesome Sorting control implementation.
*
* @author Rodrigo Régis Palmeira <regisbsb@gmail.com>
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="float-right">
{% if sorted %}
{% if direction == 'desc' %}
<i class="fa fa-sort-down"></i>
{% else %}
<i class="fa fa-sort-up"></i>
{% endif %}
{% else %}
<i class="fa fa-sort"></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,25 @@
{#
/**
* @file
* Twitter Bootstrap - Material Design Icons Sorting control implementation.
*
* Install Icon Set: https://google.github.io/material-design-icons/
* Overview: https://material.io/resources/icons/?style=baseline
*
* @author Mike Stuebbe <mike.stuebbe@gmail.com>
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
<span class="float-right">
{% if sorted %}
{% if direction == 'desc' %}
<i class="material-icons">expand_more</i>
{% else %}
<i class="material-icons">expand_less</i>
{% endif %}
{% else %}
<i class="material-icons">unfold_more</i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,84 @@
{#
/**
* @file
* Twitter Bootstrap v4 Sliding pagination control implementation.
*
* View that can be used with the pagination module
* from the Twitter Bootstrap CSS Toolkit
* https://getbootstrap.com/docs/4.5/components/pagination/
*
*/
#}
{% if pageCount > 1 %}
<nav>
{% set classAlign = (align is not defined) ? '' : align=='center' ? ' justify-content-center' : (align=='right' ? ' justify-content-end' : '') %}
{% set classSize = (size is not defined) ? '' : size=='large' ? ' pagination-lg' : (size=='small' ? ' pagination-sm' : '') %}
<ul class="pagination{{ classAlign }}{{ classSize }}">
{% if previous is defined %}
<li class="page-item">
<a class="page-link" rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</span>
</li>
{% endif %}
{% if startPage > 1 %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="page-item disabled">
<span class="page-link">&hellip;</span>
</li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page }}</span>
</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="page-item disabled">
<span class="page-link">&hellip;</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">{{ pageCount -1 }}</a>
</li>
{% endif %}
{% endif %}
<li class="page-item">
<a class="page-link" href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li class="page-item">
<a class="page-link" rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,82 @@
{#
/**
* @file
* UIkit 3.0 pagination control implementation.
*
* View that can be used with the pagination module
* from the UIKit Toolkit
* https://getuikit.com/docs/pagination
*
*
* @author KULDIP PIPALIYA <kuldipem@gmail.com>
*/
#}
{% if pageCount > 1 %}
<ul class="uk-pagination uk-flex-center uk-margin-medium-top">
{% if previous is defined %}
<li>
<a rel="prev" href="{{ path(route, query|merge({(pageParameterName): previous})) }}">&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</a>
</li>
{% else %}
<li class="uk-disabled">
<span>&laquo;&nbsp;{{ 'label_previous'|trans({}, 'KnpPaginatorBundle') }}</span>
</li>
{% endif %}
{% if startPage > 1 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 1})) }}">1</a>
</li>
{% if startPage == 3 %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): 2})) }}">2</a>
</li>
{% elseif startPage != 2 %}
<li class="uk-disabled">
<span>&hellip;</span>
</li>
{% endif %}
{% endif %}
{% for page in pagesInRange %}
{% if page != current %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}">{{ page }}</a>
</li>
{% else %}
<li class="uk-active">
<span>{{ page }}</span>
</li>
{% endif %}
{% endfor %}
{% if pageCount > endPage %}
{% if pageCount > (endPage + 1) %}
{% if pageCount > (endPage + 2) %}
<li class="uk-disabled">
<span>&hellip;</span>
</li>
{% else %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): (pageCount - 1)})) }}">{{ pageCount -1 }}</a>
</li>
{% endif %}
{% endif %}
<li>
<a href="{{ path(route, query|merge({(pageParameterName): pageCount})) }}">{{ pageCount }}</a>
</li>
{% endif %}
{% if next is defined %}
<li>
<a rel="next" href="{{ path(route, query|merge({(pageParameterName): next})) }}">{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</a>
</li>
{% else %}
<li class="uk-disabled">
<span>{{ 'label_next'|trans({}, 'KnpPaginatorBundle') }}&nbsp;&raquo;</span>
</li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,22 @@
{#
/**
* @file
* UIKit 3.0 Sorting control implementation.
*
* @author KULDIP PIPALIYA <kuldipem@gmail.com>
*/
#}
<a{% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %} style="color: #000;">
<span class="pull-right">
{% if sorted %}
{% if direction == 'desc' %}
<i data-uk-icon="triangle-down"></i>
{% else %}
<i data-uk-icon="triangle-up"></i>
{% endif %}
{% else %}
<i></i>
{% endif %}
</span>
{{ title }}
</a>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="ar" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>السابق</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>التالي</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>بحث...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="bg" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Предишна</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Следваща</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>търсене...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="ca" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Anterior</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Següent</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Cercar...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="cs" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Předchozí</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Další</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Vyhledávat...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="da" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Forrige</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Næste</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Søg...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Vorherige</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Nächste</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Suchbegriff...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Previous</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Next</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Searchword...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="es" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Anterior</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Siguiente</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Buscar...</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="eu" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="0" resname="label_previous">
<source>label_previous</source>
<target>Aurrekoa</target>
</trans-unit>
<trans-unit id="1" resname="label_next">
<source>label_next</source>
<target>Hurrengoa</target>
</trans-unit>
<trans-unit id="2" resname="filter_searchword">
<source>filter_searchword</source>
<target>Bilatu...</target>
</trans-unit>
</body>
</file>
</xliff>

Some files were not shown because too many files have changed in this diff Show More