├── src └── Knp │ └── Component │ └── Pager │ ├── Exception │ ├── InvalidValueException.php │ ├── PageLimitInvalidException.php │ ├── PageNumberInvalidException.php │ └── PageNumberOutOfRangeException.php │ ├── ArgumentAccess │ ├── ArgumentAccessInterface.php │ └── RequestArgumentAccess.php │ ├── Event │ ├── AfterEvent.php │ ├── Subscriber │ │ ├── Paginate │ │ │ ├── Doctrine │ │ │ │ ├── ORM │ │ │ │ │ ├── QueryBuilderSubscriber.php │ │ │ │ │ ├── Query │ │ │ │ │ │ └── Helper.php │ │ │ │ │ └── QuerySubscriber.php │ │ │ │ ├── ODM │ │ │ │ │ ├── PHPCR │ │ │ │ │ │ ├── QueryBuilderSubscriber.php │ │ │ │ │ │ └── QuerySubscriber.php │ │ │ │ │ └── MongoDB │ │ │ │ │ │ ├── QueryBuilderSubscriber.php │ │ │ │ │ │ └── QuerySubscriber.php │ │ │ │ ├── CollectionSubscriber.php │ │ │ │ └── DBALQueryBuilderSubscriber.php │ │ │ ├── Callback │ │ │ │ ├── CallbackPagination.php │ │ │ │ └── CallbackSubscriber.php │ │ │ ├── ArraySubscriber.php │ │ │ ├── SolariumQuerySubscriber.php │ │ │ ├── ElasticaQuerySubscriber.php │ │ │ ├── PropelQuerySubscriber.php │ │ │ └── PaginationSubscriber.php │ │ ├── Filtration │ │ │ ├── FiltrationSubscriber.php │ │ │ ├── PropelQuerySubscriber.php │ │ │ └── Doctrine │ │ │ │ └── ORM │ │ │ │ ├── QuerySubscriber.php │ │ │ │ └── Query │ │ │ │ └── WhereWalker.php │ │ └── Sortable │ │ │ ├── SortableSubscriber.php │ │ │ ├── PropelQuerySubscriber.php │ │ │ ├── ElasticaQuerySubscriber.php │ │ │ ├── SolariumQuerySubscriber.php │ │ │ ├── Doctrine │ │ │ ├── ODM │ │ │ │ └── MongoDB │ │ │ │ │ └── QuerySubscriber.php │ │ │ └── ORM │ │ │ │ ├── QuerySubscriber.php │ │ │ │ └── Query │ │ │ │ └── OrderByWalker.php │ │ │ └── ArraySubscriber.php │ ├── PaginationEvent.php │ ├── BeforeEvent.php │ └── ItemsEvent.php │ ├── Pagination │ ├── PaginationInterface.php │ ├── SlidingPagination.php │ └── AbstractPagination.php │ ├── PaginatorInterface.php │ └── Paginator.php ├── LICENSE └── composer.json /src/Knp/Component/Pager/Exception/InvalidValueException.php: -------------------------------------------------------------------------------- 1 | maxPageNumber; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/AfterEvent.php: -------------------------------------------------------------------------------- 1 | $pagination 15 | */ 16 | public function __construct(private readonly PaginationInterface $pagination) 17 | { 18 | } 19 | 20 | /** 21 | * @return PaginationInterface 22 | */ 23 | public function getPaginationView(): PaginationInterface 24 | { 25 | return $this->pagination; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ORM/QueryBuilderSubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof QueryBuilder) { 14 | // change target into query 15 | $event->target = $event->target->getQuery(); 16 | } 17 | } 18 | 19 | public static function getSubscribedEvents(): array 20 | { 21 | return [ 22 | 'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/], 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Callback/CallbackPagination.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CallbackPagination 11 | { 12 | /** 13 | * @var callable 14 | */ 15 | private $count; 16 | 17 | /** 18 | * @var callable 19 | */ 20 | private $items; 21 | 22 | public function __construct(callable $count, callable $items) 23 | { 24 | $this->count = $count; 25 | $this->items = $items; 26 | } 27 | 28 | public function getPaginationCount(): int 29 | { 30 | return call_user_func($this->count); 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function getPaginationItems(int $offset, int $limit): array 37 | { 38 | return call_user_func($this->items, $offset, $limit); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ODM/PHPCR/QueryBuilderSubscriber.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class QueryBuilderSubscriber implements EventSubscriberInterface 13 | { 14 | public function items(ItemsEvent $event): void 15 | { 16 | if (!$event->target instanceof QueryBuilder) { 17 | return; 18 | } 19 | 20 | $event->target = $event->target->getQuery(); 21 | } 22 | 23 | public static function getSubscribedEvents(): array 24 | { 25 | return [ 26 | 'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/CollectionSubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof Collection) { 14 | $event->count = $event->target->count(); 15 | $event->items = $event->target->slice( 16 | $event->getOffset(), 17 | $event->getLimit() 18 | ); 19 | $event->stopPropagation(); 20 | } 21 | } 22 | 23 | public static function getSubscribedEvents(): array 24 | { 25 | return [ 26 | 'knp_pager.items' => ['items', 0], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ODM/MongoDB/QueryBuilderSubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof Builder) { 15 | // change target into query 16 | $event->target = $event->target->getQuery($event->options[PaginatorInterface::ODM_QUERY_OPTIONS] ?? []); 17 | } 18 | } 19 | 20 | public static function getSubscribedEvents(): array 21 | { 22 | return [ 23 | 'knp_pager.items' => ['items', 10/*make sure to transform before any further modifications*/], 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/PaginationEvent.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public array $options; 22 | 23 | /** 24 | * @var PaginationInterface 25 | */ 26 | private PaginationInterface $pagination; 27 | 28 | /** 29 | * @param PaginationInterface $pagination 30 | */ 31 | public function setPagination(PaginationInterface $pagination): void 32 | { 33 | $this->pagination = $pagination; 34 | } 35 | 36 | /** 37 | * @return PaginationInterface 38 | */ 39 | public function getPagination(): PaginationInterface 40 | { 41 | return $this->pagination; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/ArgumentAccess/RequestArgumentAccess.php: -------------------------------------------------------------------------------- 1 | getRequest()->query->has($name); 17 | } 18 | 19 | public function get(string $name): string|int|float|bool|null 20 | { 21 | return $this->getRequest()->query->get($name); 22 | } 23 | 24 | public function set(string $name, string|int|float|bool|null $value): void 25 | { 26 | $this->getRequest()->query->set($name, $value); 27 | } 28 | 29 | private function getRequest(): Request 30 | { 31 | if (null === $request = $this->requestStack->getCurrentRequest()) { 32 | $request = Request::createFromGlobals(); 33 | } 34 | 35 | return $request; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Callback/CallbackSubscriber.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class CallbackSubscriber implements EventSubscriberInterface 14 | { 15 | public function items(ItemsEvent $event): void 16 | { 17 | if ($event->target instanceof CallbackPagination) { 18 | $event->count = $event->target->getPaginationCount(); 19 | if($event->count > 0) { 20 | $event->items = $event->target->getPaginationItems($event->getOffset(), $event->getLimit()); 21 | } 22 | else { 23 | $event->items = []; 24 | } 25 | 26 | $event->stopPropagation(); 27 | } 28 | } 29 | 30 | public static function getSubscribedEvents(): array 31 | { 32 | return [ 33 | 'knp_pager.items' => ['items', 0], 34 | ]; 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 KnpLabs 2 | 3 | The MIT license, reference http://www.opensource.org/licenses/mit-license.php 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/BeforeEvent.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public array $options = []; 19 | 20 | public function __construct( 21 | private readonly EventDispatcherInterface $eventDispatcher, 22 | private readonly ArgumentAccessInterface $argumentAccess, 23 | private readonly ?Connection $connection = null 24 | ) { 25 | } 26 | 27 | public function getEventDispatcher(): EventDispatcherInterface 28 | { 29 | return $this->eventDispatcher; 30 | } 31 | 32 | public function getArgumentAccess(): ArgumentAccessInterface 33 | { 34 | return $this->argumentAccess; 35 | } 36 | 37 | public function getConnection(): ?Connection 38 | { 39 | return $this->connection; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ODM/PHPCR/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class QuerySubscriber implements EventSubscriberInterface 13 | { 14 | public function items(ItemsEvent $event): void 15 | { 16 | if (!$event->target instanceof Query) { 17 | return; 18 | } 19 | 20 | $queryCount = clone $event->target; 21 | $event->count = iterator_count($queryCount->execute(null, Query::HYDRATE_PHPCR)->getRows()); 22 | 23 | $query = $event->target; 24 | $query->setMaxResults($event->getLimit()); 25 | $query->setFirstResult($event->getOffset()); 26 | 27 | $event->items = $query->execute(); 28 | $event->stopPropagation(); 29 | } 30 | 31 | public static function getSubscribedEvents(): array 32 | { 33 | return [ 34 | 'knp_pager.items' => ['items', 0], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Filtration/FiltrationSubscriber.php: -------------------------------------------------------------------------------- 1 | isLoaded) { 19 | return; 20 | } 21 | 22 | /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ 23 | $dispatcher = $event->getEventDispatcher(); 24 | // hook all standard filtration subscribers 25 | $dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber()); 26 | $dispatcher->addSubscriber(new PropelQuerySubscriber()); 27 | 28 | $this->isLoaded = true; 29 | } 30 | 31 | public static function getSubscribedEvents(): array 32 | { 33 | return [ 34 | 'knp_pager.before' => ['before', 1], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/ArraySubscriber.php: -------------------------------------------------------------------------------- 1 | target)) { 14 | $event->count = count($event->target); 15 | $event->items = array_slice( 16 | $event->target, 17 | $event->getOffset(), 18 | $event->getLimit() 19 | ); 20 | $event->stopPropagation(); 21 | } elseif ($event->target instanceof ArrayObject) { 22 | $event->count = $event->target->count(); 23 | $event->items = new ArrayObject(array_slice( 24 | $event->target->getArrayCopy(), 25 | $event->getOffset(), 26 | $event->getLimit() 27 | )); 28 | $event->stopPropagation(); 29 | } 30 | } 31 | 32 | public static function getSubscribedEvents(): array 33 | { 34 | return [ 35 | 'knp_pager.items' => ['items', -1/* other data arrays should be analyzed first*/], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/SortableSubscriber.php: -------------------------------------------------------------------------------- 1 | isLoaded) { 19 | return; 20 | } 21 | 22 | /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ 23 | $dispatcher = $event->getEventDispatcher(); 24 | // hook all standard sortable subscribers 25 | $dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber()); 26 | $dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QuerySubscriber()); 27 | $dispatcher->addSubscriber(new ElasticaQuerySubscriber()); 28 | $dispatcher->addSubscriber(new PropelQuerySubscriber()); 29 | $dispatcher->addSubscriber(new SolariumQuerySubscriber()); 30 | $dispatcher->addSubscriber(new ArraySubscriber()); 31 | 32 | $this->isLoaded = true; 33 | } 34 | 35 | public static function getSubscribedEvents(): array 36 | { 37 | return [ 38 | 'knp_pager.before' => ['before', 1], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ORM/Query/Helper.php: -------------------------------------------------------------------------------- 1 | setParameters(clone $query->getParameters()); 21 | // attach hints 22 | foreach ($query->getHints() as $name => $hint) { 23 | $clonedQuery->setHint($name, $hint); 24 | } 25 | 26 | return $clonedQuery; 27 | } 28 | 29 | /** 30 | * Add a custom TreeWalker $walker class name to 31 | * be included in the CustomTreeWalker hint list 32 | * of the given $query 33 | */ 34 | public static function addCustomTreeWalker(Query $query, string $walker): void 35 | { 36 | $customTreeWalkers = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); 37 | if ($customTreeWalkers !== false && is_array($customTreeWalkers)) { 38 | $customTreeWalkers = array_merge($customTreeWalkers, [$walker]); 39 | } else { 40 | $customTreeWalkers = [$walker]; 41 | } 42 | $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $customTreeWalkers); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/SolariumQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class SolariumQuerySubscriber implements EventSubscriberInterface 16 | { 17 | public function items(ItemsEvent $event): void 18 | { 19 | if (is_array($event->target) && 2 === count($event->target)) { 20 | $values = array_values($event->target); 21 | [$client, $query] = $values; 22 | 23 | if ($client instanceof Client && $query instanceof Query) { 24 | $query->setStart($event->getOffset())->setRows($event->getLimit()); 25 | $solrResult = $client->select($query); 26 | 27 | $event->items = iterator_to_array($solrResult->getIterator()); 28 | $event->count = $solrResult->getNumFound(); 29 | $event->setCustomPaginationParameter('result', $solrResult); 30 | $event->stopPropagation(); 31 | } 32 | } 33 | } 34 | 35 | public static function getSubscribedEvents(): array 36 | { 37 | return [ 38 | 'knp_pager.items' => ['items', 0], /* triggers before a standard array subscriber*/ 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/ElasticaQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target) && 2 === count($event->target) && reset($event->target) instanceof SearchableInterface && end($event->target) instanceof Query) { 19 | [$searchable, $query] = $event->target; 20 | 21 | $query->setFrom($event->getOffset()); 22 | $query->setSize($event->getLimit()); 23 | $results = $searchable->search($query); 24 | 25 | $event->count = $results->getTotalHits(); 26 | 27 | if ($results->hasAggregations()) { 28 | $event->setCustomPaginationParameter('aggregations', $results->getAggregations()); 29 | } 30 | 31 | $event->setCustomPaginationParameter('resultSet', $results); 32 | $event->items = $results->getResults(); 33 | $event->stopPropagation(); 34 | } 35 | } 36 | 37 | public static function getSubscribedEvents(): array 38 | { 39 | return [ 40 | 'knp_pager.items' => ['items', 0], /* triggers before a standard array subscriber*/ 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/PropelQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof ModelCriteria) { 16 | // process count 17 | $countQuery = clone $event->target; 18 | $countQuery 19 | ->limit(0) 20 | ->offset(0) 21 | ; 22 | if ($event->options[PaginatorInterface::DISTINCT]) { 23 | $countQuery->distinct(); 24 | } 25 | $event->count = (int) $countQuery->count(); 26 | // process items 27 | $result = null; 28 | if ($event->count) { 29 | $resultQuery = clone $event->target; 30 | if ($event->options[PaginatorInterface::DISTINCT]) { 31 | $resultQuery->distinct(); 32 | } 33 | $resultQuery 34 | ->offset($event->getOffset()) 35 | ->limit($event->getLimit()) 36 | ; 37 | $result = $resultQuery->find(); 38 | } else { 39 | $result = []; // count is 0 40 | } 41 | $event->items = $result; 42 | $event->stopPropagation(); 43 | } 44 | } 45 | 46 | public static function getSubscribedEvents(): array 47 | { 48 | return [ 49 | 'knp_pager.items' => ['items', 0], 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Pagination/PaginationInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface PaginationInterface extends Countable, Traversable, ArrayAccess 18 | { 19 | public function setCurrentPageNumber(int $pageNumber): void; 20 | 21 | /** 22 | * Get currently used page number 23 | */ 24 | public function getCurrentPageNumber(): int; 25 | 26 | public function setItemNumberPerPage(int $numItemsPerPage): void; 27 | 28 | /** 29 | * Get number of items per page 30 | */ 31 | public function getItemNumberPerPage(): int; 32 | 33 | public function setTotalItemCount(int $numTotal): void; 34 | 35 | /** 36 | * Get total item number available 37 | */ 38 | public function getTotalItemCount(): int; 39 | 40 | /** 41 | * @param iterable $items 42 | */ 43 | public function setItems(iterable $items): void; 44 | 45 | /** 46 | * Get current items 47 | * 48 | * @return iterable 49 | */ 50 | public function getItems(): iterable; 51 | 52 | /** 53 | * @param array $options 54 | */ 55 | public function setPaginatorOptions(array $options): void; 56 | 57 | /** 58 | * Get pagination alias 59 | */ 60 | public function getPaginatorOption(string $name): mixed; 61 | 62 | /** 63 | * @param array $parameters 64 | */ 65 | public function setCustomParameters(array $parameters): void; 66 | 67 | /** 68 | * Return custom parameter 69 | */ 70 | public function getCustomParameter(string $name): mixed; 71 | } 72 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/ItemsEvent.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public array $options; 22 | 23 | /** 24 | * Items result 25 | */ 26 | public mixed $items = null; 27 | 28 | /** 29 | * Count result 30 | */ 31 | public int $count; 32 | 33 | /** 34 | * @var array 35 | */ 36 | private array $customPaginationParams = []; 37 | 38 | public function __construct( 39 | private readonly int $offset, 40 | private readonly int $limit, 41 | private readonly ArgumentAccessInterface $argumentAccess 42 | ) { 43 | } 44 | 45 | public function setCustomPaginationParameter(string $name, mixed $value): void 46 | { 47 | $this->customPaginationParams[$name] = $value; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getCustomPaginationParameters(): array 54 | { 55 | return $this->customPaginationParams; 56 | } 57 | 58 | public function unsetCustomPaginationParameter(string $name): void 59 | { 60 | if (isset($this->customPaginationParams[$name])) { 61 | unset($this->customPaginationParams[$name]); 62 | } 63 | } 64 | 65 | public function getLimit(): int 66 | { 67 | return $this->limit; 68 | } 69 | 70 | public function getOffset(): int 71 | { 72 | return $this->offset; 73 | } 74 | 75 | public function getArgumentAccess(): ArgumentAccessInterface 76 | { 77 | return $this->argumentAccess; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ORM/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof Query) { 19 | return; 20 | } 21 | $event->stopPropagation(); 22 | 23 | $useOutputWalkers = $event->options['wrap-queries'] ?? false; 24 | 25 | $event->target 26 | ->setFirstResult($event->getOffset()) 27 | ->setMaxResults($event->getLimit()) 28 | ->setHint(CountWalker::HINT_DISTINCT, $event->options['distinct']) 29 | ; 30 | 31 | $fetchJoinCollection = true; 32 | if ($event->target->hasHint(self::HINT_FETCH_JOIN_COLLECTION)) { 33 | $fetchJoinCollection = $event->target->getHint(self::HINT_FETCH_JOIN_COLLECTION); 34 | } else if (isset($event->options['distinct'])) { 35 | $fetchJoinCollection = $event->options['distinct']; 36 | } 37 | 38 | $paginator = new Paginator($event->target, $fetchJoinCollection); 39 | $paginator->setUseOutputWalkers($useOutputWalkers); 40 | if (($count = $event->target->getHint(self::HINT_COUNT)) !== false) { 41 | $event->count = (int) $count; 42 | } else { 43 | $event->count = count($paginator); 44 | } 45 | $event->items = iterator_to_array($paginator); 46 | } 47 | 48 | public static function getSubscribedEvents(): array 49 | { 50 | return [ 51 | 'knp_pager.items' => ['items', 0], 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/PropelQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | getArgumentAccess(); 15 | 16 | // Check if the result has already been sorted by another sort subscriber 17 | $customPaginationParameters = $event->getCustomPaginationParameters(); 18 | if (!empty($customPaginationParameters['sorted']) ) { 19 | return; 20 | } 21 | 22 | $query = $event->target; 23 | if ($query instanceof \ModelCriteria) { 24 | $event->setCustomPaginationParameter('sorted', true); 25 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 26 | $sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]; 27 | if (null !== $sortField && $argumentAccess->has($sortField)) { 28 | $part = $argumentAccess->get($sortField); 29 | $direction = (null !== $sortDir && $argumentAccess->has($sortDir) && strtolower($argumentAccess->get($sortDir)) === 'asc') 30 | ? 'asc' : 'desc'; 31 | 32 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 33 | throw new InvalidValueException("Cannot sort by: [{$argumentAccess->get($sortField)}] this field is not in allow list."); 34 | } 35 | 36 | $query->orderBy($part, $direction); 37 | } 38 | } 39 | } 40 | 41 | public static function getSubscribedEvents(): array 42 | { 43 | return [ 44 | 'knp_pager.items' => ['items', 1], 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/DBALQueryBuilderSubscriber.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DBALQueryBuilderSubscriber implements EventSubscriberInterface 16 | { 17 | public function __construct(private readonly Connection $connection) 18 | { 19 | } 20 | 21 | public function items(ItemsEvent $event): void 22 | { 23 | if ($event->target instanceof QueryBuilder) { 24 | $target = $event->target; 25 | 26 | $qb = $this 27 | ->connection 28 | ->createQueryBuilder() 29 | ->select('COUNT(*)') 30 | ->from('(' . (clone $target)->resetOrderBy()->getSQL() . ')', 'tmp') 31 | ->setParameters($target->getParameters(), $target->getParameterTypes()) 32 | ; 33 | 34 | $compat = $qb->executeQuery(); 35 | $event->count = method_exists($compat, 'fetchColumn') ? $compat->fetchColumn(0) : $compat->fetchOne(); 36 | 37 | // if there is results 38 | $event->items = []; 39 | if ($event->count) { 40 | $qb = clone $target; 41 | $qb 42 | ->setFirstResult($event->getOffset()) 43 | ->setMaxResults($event->getLimit()) 44 | ; 45 | 46 | $event->items = $qb 47 | ->executeQuery() 48 | ->fetchAllAssociative() 49 | ; 50 | } 51 | 52 | $event->stopPropagation(); 53 | } 54 | } 55 | 56 | public static function getSubscribedEvents(): array 57 | { 58 | return [ 59 | 'knp_pager.items' => ['items', 10 /*make sure to transform before any further modifications*/], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/Doctrine/ODM/MongoDB/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof Query) { 14 | // items 15 | $type = $event->target->getType(); 16 | if ($type !== Query::TYPE_FIND) { 17 | throw new \UnexpectedValueException('ODM query must be a FIND type query'); 18 | } 19 | static $reflectionProperty; 20 | if (is_null($reflectionProperty)) { 21 | $reflectionClass = new \ReflectionClass(Query::class); 22 | $reflectionProperty = $reflectionClass->getProperty('query'); 23 | $reflectionProperty->setAccessible(true); 24 | } 25 | $queryOptions = $reflectionProperty->getValue($event->target); 26 | $resultCount = clone $event->target; 27 | $queryOptions['type'] = Query::TYPE_COUNT; 28 | $reflectionProperty->setValue($resultCount, $queryOptions); 29 | $event->count = $resultCount->execute(); 30 | 31 | $queryOptions = $reflectionProperty->getValue($event->target); 32 | $queryOptions['type'] = Query::TYPE_FIND; 33 | $queryOptions['limit'] = $event->getLimit(); 34 | $queryOptions['skip'] = $event->getOffset(); 35 | $resultQuery = clone $event->target; 36 | $reflectionProperty->setValue($resultQuery, $queryOptions); 37 | $cursor = $resultQuery->execute(); 38 | 39 | $event->items = []; 40 | // iterator_to_array for GridFS results in 1 item 41 | foreach ($cursor as $item) { 42 | $event->items[] = $item; 43 | } 44 | $event->stopPropagation(); 45 | } 46 | } 47 | 48 | public static function getSubscribedEvents(): array 49 | { 50 | return [ 51 | 'knp_pager.items' => ['items', 0], 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/ElasticaQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | getArgumentAccess(); 17 | 18 | // Check if the result has already been sorted by another sort subscriber 19 | $customPaginationParameters = $event->getCustomPaginationParameters(); 20 | if (!empty($customPaginationParameters['sorted']) ) { 21 | return; 22 | } 23 | 24 | if (is_array($event->target) && 2 === count($event->target) && reset($event->target) instanceof SearchableInterface && end($event->target) instanceof Query) { 25 | [$searchable, $query] = $event->target; 26 | $event->setCustomPaginationParameter('sorted', true); 27 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 28 | $sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]; 29 | if (null !== $sortField && $argumentAccess->has($sortField)) { 30 | $field = $argumentAccess->get($sortField); 31 | $dir = null !== $sortDir && $argumentAccess->has($sortDir) && strtolower($argumentAccess->get($sortDir)) === 'asc' ? 'asc' : 'desc'; 32 | 33 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($field, $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 34 | throw new InvalidValueException(sprintf('Cannot sort by: [%s] this field is not in allow list.', $field)); 35 | } 36 | 37 | $query->setSort([ 38 | $field => ['order' => $dir], 39 | ]); 40 | } 41 | } 42 | } 43 | 44 | public static function getSubscribedEvents(): array 45 | { 46 | return [ 47 | 'knp_pager.items' => ['items', 1], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Paginate/PaginationSubscriber.php: -------------------------------------------------------------------------------- 1 | setPagination(new SlidingPagination); 20 | $event->stopPropagation(); 21 | } 22 | 23 | public function before(BeforeEvent $event): void 24 | { 25 | // Do not lazy-load more than once 26 | if ($this->isLoaded) { 27 | return; 28 | } 29 | 30 | /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ 31 | $dispatcher = $event->getEventDispatcher(); 32 | // hook all standard subscribers 33 | $dispatcher->addSubscriber(new ArraySubscriber); 34 | $dispatcher->addSubscriber(new Callback\CallbackSubscriber); 35 | $dispatcher->addSubscriber(new Doctrine\ORM\QueryBuilderSubscriber); 36 | $dispatcher->addSubscriber(new Doctrine\ORM\QuerySubscriber); 37 | $dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QueryBuilderSubscriber); 38 | $dispatcher->addSubscriber(new Doctrine\ODM\MongoDB\QuerySubscriber); 39 | $dispatcher->addSubscriber(new Doctrine\ODM\PHPCR\QueryBuilderSubscriber); 40 | $dispatcher->addSubscriber(new Doctrine\ODM\PHPCR\QuerySubscriber); 41 | $dispatcher->addSubscriber(new Doctrine\CollectionSubscriber); 42 | if (null !== $connection = $event->getConnection()) { 43 | $dispatcher->addSubscriber(new Doctrine\DBALQueryBuilderSubscriber($connection)); 44 | } 45 | $dispatcher->addSubscriber(new PropelQuerySubscriber); 46 | $dispatcher->addSubscriber(new SolariumQuerySubscriber()); 47 | $dispatcher->addSubscriber(new ElasticaQuerySubscriber()); 48 | 49 | $this->isLoaded = true; 50 | } 51 | 52 | public static function getSubscribedEvents(): array 53 | { 54 | return [ 55 | 'knp_pager.before' => ['before', 0], 56 | 'knp_pager.pagination' => ['pagination', 0], 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/PaginatorInterface.php: -------------------------------------------------------------------------------- 1 | $options less used options: 42 | * bool $distinct default true for distinction of results 43 | * string $alias pagination alias, default none 44 | * array $sortFieldAllowList sortable allow list for target fields being paginated 45 | * 46 | * @return PaginationInterface 47 | */ 48 | public function paginate(mixed $target, int $page = 1, ?int $limit = null, array $options = []): PaginationInterface; 49 | } 50 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/SolariumQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class SolariumQuerySubscriber implements EventSubscriberInterface 16 | { 17 | public function items(ItemsEvent $event): void 18 | { 19 | $argumentAccess = $event->getArgumentAccess(); 20 | 21 | // Check if the result has already been sorted by another sort subscriber 22 | $customPaginationParameters = $event->getCustomPaginationParameters(); 23 | if (!empty($customPaginationParameters['sorted']) ) { 24 | return; 25 | } 26 | 27 | if (is_array($event->target) && 2 === count($event->target)) { 28 | $values = array_values($event->target); 29 | [$client, $query] = $values; 30 | 31 | if ($client instanceof \Solarium\Client && $query instanceof \Solarium\QueryType\Select\Query\Query) { 32 | $event->setCustomPaginationParameter('sorted', true); 33 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 34 | if (null !== $sortField && $argumentAccess->has($sortField)) { 35 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 36 | throw new InvalidValueException("Cannot sort by: [{$argumentAccess->get($sortField)}] this field is not in allow list."); 37 | } 38 | 39 | $query->addSort($argumentAccess->get($sortField), $this->getSortDirection($event)); 40 | } 41 | } 42 | } 43 | } 44 | 45 | public static function getSubscribedEvents(): array 46 | { 47 | return [ 48 | // trigger before the pagination subscriber 49 | 'knp_pager.items' => ['items', 1], 50 | ]; 51 | } 52 | 53 | private function getSortDirection(ItemsEvent $event): string 54 | { 55 | $argumentAccess = $event->getArgumentAccess(); 56 | 57 | $sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]; 58 | 59 | return null !== $sortDir && $argumentAccess->has($sortDir) && 60 | strtolower($argumentAccess->get($sortDir)) === 'asc' ? 'asc' : 'desc'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knplabs/knp-components", 3 | "type": "library", 4 | "description": "Knplabs component library", 5 | "keywords": [ 6 | "components", 7 | "paginator", 8 | "pager", 9 | "knp", 10 | "knplabs" 11 | ], 12 | "homepage": "https://github.com/KnpLabs/knp-components", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "KnpLabs Team", 17 | "homepage": "https://knplabs.com" 18 | }, 19 | { 20 | "name": "Symfony Community", 21 | "homepage": "https://github.com/KnpLabs/knp-components/contributors" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.1", 26 | "symfony/event-dispatcher-contracts": "^3.0" 27 | }, 28 | "require-dev": { 29 | "ext-pdo_sqlite": "*", 30 | "doctrine/dbal": "^3.8 || ^4.0", 31 | "doctrine/mongodb-odm": "^2.5.5", 32 | "doctrine/orm": "^2.13 || ^3.0", 33 | "doctrine/phpcr-odm": "^1.8 || ^2.0", 34 | "jackalope/jackalope-doctrine-dbal": "^1.12 || ^2.0", 35 | "phpunit/phpunit": "^10.5 || ^11.3", 36 | "propel/propel1": "^1.7", 37 | "ruflin/elastica": "^7.0", 38 | "solarium/solarium": "^6.0", 39 | "symfony/http-foundation": "^5.4.38 || ^6.4.4 || ^7.0", 40 | "symfony/http-kernel": "^5.4.38 || ^6.4.4 || ^7.0", 41 | "symfony/property-access": "^5.4.38 || ^6.4.4 || ^7.0" 42 | }, 43 | "suggest": { 44 | "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", 45 | "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", 46 | "doctrine/orm": "to allow usage pagination with Doctrine ORM", 47 | "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", 48 | "propel/propel1": "to allow usage pagination with Propel ORM", 49 | "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", 50 | "solarium/solarium": "to allow usage pagination with Solarium Client", 51 | "symfony/http-foundation": "to retrieve arguments from Request", 52 | "symfony/property-access": "to allow sorting arrays" 53 | }, 54 | "conflict": { 55 | "doctrine/dbal": "<3.8" 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "5.x-dev" 60 | } 61 | }, 62 | "autoload": { 63 | "psr-4": { 64 | "Knp\\Component\\": "src/Knp/Component" 65 | } 66 | }, 67 | "autoload-dev": { 68 | "psr-4": { 69 | "Test\\": "tests/Test" 70 | } 71 | }, 72 | "scripts": { 73 | "test": "phpunit" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Filtration/PropelQuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target; 15 | $argumentAccess = $event->getArgumentAccess(); 16 | 17 | if ($query instanceof \ModelCriteria) { 18 | if (!$argumentAccess->has($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME])) { 19 | return; 20 | } 21 | if ($argumentAccess->has($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME])) { 22 | $columns = $argumentAccess->get($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME]); 23 | } elseif (!empty($event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS])) { 24 | $columns = $event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS]; 25 | } else { 26 | return; 27 | } 28 | if (is_string($columns) && str_contains($columns, ',')) { 29 | $columns = explode(',', $columns); 30 | } 31 | $columns = (array) $columns; 32 | if (isset($event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) { 33 | foreach ($columns as $column) { 34 | if (!in_array($column, $event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) { 35 | throw new InvalidValueException("Cannot filter by: [$column] this field is not in allow list"); 36 | } 37 | } 38 | } 39 | $value = $argumentAccess->get($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]); 40 | $criteria = \Criteria::EQUAL; 41 | if (str_contains($value, '*')) { 42 | $value = str_replace('*', '%', $value); 43 | $criteria = \Criteria::LIKE; 44 | } 45 | foreach ($columns as $column) { 46 | if (str_contains($column, '.')) { 47 | $query->where($column.$criteria.'?', $value); 48 | } else { 49 | $query->{'filterBy'.$column}($value, $criteria); 50 | } 51 | } 52 | } 53 | } 54 | 55 | public static function getSubscribedEvents(): array 56 | { 57 | return [ 58 | 'knp_pager.items' => ['items', 0], 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/Doctrine/ODM/MongoDB/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | getArgumentAccess(); 16 | 17 | // Check if the result has already been sorted by another sort subscriber 18 | $customPaginationParameters = $event->getCustomPaginationParameters(); 19 | if (!empty($customPaginationParameters['sorted']) ) { 20 | return; 21 | } 22 | 23 | if ($event->target instanceof Query) { 24 | $event->setCustomPaginationParameter('sorted', true); 25 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 26 | $sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]; 27 | if (null !== $sortField && $argumentAccess->has($sortField)) { 28 | $field = $argumentAccess->get($sortField); 29 | $dir = null !== $sortDir && strtolower($argumentAccess->get($sortDir)) === 'asc' ? 1 : -1; 30 | 31 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && (!in_array($field, $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]))) { 32 | throw new InvalidValueException("Cannot sort by: [$field] this field is not in allow list."); 33 | } 34 | static $reflectionProperty; 35 | if (is_null($reflectionProperty)) { 36 | $reflectionClass = new \ReflectionClass(Query::class); 37 | $reflectionProperty = $reflectionClass->getProperty('query'); 38 | $reflectionProperty->setAccessible(true); 39 | } 40 | $queryOptions = $reflectionProperty->getValue($event->target); 41 | 42 | // handle multi sort 43 | $sortFields = explode('+', $field); 44 | $sortOption = []; 45 | foreach ($sortFields as $sortField) { 46 | $sortOption[$sortField] = $dir; 47 | } 48 | 49 | $queryOptions['sort'] = $sortOption; 50 | $reflectionProperty->setValue($event->target, $queryOptions); 51 | } 52 | } 53 | } 54 | 55 | public static function getSubscribedEvents(): array 56 | { 57 | return [ 58 | 'knp_pager.items' => ['items', 1], 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Filtration/Doctrine/ORM/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | target instanceof Query) { 18 | return; 19 | } 20 | $argumentAccess = $event->getArgumentAccess(); 21 | 22 | if (!$argumentAccess->has($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME])) { 23 | return; 24 | } 25 | $filterValue = $argumentAccess->get($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]); 26 | if ((empty($filterValue) && $filterValue !== '0')) { 27 | return; 28 | } 29 | $filterName = null; 30 | if ($argumentAccess->has($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME])) { 31 | $filterName = $argumentAccess->get($event->options[PaginatorInterface::FILTER_FIELD_PARAMETER_NAME]); 32 | } 33 | if (!empty($filterName)) { 34 | $columns = $filterName; 35 | } elseif (!empty($event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS])) { 36 | $columns = $event->options[PaginatorInterface::DEFAULT_FILTER_FIELDS]; 37 | } else { 38 | return; 39 | } 40 | $value = $argumentAccess->get($event->options[PaginatorInterface::FILTER_VALUE_PARAMETER_NAME]); 41 | if (str_contains($value, '*')) { 42 | $value = str_replace('*', '%', $value); 43 | } 44 | if (is_string($columns) && str_contains($columns, ',')) { 45 | $columns = explode(',', $columns); 46 | } 47 | $columns = (array) $columns; 48 | if (isset($event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) { 49 | foreach ($columns as $column) { 50 | if (!in_array($column, $event->options[PaginatorInterface::FILTER_FIELD_ALLOW_LIST])) { 51 | throw new InvalidValueException("Cannot filter by: [$column] this field is not in allow list"); 52 | } 53 | } 54 | } 55 | $event->target 56 | ->setHint(WhereWalker::HINT_PAGINATOR_FILTER_VALUE, $value) 57 | ->setHint(WhereWalker::HINT_PAGINATOR_FILTER_COLUMNS, $columns); 58 | QueryHelper::addCustomTreeWalker($event->target, WhereWalker::class); 59 | } 60 | 61 | public static function getSubscribedEvents(): array 62 | { 63 | return [ 64 | 'knp_pager.items' => ['items', 0], 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Pagination/SlidingPagination.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class SlidingPagination extends AbstractPagination implements \Stringable 18 | { 19 | /** 20 | * Pagination page range 21 | */ 22 | private int $range = 5; 23 | 24 | /** 25 | * Closure which is executed to render pagination 26 | */ 27 | public ?Closure $renderer = null; 28 | 29 | public function setPageRange(int $range): void 30 | { 31 | $this->range = abs($range); 32 | } 33 | 34 | /** 35 | * Renders the pagination 36 | */ 37 | public function __toString(): string 38 | { 39 | $data = $this->getPaginationData(); 40 | $output = 'override in order to render a template'; 41 | if ($this->renderer instanceof Closure) { 42 | $output = call_user_func($this->renderer, $data); 43 | } 44 | 45 | return (string) $output; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getPaginationData(): array 52 | { 53 | $pageCount = (int) ceil($this->totalCount / $this->numItemsPerPage); 54 | $current = $this->currentPageNumber; 55 | 56 | if ($this->range > $pageCount) { 57 | $this->range = $pageCount; 58 | } 59 | 60 | $delta = ceil($this->range / 2); 61 | 62 | if ($current - $delta > $pageCount - $this->range) { 63 | $pages = range($pageCount - $this->range + 1, $pageCount); 64 | } else { 65 | if ($current - $delta < 0) { 66 | $delta = $current; 67 | } 68 | 69 | $offset = $current - $delta; 70 | $pages = range($offset + 1, $offset + $this->range); 71 | } 72 | 73 | $viewData = [ 74 | 'last' => $pageCount, 75 | 'current' => $current, 76 | 'numItemsPerPage' => $this->numItemsPerPage, 77 | 'first' => 1, 78 | 'pageCount' => $pageCount, 79 | 'totalCount' => $this->totalCount, 80 | ]; 81 | $viewData = array_merge($viewData, $this->paginatorOptions, $this->customParameters); 82 | 83 | if ($current - 1 > 0) { 84 | $viewData['previous'] = $current - 1; 85 | } 86 | 87 | if ($current + 1 <= $pageCount) { 88 | $viewData['next'] = $current + 1; 89 | } 90 | $viewData['pagesInRange'] = $pages; 91 | $viewData['firstPageInRange'] = min($pages); 92 | $viewData['lastPageInRange'] = max($pages); 93 | 94 | if ($this->getItems() !== null) { 95 | $viewData['currentItemCount'] = $this->count(); 96 | $viewData['firstItemNumber'] = (($current - 1) * $this->numItemsPerPage) + 1; 97 | $viewData['lastItemNumber'] = $viewData['firstItemNumber'] + $viewData['currentItemCount'] - 1; 98 | } 99 | 100 | return $viewData; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/Doctrine/ORM/QuerySubscriber.php: -------------------------------------------------------------------------------- 1 | getArgumentAccess(); 18 | 19 | // Check if the result has already been sorted by another sort subscriber 20 | $customPaginationParameters = $event->getCustomPaginationParameters(); 21 | if (!empty($customPaginationParameters['sorted']) ) { 22 | return; 23 | } 24 | 25 | if ($event->target instanceof Query) { 26 | $event->setCustomPaginationParameter('sorted', true); 27 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 28 | $sortDir = $event->options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]; 29 | if (null !== $sortField && $argumentAccess->has($sortField)) { 30 | $dir = null !== $sortDir && $argumentAccess->has($sortDir) && strtolower($argumentAccess->get($sortDir)) === 'asc' ? 'asc' : 'desc'; 31 | 32 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 33 | throw new InvalidValueException("Cannot sort by: [{$argumentAccess->get($sortField)}] this field is not in allow list."); 34 | } 35 | 36 | $sortFieldParameterNames = $argumentAccess->get($sortField); 37 | $fields = []; 38 | $aliases = []; 39 | if (!is_string($sortFieldParameterNames)) { 40 | throw new InvalidValueException('Cannot sort with array parameter.'); 41 | } 42 | 43 | foreach (explode('+', $sortFieldParameterNames) as $sortFieldParameterName) { 44 | $parts = explode('.', $sortFieldParameterName, 2); 45 | 46 | // We have to prepend the field. Otherwise, OrderByWalker will add 47 | // the order-by items in the wrong order 48 | array_unshift($fields, end($parts)); 49 | array_unshift($aliases, 2 <= count($parts) ? reset($parts) : false); 50 | } 51 | 52 | $event->target 53 | ->setHint(OrderByWalker::HINT_PAGINATOR_SORT_DIRECTION, $dir) 54 | ->setHint(OrderByWalker::HINT_PAGINATOR_SORT_FIELD, $fields) 55 | ->setHint(OrderByWalker::HINT_PAGINATOR_SORT_ALIAS, $aliases) 56 | ; 57 | 58 | QueryHelper::addCustomTreeWalker($event->target, OrderByWalker::class); 59 | } 60 | } 61 | } 62 | 63 | public static function getSubscribedEvents(): array 64 | { 65 | return [ 66 | 'knp_pager.items' => ['items', 1], 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/Doctrine/ORM/Query/OrderByWalker.php: -------------------------------------------------------------------------------- 1 | _getQuery(); 40 | $fields = (array)$query->getHint(self::HINT_PAGINATOR_SORT_FIELD); 41 | $aliases = (array)$query->getHint(self::HINT_PAGINATOR_SORT_ALIAS); 42 | 43 | $components = $this->getQueryComponents(); 44 | foreach ($fields as $index => $field) { 45 | if (!$field) { 46 | continue; 47 | } 48 | 49 | $alias = $aliases[$index]; 50 | if ($alias !== false) { 51 | if (!array_key_exists($alias, $components)) { 52 | throw new InvalidValueException("There is no component aliased by [$alias] in the given Query"); 53 | } 54 | $meta = $components[$alias]; 55 | if (!$meta['metadata']->hasField($field)) { 56 | throw new InvalidValueException("There is no such field [$field] in the given Query component, aliased by [$alias]"); 57 | } 58 | } elseif (!array_key_exists($field, $components)) { 59 | throw new InvalidValueException("There is no component field [$field] in the given Query"); 60 | } 61 | 62 | $direction = $query->getHint(self::HINT_PAGINATOR_SORT_DIRECTION); 63 | if ($alias !== false) { 64 | $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field); 65 | $pathExpression->type = PathExpression::TYPE_STATE_FIELD; 66 | } else { 67 | $pathExpression = $field; 68 | } 69 | 70 | $orderByItem = new OrderByItem($pathExpression); 71 | $orderByItem->type = $direction; 72 | 73 | if ($AST->orderByClause) { 74 | $set = false; 75 | foreach ($AST->orderByClause->orderByItems as $item) { 76 | if ( 77 | $item->expression instanceof PathExpression && 78 | $item->expression->identificationVariable === $alias && 79 | $item->expression->field === $field 80 | ) { 81 | $item->type = $direction; 82 | $set = true; 83 | break; 84 | } 85 | } 86 | if (!$set) { 87 | array_unshift($AST->orderByClause->orderByItems, $orderByItem); 88 | } 89 | } else { 90 | $AST->orderByClause = new OrderByClause([$orderByItem]); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Pagination/AbstractPagination.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class AbstractPagination implements Iterator, PaginationInterface 14 | { 15 | protected ?int $currentPageNumber = null; 16 | protected ?int $numItemsPerPage = null; 17 | /** @var iterable|object */ 18 | protected iterable $items = []; 19 | protected ?int $totalCount = null; 20 | /** @var array */ 21 | protected ?array $paginatorOptions = null; 22 | /** @var array */ 23 | protected ?array $customParameters = null; 24 | 25 | public function rewind(): void 26 | { 27 | if (is_object($this->items)) { 28 | $items = get_mangled_object_vars($this->items); 29 | reset($items); 30 | $this->items = new \ArrayObject($items); 31 | } else { 32 | reset($this->items); 33 | } 34 | } 35 | 36 | public function current(): mixed 37 | { 38 | return current($this->items); 39 | } 40 | 41 | public function key(): string|int|null 42 | { 43 | if (is_object($this->items)) { 44 | $items = get_mangled_object_vars($this->items); 45 | 46 | return key($items); 47 | } 48 | 49 | return key($this->items); 50 | } 51 | 52 | public function next(): void 53 | { 54 | next($this->items); 55 | } 56 | 57 | public function valid(): bool 58 | { 59 | return null !== $this->key(); 60 | } 61 | 62 | public function count(): int 63 | { 64 | return count($this->items); 65 | } 66 | 67 | public function setCustomParameters(array $parameters): void 68 | { 69 | $this->customParameters = $parameters; 70 | } 71 | 72 | public function getCustomParameter(string $name): mixed 73 | { 74 | return $this->customParameters[$name] ?? null; 75 | } 76 | 77 | public function setCurrentPageNumber(int $pageNumber): void 78 | { 79 | $this->currentPageNumber = $pageNumber; 80 | } 81 | 82 | public function getCurrentPageNumber(): int 83 | { 84 | return $this->currentPageNumber; 85 | } 86 | 87 | public function setItemNumberPerPage(int $numItemsPerPage): void 88 | { 89 | $this->numItemsPerPage = $numItemsPerPage; 90 | } 91 | 92 | public function getItemNumberPerPage(): int 93 | { 94 | return $this->numItemsPerPage; 95 | } 96 | 97 | public function setTotalItemCount(int $numTotal): void 98 | { 99 | $this->totalCount = $numTotal; 100 | } 101 | 102 | public function getTotalItemCount(): int 103 | { 104 | return $this->totalCount; 105 | } 106 | 107 | public function setPaginatorOptions(array $options): void 108 | { 109 | $this->paginatorOptions = $options; 110 | } 111 | 112 | public function getPaginatorOption(string $name): mixed 113 | { 114 | return $this->paginatorOptions[$name] ?? null; 115 | } 116 | 117 | public function setItems(iterable $items): void 118 | { 119 | $this->items = $items; 120 | } 121 | 122 | public function getItems(): iterable 123 | { 124 | return $this->items; 125 | } 126 | 127 | /** 128 | * @param string|int|float|bool|null $offset 129 | */ 130 | public function offsetExists($offset): bool 131 | { 132 | if ($this->items instanceof \ArrayIterator) { 133 | return array_key_exists($offset, iterator_to_array($this->items)); 134 | } 135 | 136 | return array_key_exists($offset, $this->items); 137 | } 138 | 139 | /** 140 | * @param string|int|float|bool|null $offset 141 | */ 142 | public function offsetGet($offset): mixed 143 | { 144 | return $this->items[$offset]; 145 | } 146 | 147 | /** 148 | * @param string|int|float|bool|null $offset 149 | */ 150 | public function offsetSet($offset, mixed $value): void 151 | { 152 | if (null === $offset) { 153 | $this->items[] = $value; 154 | } else { 155 | $this->items[$offset] = $value; 156 | } 157 | } 158 | 159 | /** 160 | * @param string|int|float|bool|null $offset 161 | */ 162 | public function offsetUnset($offset): void 163 | { 164 | unset($this->items[$offset]); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Sortable/ArraySubscriber.php: -------------------------------------------------------------------------------- 1 | enableMagicCall()->getPropertyAccessor(); 31 | } 32 | 33 | $this->propertyAccessor = $accessor; 34 | } 35 | 36 | public function items(ItemsEvent $event): void 37 | { 38 | $argumentAccess = $event->getArgumentAccess(); 39 | 40 | // Check if the result has already been sorted by another sort subscriber 41 | $customPaginationParameters = $event->getCustomPaginationParameters(); 42 | if (!empty($customPaginationParameters['sorted']) ) { 43 | return; 44 | } 45 | $sortField = $event->options[PaginatorInterface::SORT_FIELD_PARAMETER_NAME]; 46 | if (!is_array($event->target) || null === $sortField || !$argumentAccess->has($sortField)) { 47 | return; 48 | } 49 | 50 | $event->setCustomPaginationParameter('sorted', true); 51 | 52 | if (isset($event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST]) && !in_array($argumentAccess->get($sortField), $event->options[PaginatorInterface::SORT_FIELD_ALLOW_LIST])) { 53 | throw new InvalidValueException("Cannot sort by: [{$argumentAccess->get($sortField)}] this field is not in allow list."); 54 | } 55 | 56 | $sortFunction = $event->options['sortFunction'] ?? [$this, 'proxySortFunction']; 57 | $sortField = $argumentAccess->get($sortField); 58 | 59 | // compatibility layer 60 | if ($sortField[0] === '.') { 61 | $sortField = substr($sortField, 1); 62 | } 63 | 64 | call_user_func_array($sortFunction, [ 65 | &$event->target, 66 | $sortField, 67 | $this->getSortDirection($event), 68 | ]); 69 | } 70 | 71 | private function getSortDirection(ItemsEvent $event): string 72 | { 73 | $argumentAccess = $event->getArgumentAccess(); 74 | $options = $event->options; 75 | 76 | if (!$argumentAccess->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) { 77 | return 'desc'; 78 | } 79 | $direction = $argumentAccess->get($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME]); 80 | if (strtolower($direction) === 'asc') { 81 | return 'asc'; 82 | } 83 | 84 | return 'desc'; 85 | } 86 | 87 | private function proxySortFunction(mixed &$target, string $sortField, string $sortDirection): bool 88 | { 89 | $this->currentSortingField = $sortField; 90 | $this->sortDirection = $sortDirection; 91 | 92 | return usort($target, [$this, 'sortFunction']); 93 | } 94 | 95 | private function sortFunction(object|array $object1, object|array $object2): int 96 | { 97 | if (null === $this->propertyAccessor) { 98 | throw new \UnexpectedValueException('You need symfony/property-access component to use this sorting function'); 99 | } 100 | 101 | if (!$this->propertyAccessor->isReadable($object1, $this->currentSortingField) || !$this->propertyAccessor->isReadable($object2, $this->currentSortingField)) { 102 | return 0; 103 | } 104 | 105 | try { 106 | $fieldValue1 = $this->propertyAccessor->getValue($object1, $this->currentSortingField); 107 | } catch (UnexpectedTypeException) { 108 | return -1 * $this->getSortCoefficient(); 109 | } 110 | 111 | try { 112 | $fieldValue2 = $this->propertyAccessor->getValue($object2, $this->currentSortingField); 113 | } catch (UnexpectedTypeException) { 114 | return $this->getSortCoefficient(); 115 | } 116 | 117 | if (is_string($fieldValue1)) { 118 | $fieldValue1 = mb_strtolower($fieldValue1); 119 | } 120 | 121 | if (is_string($fieldValue2)) { 122 | $fieldValue2 = mb_strtolower($fieldValue2); 123 | } 124 | 125 | if ($fieldValue1 === $fieldValue2) { 126 | return 0; 127 | } 128 | 129 | return ($fieldValue1 > $fieldValue2 ? 1 : -1) * $this->getSortCoefficient(); 130 | } 131 | 132 | private function getSortCoefficient(): int 133 | { 134 | return $this->sortDirection === 'asc' ? 1 : -1; 135 | } 136 | 137 | public static function getSubscribedEvents(): array 138 | { 139 | return [ 140 | 'knp_pager.items' => ['items', 1], 141 | ]; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Paginator.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $defaultOptions = [ 27 | self::PAGE_PARAMETER_NAME => 'page', 28 | self::SORT_FIELD_PARAMETER_NAME => 'sort', 29 | self::SORT_DIRECTION_PARAMETER_NAME => 'direction', 30 | self::FILTER_FIELD_PARAMETER_NAME => 'filterParam', 31 | self::FILTER_VALUE_PARAMETER_NAME => 'filterValue', 32 | self::DISTINCT => true, 33 | self::PAGE_OUT_OF_RANGE => self::PAGE_OUT_OF_RANGE_IGNORE, 34 | self::DEFAULT_LIMIT => self::DEFAULT_LIMIT_VALUE, 35 | ]; 36 | 37 | public function __construct( 38 | private readonly EventDispatcherInterface $eventDispatcher, 39 | private ArgumentAccessInterface $argumentAccess, 40 | private readonly ?Connection $connection = null 41 | ) { 42 | } 43 | 44 | /** 45 | * Override the default paginator options to be reused for paginations 46 | * 47 | * @param array $options 48 | */ 49 | public function setDefaultPaginatorOptions(array $options): void 50 | { 51 | $this->defaultOptions = \array_merge($this->defaultOptions, $options); 52 | } 53 | 54 | /** 55 | * @param array $options 56 | * 57 | * @return PaginationInterface 58 | */ 59 | public function paginate($target, int $page = 1, ?int $limit = null, array $options = []): PaginationInterface 60 | { 61 | if ($page <= 0) { 62 | throw PageNumberInvalidException::create($page); 63 | } 64 | 65 | $limit ??= (int) $this->defaultOptions[self::DEFAULT_LIMIT]; 66 | if ($limit <= 0) { 67 | throw PageLimitInvalidException::create($limit); 68 | } 69 | 70 | $offset = ($page - 1) * $limit; 71 | $options = \array_merge($this->defaultOptions, $options); 72 | 73 | // normalize default sort field 74 | if (isset($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]) && is_array($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME])) { 75 | $options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME] = implode('+', $options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]); 76 | } 77 | 78 | $argumentAccess = $this->argumentAccess; 79 | 80 | // default sort field and direction are set based on options (if available) 81 | if (isset($options[self::DEFAULT_SORT_FIELD_NAME]) && !$argumentAccess->has($options[self::SORT_FIELD_PARAMETER_NAME])) { 82 | $argumentAccess->set($options[self::SORT_FIELD_PARAMETER_NAME], $options[self::DEFAULT_SORT_FIELD_NAME]); 83 | 84 | if (!$argumentAccess->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) { 85 | $argumentAccess->set($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME], $options[PaginatorInterface::DEFAULT_SORT_DIRECTION] ?? 'asc'); 86 | } 87 | } 88 | 89 | // before pagination start 90 | $beforeEvent = new Event\BeforeEvent($this->eventDispatcher, $this->argumentAccess, $this->connection); 91 | $beforeEvent->options = &$options; 92 | $this->eventDispatcher->dispatch($beforeEvent, 'knp_pager.before'); 93 | // items 94 | $itemsEvent = new Event\ItemsEvent($offset, $limit, $this->argumentAccess); 95 | $itemsEvent->options = &$options; 96 | $itemsEvent->target = &$target; 97 | $this->eventDispatcher->dispatch($itemsEvent, 'knp_pager.items'); 98 | if (!$itemsEvent->isPropagationStopped()) { 99 | throw new \RuntimeException('One of listeners must count and slice given target'); 100 | } 101 | if ($page > ceil($itemsEvent->count / $limit)) { 102 | $pageOutOfRangeOption = $options[PaginatorInterface::PAGE_OUT_OF_RANGE] ?? $this->defaultOptions[PaginatorInterface::PAGE_OUT_OF_RANGE]; 103 | if ($pageOutOfRangeOption === PaginatorInterface::PAGE_OUT_OF_RANGE_FIX && $itemsEvent->count > 0) { 104 | // replace page number out of range with max page 105 | return $this->paginate($target, (int) ceil($itemsEvent->count / $limit), $limit, $options); 106 | } 107 | if ($pageOutOfRangeOption === self::PAGE_OUT_OF_RANGE_THROW_EXCEPTION && $page > 1) { 108 | throw new PageNumberOutOfRangeException( 109 | sprintf('Page number: %d is out of range.', $page), 110 | (int) ceil($itemsEvent->count / $limit) 111 | ); 112 | } 113 | } 114 | 115 | // pagination initialization event 116 | $paginationEvent = new Event\PaginationEvent; 117 | $paginationEvent->target = &$target; 118 | $paginationEvent->options = &$options; 119 | $this->eventDispatcher->dispatch($paginationEvent, 'knp_pager.pagination'); 120 | if (!$paginationEvent->isPropagationStopped()) { 121 | throw new \RuntimeException('One of listeners must create pagination view'); 122 | } 123 | // pagination class can be different, with different rendering methods 124 | $paginationView = $paginationEvent->getPagination(); 125 | $paginationView->setCustomParameters($itemsEvent->getCustomPaginationParameters()); 126 | $paginationView->setCurrentPageNumber($page); 127 | $paginationView->setItemNumberPerPage($limit); 128 | $paginationView->setTotalItemCount($itemsEvent->count); 129 | $paginationView->setPaginatorOptions($options); 130 | $paginationView->setItems($itemsEvent->items); 131 | 132 | // after 133 | $afterEvent = new Event\AfterEvent($paginationView); 134 | $this->eventDispatcher->dispatch($afterEvent, 'knp_pager.after'); 135 | 136 | return $paginationView; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Knp/Component/Pager/Event/Subscriber/Filtration/Doctrine/ORM/Query/WhereWalker.php: -------------------------------------------------------------------------------- 1 | _getQuery(); 49 | $queriedValue = $query->getHint(self::HINT_PAGINATOR_FILTER_VALUE); 50 | $columns = $query->getHint(self::HINT_PAGINATOR_FILTER_COLUMNS); 51 | $filterCaseInsensitive = $query->getHint(self::HINT_PAGINATOR_FILTER_CASE_INSENSITIVE); 52 | $components = $this->getQueryComponents(); 53 | $filterExpressions = []; 54 | $expressions = []; 55 | foreach ($columns as $column) { 56 | $alias = false; 57 | $parts = explode('.', $column, 2); 58 | $field = end($parts); 59 | if (2 <= count($parts)) { 60 | $alias = trim(reset($parts)); 61 | if (!array_key_exists($alias, $components)) { 62 | throw new InvalidValueException("There is no component aliased by [$alias] in the given Query"); 63 | } 64 | $meta = $components[$alias]; 65 | if (!$meta['metadata']->hasField($field)) { 66 | throw new InvalidValueException("There is no such field [$field] in the given Query component, aliased by [$alias]"); 67 | } 68 | $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD, $alias, $field); 69 | $pathExpression->type = PathExpression::TYPE_STATE_FIELD; 70 | } else { 71 | if (!array_key_exists($field, $components)) { 72 | throw new InvalidValueException("There is no component field [$field] in the given Query"); 73 | } 74 | $pathExpression = $components[$field]['resultVariable']; 75 | } 76 | $expression = new ConditionalPrimary(); 77 | if (isset($meta) && $meta['metadata']->getTypeOfField($field) === 'boolean') { 78 | if (in_array(strtolower($queriedValue), ['true', 'false'])) { 79 | $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue)); 80 | } elseif (is_numeric($queriedValue)) { 81 | $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::BOOLEAN, $queriedValue == '1' ? 'true' : 'false')); 82 | } else { 83 | continue; 84 | } 85 | unset($meta); 86 | } elseif (is_numeric($queriedValue) 87 | && ( 88 | !isset($meta) 89 | || in_array( 90 | $meta['metadata']->getTypeOfField($field), 91 | [ 92 | Type::SMALLINT, 93 | Type::INTEGER, 94 | Type::BIGINT, 95 | Type::FLOAT, 96 | Type::DECIMAL, 97 | ], 98 | true 99 | ) 100 | ) 101 | ) { 102 | $expression->simpleConditionalExpression = new ComparisonExpression($pathExpression, '=', new Literal(Literal::NUMERIC, $queriedValue)); 103 | } else { 104 | $likePathExpression = $pathExpression; 105 | $likeQueriedValue = $queriedValue; 106 | 107 | if ($filterCaseInsensitive) { 108 | $lower = new LowerFunction('lower'); 109 | $lower->stringPrimary = $pathExpression; 110 | $likePathExpression = $lower; 111 | $likeQueriedValue = strtolower($queriedValue); 112 | } 113 | 114 | $expression->simpleConditionalExpression = new LikeExpression($likePathExpression, new Literal(Literal::STRING, $likeQueriedValue)); 115 | } 116 | $filterExpressions[] = $expression->simpleConditionalExpression; 117 | $expressions[] = $expression; 118 | } 119 | if (count($expressions) > 1) { 120 | $conditionalPrimary = new ConditionalExpression($expressions); 121 | } elseif (count($expressions) > 0) { 122 | $conditionalPrimary = reset($expressions); 123 | } else { 124 | return; 125 | } 126 | if ($AST->whereClause) { 127 | if ($AST->whereClause->conditionalExpression instanceof ConditionalTerm) { 128 | if (!$this->termContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) { 129 | array_unshift( 130 | $AST->whereClause->conditionalExpression->conditionalFactors, 131 | $this->createPrimaryFromNode($conditionalPrimary) 132 | ); 133 | } 134 | } elseif ($AST->whereClause->conditionalExpression instanceof ConditionalPrimary) { 135 | if (!$this->primaryContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) { 136 | $AST->whereClause->conditionalExpression = new ConditionalTerm([ 137 | $this->createPrimaryFromNode($conditionalPrimary), 138 | $AST->whereClause->conditionalExpression, 139 | ]); 140 | } 141 | } elseif ($AST->whereClause->conditionalExpression instanceof ConditionalExpression) { 142 | if (!$this->expressionContainsFilter($AST->whereClause->conditionalExpression, $filterExpressions)) { 143 | $previousPrimary = new ConditionalPrimary(); 144 | $previousPrimary->conditionalExpression = $AST->whereClause->conditionalExpression; 145 | $AST->whereClause->conditionalExpression = new ConditionalTerm([ 146 | $this->createPrimaryFromNode($conditionalPrimary), 147 | $previousPrimary, 148 | ]); 149 | } 150 | } 151 | } else { 152 | $AST->whereClause = new WhereClause( 153 | $conditionalPrimary 154 | ); 155 | } 156 | } 157 | 158 | /** 159 | * @param ConditionalExpression $node 160 | * @param Node[] $filterExpressions 161 | * @return bool 162 | */ 163 | private function expressionContainsFilter(ConditionalExpression $node, array $filterExpressions): bool 164 | { 165 | foreach ($node->conditionalTerms as $conditionalTerm) { 166 | if ($conditionalTerm instanceof ConditionalTerm && $this->termContainsFilter($conditionalTerm, $filterExpressions)) { 167 | return true; 168 | } 169 | if ($conditionalTerm instanceof ConditionalPrimary && $this->primaryContainsFilter($conditionalTerm, $filterExpressions)) { 170 | return true; 171 | } 172 | } 173 | 174 | return false; 175 | } 176 | 177 | /** 178 | * @param ConditionalTerm $node 179 | * @param Node[] $filterExpressions 180 | * @return bool 181 | */ 182 | private function termContainsFilter(ConditionalTerm $node, array $filterExpressions): bool 183 | { 184 | foreach ($node->conditionalFactors as $conditionalFactor) { 185 | if ($conditionalFactor instanceof ConditionalFactor) { 186 | if ($this->factorContainsFilter($conditionalFactor, $filterExpressions)) { 187 | return true; 188 | } 189 | } elseif ($conditionalFactor instanceof ConditionalPrimary) { 190 | if ($this->primaryContainsFilter($conditionalFactor, $filterExpressions)) { 191 | return true; 192 | } 193 | } 194 | } 195 | 196 | return false; 197 | } 198 | 199 | /** 200 | * @param ConditionalFactor $node 201 | * @param Node[] $filterExpressions 202 | * @return bool 203 | */ 204 | private function factorContainsFilter(ConditionalFactor $node, array $filterExpressions): bool 205 | { 206 | if ($node->conditionalPrimary instanceof ConditionalPrimary && $node->not === false) { 207 | return $this->primaryContainsFilter($node->conditionalPrimary, $filterExpressions); 208 | } 209 | 210 | return false; 211 | } 212 | 213 | /** 214 | * @param ConditionalPrimary $node 215 | * @param Node[] $filterExpressions 216 | * @return bool 217 | */ 218 | private function primaryContainsFilter(ConditionalPrimary $node, array $filterExpressions): bool 219 | { 220 | if (($node->simpleConditionalExpression instanceof LikeExpression || $node->simpleConditionalExpression instanceof ComparisonExpression) && $node->isSimpleConditionalExpression()) { 221 | return $this->isExpressionInFilterExpressions($node->simpleConditionalExpression, $filterExpressions); 222 | } 223 | if ($node->isConditionalExpression()) { 224 | if ($node->conditionalExpression instanceof ConditionalExpression) { 225 | return $this->expressionContainsFilter($node->conditionalExpression, $filterExpressions); 226 | } elseif ($node->conditionalExpression instanceof ConditionalTerm) { 227 | return $this->termContainsFilter($node->conditionalExpression, $filterExpressions); 228 | } 229 | } 230 | 231 | return false; 232 | } 233 | 234 | /** 235 | * @param Node $node 236 | * @param Node[] $filterExpressions 237 | * @return bool 238 | */ 239 | private function isExpressionInFilterExpressions(Node $node, array $filterExpressions): bool 240 | { 241 | foreach ($filterExpressions as $filterExpression) { 242 | if ((string) $filterExpression === (string) $node) { 243 | return true; 244 | } 245 | } 246 | 247 | return false; 248 | } 249 | 250 | private function createPrimaryFromNode(ConditionalPrimary|ConditionalExpression $node): ConditionalPrimary 251 | { 252 | if ($node instanceof ConditionalPrimary) { 253 | $conditionalPrimary = $node; 254 | } else { 255 | $conditionalPrimary = new ConditionalPrimary(); 256 | $conditionalPrimary->conditionalExpression = $node; 257 | } 258 | 259 | return $conditionalPrimary; 260 | } 261 | } 262 | --------------------------------------------------------------------------------