vendor/knplabs/knp-components/src/Knp/Component/Pager/Paginator.php line 56

Open in your IDE?
  1. <?php
  2. namespace Knp\Component\Pager;
  3. use Knp\Component\Pager\Exception\PageLimitInvalidException;
  4. use Knp\Component\Pager\Exception\PageNumberInvalidException;
  5. use Knp\Component\Pager\Exception\PageNumberOutOfRangeException;
  6. use Knp\Component\Pager\Pagination\PaginationInterface;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Symfony\Component\HttpFoundation\RequestStack;
  9. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  10. /**
  11. * Paginator uses event dispatcher to trigger pagination
  12. * lifecycle events. Subscribers are expected to paginate
  13. * wanted target and finally it generates pagination view
  14. * which is only the result of paginator
  15. */
  16. final class Paginator implements PaginatorInterface
  17. {
  18. private EventDispatcherInterface $eventDispatcher;
  19. /**
  20. * Default options of paginator
  21. *
  22. * @var array<string, scalar>
  23. */
  24. private array $defaultOptions = [
  25. self::PAGE_PARAMETER_NAME => 'page',
  26. self::SORT_FIELD_PARAMETER_NAME => 'sort',
  27. self::SORT_DIRECTION_PARAMETER_NAME => 'direction',
  28. self::FILTER_FIELD_PARAMETER_NAME => 'filterParam',
  29. self::FILTER_VALUE_PARAMETER_NAME => 'filterValue',
  30. self::DISTINCT => true,
  31. self::PAGE_OUT_OF_RANGE => self::PAGE_OUT_OF_RANGE_IGNORE,
  32. self::DEFAULT_LIMIT => self::DEFAULT_LIMIT_VALUE,
  33. ];
  34. private ?RequestStack $requestStack;
  35. public function __construct(EventDispatcherInterface $eventDispatcher, RequestStack $requestStack = null)
  36. {
  37. $this->eventDispatcher = $eventDispatcher;
  38. $this->requestStack = $requestStack;
  39. }
  40. /**
  41. * Override the default paginator options
  42. * to be reused for paginations
  43. */
  44. public function setDefaultPaginatorOptions(array $options): void
  45. {
  46. $this->defaultOptions = \array_merge($this->defaultOptions, $options);
  47. }
  48. public function paginate($target, int $page = 1, int $limit = null, array $options = []): PaginationInterface
  49. {
  50. if ($page <= 0) {
  51. throw PageNumberInvalidException::create($page);
  52. }
  53. $limit = $limit ?? $this->defaultOptions[self::DEFAULT_LIMIT];
  54. if ($limit <= 0) {
  55. throw PageLimitInvalidException::create($limit);
  56. }
  57. $offset = ($page - 1) * $limit;
  58. $options = \array_merge($this->defaultOptions, $options);
  59. // normalize default sort field
  60. if (isset($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]) && is_array($options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME])) {
  61. $options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME] = implode('+', $options[PaginatorInterface::DEFAULT_SORT_FIELD_NAME]);
  62. }
  63. $request = null === $this->requestStack ? Request::createFromGlobals() : $this->requestStack->getCurrentRequest();
  64. // default sort field and direction are set based on options (if available)
  65. if (isset($options[self::DEFAULT_SORT_FIELD_NAME]) && !$request->query->has($options[self::SORT_FIELD_PARAMETER_NAME])) {
  66. $request->query->set($options[self::SORT_FIELD_PARAMETER_NAME], $options[self::DEFAULT_SORT_FIELD_NAME]);
  67. if (!$request->query->has($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME])) {
  68. $request->query->set($options[PaginatorInterface::SORT_DIRECTION_PARAMETER_NAME], $options[PaginatorInterface::DEFAULT_SORT_DIRECTION] ?? 'asc');
  69. }
  70. }
  71. // before pagination start
  72. $beforeEvent = new Event\BeforeEvent($this->eventDispatcher, $request);
  73. $this->eventDispatcher->dispatch($beforeEvent, 'knp_pager.before');
  74. // items
  75. $itemsEvent = new Event\ItemsEvent($offset, $limit);
  76. $itemsEvent->options = &$options;
  77. $itemsEvent->target = &$target;
  78. $this->eventDispatcher->dispatch($itemsEvent, 'knp_pager.items');
  79. if (!$itemsEvent->isPropagationStopped()) {
  80. throw new \RuntimeException('One of listeners must count and slice given target');
  81. }
  82. if ($page > ceil($itemsEvent->count / $limit)) {
  83. $pageOutOfRangeOption = $options[PaginatorInterface::PAGE_OUT_OF_RANGE] ?? $this->defaultOptions[PaginatorInterface::PAGE_OUT_OF_RANGE];
  84. if ($pageOutOfRangeOption === PaginatorInterface::PAGE_OUT_OF_RANGE_FIX && $itemsEvent->count > 0) {
  85. // replace page number out of range with max page
  86. return $this->paginate($target, (int) ceil($itemsEvent->count / $limit), $limit, $options);
  87. }
  88. if ($pageOutOfRangeOption === self::PAGE_OUT_OF_RANGE_THROW_EXCEPTION && $page > 1) {
  89. throw new PageNumberOutOfRangeException(
  90. sprintf('Page number: %d is out of range.', $page),
  91. (int) ceil($itemsEvent->count / $limit)
  92. );
  93. }
  94. }
  95. // pagination initialization event
  96. $paginationEvent = new Event\PaginationEvent;
  97. $paginationEvent->target = &$target;
  98. $paginationEvent->options = &$options;
  99. $this->eventDispatcher->dispatch($paginationEvent, 'knp_pager.pagination');
  100. if (!$paginationEvent->isPropagationStopped()) {
  101. throw new \RuntimeException('One of listeners must create pagination view');
  102. }
  103. // pagination class can be different, with different rendering methods
  104. $paginationView = $paginationEvent->getPagination();
  105. $paginationView->setCustomParameters($itemsEvent->getCustomPaginationParameters());
  106. $paginationView->setCurrentPageNumber($page);
  107. $paginationView->setItemNumberPerPage($limit);
  108. $paginationView->setTotalItemCount($itemsEvent->count);
  109. $paginationView->setPaginatorOptions($options);
  110. $paginationView->setItems($itemsEvent->items);
  111. // after
  112. $afterEvent = new Event\AfterEvent($paginationView);
  113. $this->eventDispatcher->dispatch($afterEvent, 'knp_pager.after');
  114. return $paginationView;
  115. }
  116. }