vendor/pagerfanta/pagerfanta/lib/Core/Pagerfanta.php line 246

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Pagerfanta;
  3. use Pagerfanta\Adapter\AdapterInterface;
  4. use Pagerfanta\Exception\LessThan1CurrentPageException;
  5. use Pagerfanta\Exception\LessThan1MaxPagesException;
  6. use Pagerfanta\Exception\LessThan1MaxPerPageException;
  7. use Pagerfanta\Exception\LogicException;
  8. use Pagerfanta\Exception\OutOfBoundsException;
  9. use Pagerfanta\Exception\OutOfRangeCurrentPageException;
  10. /**
  11. * @template T
  12. * @implements PagerfantaInterface<T>
  13. */
  14. class Pagerfanta implements PagerfantaInterface, \JsonSerializable
  15. {
  16. /**
  17. * @var AdapterInterface<T>
  18. */
  19. private AdapterInterface $adapter;
  20. private bool $allowOutOfRangePages = false;
  21. private bool $normalizeOutOfRangePages = false;
  22. /**
  23. * @phpstan-var positive-int
  24. */
  25. private int $maxPerPage = 10;
  26. /**
  27. * @phpstan-var positive-int
  28. */
  29. private int $currentPage = 1;
  30. /**
  31. * @phpstan-var int<0, max>|null
  32. */
  33. private ?int $nbResults = null;
  34. /**
  35. * @phpstan-var positive-int|null
  36. */
  37. private ?int $maxNbPages = null;
  38. /**
  39. * @phpstan-var iterable<array-key, T>|null
  40. */
  41. private ?iterable $currentPageResults = null;
  42. /**
  43. * @param AdapterInterface<T> $adapter
  44. */
  45. public function __construct(AdapterInterface $adapter)
  46. {
  47. $this->adapter = $adapter;
  48. }
  49. /**
  50. * @param AdapterInterface<T> $adapter
  51. *
  52. * @return self<T>
  53. */
  54. public static function createForCurrentPageWithMaxPerPage(AdapterInterface $adapter, int $currentPage, int $maxPerPage): self
  55. {
  56. $pagerfanta = new self($adapter);
  57. $pagerfanta->setMaxPerPage($maxPerPage);
  58. $pagerfanta->setCurrentPage($currentPage);
  59. return $pagerfanta;
  60. }
  61. /**
  62. * @return AdapterInterface<T>
  63. *
  64. * @deprecated to be removed in 4.0
  65. */
  66. public function getAdapter(): AdapterInterface
  67. {
  68. trigger_deprecation('pagerfanta/pagerfanta', '3.2', 'Retrieving the %s from "%s" implementations is deprecated and will be removed in 4.0.', AdapterInterface::class, PagerfantaInterface::class);
  69. return $this->adapter;
  70. }
  71. /**
  72. * @return $this<T>
  73. */
  74. public function setAllowOutOfRangePages(bool $allowOutOfRangePages): PagerfantaInterface
  75. {
  76. $this->allowOutOfRangePages = $allowOutOfRangePages;
  77. return $this;
  78. }
  79. public function getAllowOutOfRangePages(): bool
  80. {
  81. return $this->allowOutOfRangePages;
  82. }
  83. public function setNormalizeOutOfRangePages(bool $normalizeOutOfRangePages): PagerfantaInterface
  84. {
  85. $this->normalizeOutOfRangePages = $normalizeOutOfRangePages;
  86. return $this;
  87. }
  88. public function getNormalizeOutOfRangePages(): bool
  89. {
  90. return $this->normalizeOutOfRangePages;
  91. }
  92. /**
  93. * @return $this<T>
  94. *
  95. * @throws LessThan1MaxPerPageException if the page is less than 1
  96. */
  97. public function setMaxPerPage(int $maxPerPage): PagerfantaInterface
  98. {
  99. $this->filterMaxPerPage($maxPerPage);
  100. $this->maxPerPage = $maxPerPage;
  101. $this->resetForMaxPerPageChange();
  102. $this->filterOutOfRangeCurrentPage($this->currentPage);
  103. return $this;
  104. }
  105. private function filterMaxPerPage(int $maxPerPage): void
  106. {
  107. $this->checkMaxPerPage($maxPerPage);
  108. }
  109. /**
  110. * @throws LessThan1MaxPerPageException if the page is less than 1
  111. */
  112. private function checkMaxPerPage(int $maxPerPage): void
  113. {
  114. if ($maxPerPage < 1) {
  115. throw new LessThan1MaxPerPageException();
  116. }
  117. }
  118. private function resetForMaxPerPageChange(): void
  119. {
  120. $this->currentPageResults = null;
  121. }
  122. /**
  123. * @phpstan-return positive-int
  124. */
  125. public function getMaxPerPage(): int
  126. {
  127. return $this->maxPerPage;
  128. }
  129. /**
  130. * @return $this<T>
  131. *
  132. * @throws LessThan1CurrentPageException if the current page is less than 1
  133. * @throws OutOfRangeCurrentPageException if It is not allowed out of range pages and they are not normalized
  134. */
  135. public function setCurrentPage(int $currentPage): PagerfantaInterface
  136. {
  137. $this->currentPage = $this->filterCurrentPage($currentPage);
  138. $this->resetForCurrentPageChange();
  139. return $this;
  140. }
  141. /**
  142. * @phpstan-return positive-int
  143. */
  144. private function filterCurrentPage(int $currentPage): int
  145. {
  146. $this->checkCurrentPage($currentPage);
  147. return $this->filterOutOfRangeCurrentPage($currentPage);
  148. }
  149. /**
  150. * @throws LessThan1CurrentPageException if the current page is less than 1
  151. */
  152. private function checkCurrentPage(int $currentPage): void
  153. {
  154. if ($currentPage < 1) {
  155. throw new LessThan1CurrentPageException();
  156. }
  157. }
  158. /**
  159. * @phpstan-return positive-int
  160. */
  161. private function filterOutOfRangeCurrentPage(int $currentPage): int
  162. {
  163. if ($this->notAllowedCurrentPageOutOfRange($currentPage)) {
  164. return $this->normalizeOutOfRangeCurrentPage($currentPage);
  165. }
  166. return $currentPage;
  167. }
  168. private function notAllowedCurrentPageOutOfRange(int $currentPage): bool
  169. {
  170. return !$this->getAllowOutOfRangePages() && $this->currentPageOutOfRange($currentPage);
  171. }
  172. private function currentPageOutOfRange(int $currentPage): bool
  173. {
  174. return $currentPage > 1 && $currentPage > $this->getNbPages();
  175. }
  176. /**
  177. * @phpstan-return positive-int
  178. *
  179. * @throws OutOfRangeCurrentPageException if the page should not be normalized
  180. */
  181. private function normalizeOutOfRangeCurrentPage(int $currentPage): int
  182. {
  183. if ($this->getNormalizeOutOfRangePages()) {
  184. return $this->getNbPages();
  185. }
  186. throw new OutOfRangeCurrentPageException(sprintf('Page "%d" does not exist. The currentPage must be inferior to "%d"', $currentPage, $this->getNbPages()));
  187. }
  188. private function resetForCurrentPageChange(): void
  189. {
  190. $this->currentPageResults = null;
  191. }
  192. /**
  193. * @phpstan-return positive-int
  194. */
  195. public function getCurrentPage(): int
  196. {
  197. return $this->currentPage;
  198. }
  199. /**
  200. * @phpstan-return iterable<array-key, T>
  201. */
  202. public function getCurrentPageResults(): iterable
  203. {
  204. if (null === $this->currentPageResults) {
  205. $this->currentPageResults = $this->getCurrentPageResultsFromAdapter();
  206. }
  207. return $this->currentPageResults;
  208. }
  209. /**
  210. * @return iterable<array-key, T>
  211. */
  212. private function getCurrentPageResultsFromAdapter(): iterable
  213. {
  214. $offset = $this->calculateOffsetForCurrentPageResults();
  215. $length = $this->getMaxPerPage();
  216. return $this->adapter->getSlice($offset, $length);
  217. }
  218. /**
  219. * @phpstan-return int<0, max>
  220. */
  221. private function calculateOffsetForCurrentPageResults(): int
  222. {
  223. return ($this->getCurrentPage() - 1) * $this->getMaxPerPage();
  224. }
  225. /**
  226. * @phpstan-return int<0, max>
  227. */
  228. public function getCurrentPageOffsetStart(): int
  229. {
  230. return $this->getNbResults() ? $this->calculateOffsetForCurrentPageResults() + 1 : 0;
  231. }
  232. /**
  233. * @phpstan-return int<0, max>
  234. */
  235. public function getCurrentPageOffsetEnd(): int
  236. {
  237. return $this->hasNextPage() ? $this->getCurrentPage() * $this->getMaxPerPage() : $this->getNbResults();
  238. }
  239. /**
  240. * @phpstan-return int<0, max>
  241. */
  242. public function getNbResults(): int
  243. {
  244. if (null === $this->nbResults) {
  245. $this->nbResults = $this->adapter->getNbResults();
  246. }
  247. return $this->nbResults;
  248. }
  249. /**
  250. * @phpstan-return positive-int
  251. */
  252. public function getNbPages(): int
  253. {
  254. $nbPages = $this->calculateNbPages();
  255. if (0 === $nbPages) {
  256. return $this->minimumNbPages();
  257. }
  258. if (null !== $this->maxNbPages && $this->maxNbPages < $nbPages) {
  259. return $this->maxNbPages;
  260. }
  261. return $nbPages;
  262. }
  263. /**
  264. * @phpstan-return int<0, max>
  265. */
  266. private function calculateNbPages(): int
  267. {
  268. return (int) ceil($this->getNbResults() / $this->getMaxPerPage());
  269. }
  270. /**
  271. * @phpstan-return positive-int
  272. */
  273. private function minimumNbPages(): int
  274. {
  275. return 1;
  276. }
  277. /**
  278. * @return $this<T>
  279. *
  280. * @throws LessThan1MaxPagesException if the max number of pages is less than 1
  281. */
  282. public function setMaxNbPages(int $maxNbPages): PagerfantaInterface
  283. {
  284. if ($maxNbPages < 1) {
  285. throw new LessThan1MaxPagesException();
  286. }
  287. $this->maxNbPages = $maxNbPages;
  288. return $this;
  289. }
  290. /**
  291. * @return $this<T>
  292. */
  293. public function resetMaxNbPages(): PagerfantaInterface
  294. {
  295. $this->maxNbPages = null;
  296. return $this;
  297. }
  298. public function haveToPaginate(): bool
  299. {
  300. return $this->getNbResults() > $this->maxPerPage;
  301. }
  302. public function hasPreviousPage(): bool
  303. {
  304. return $this->currentPage > 1;
  305. }
  306. /**
  307. * @phpstan-return positive-int
  308. *
  309. * @throws LogicException if there is no previous page
  310. */
  311. public function getPreviousPage(): int
  312. {
  313. if (!$this->hasPreviousPage()) {
  314. throw new LogicException('There is no previous page.');
  315. }
  316. return $this->currentPage - 1;
  317. }
  318. public function hasNextPage(): bool
  319. {
  320. return $this->currentPage < $this->getNbPages();
  321. }
  322. /**
  323. * @phpstan-return positive-int
  324. *
  325. * @throws LogicException if there is no next page
  326. */
  327. public function getNextPage(): int
  328. {
  329. if (!$this->hasNextPage()) {
  330. throw new LogicException('There is no next page.');
  331. }
  332. return $this->currentPage + 1;
  333. }
  334. /**
  335. * @phpstan-return int<0, max>
  336. */
  337. public function count(): int
  338. {
  339. return $this->getNbResults();
  340. }
  341. /**
  342. * @return \Traversable<array-key, T>
  343. */
  344. public function getIterator(): \Traversable
  345. {
  346. $results = $this->getCurrentPageResults();
  347. if ($results instanceof \Iterator) {
  348. return $results;
  349. }
  350. if ($results instanceof \IteratorAggregate) {
  351. return $results->getIterator();
  352. }
  353. if (\is_array($results)) {
  354. return new \ArrayIterator($results);
  355. }
  356. throw new \InvalidArgumentException(sprintf('Cannot create iterator with page results of type "%s".', get_debug_type($results)));
  357. }
  358. public function jsonSerialize(): array
  359. {
  360. $results = $this->getCurrentPageResults();
  361. if ($results instanceof \Traversable) {
  362. return iterator_to_array($results);
  363. }
  364. return $results;
  365. }
  366. /**
  367. * Get page number of the item at specified position (1-based index).
  368. *
  369. * @phpstan-param positive-int $position
  370. *
  371. * @phpstan-return positive-int
  372. *
  373. * @throws OutOfBoundsException if the item is outside the result set
  374. */
  375. public function getPageNumberForItemAtPosition(int $position): int
  376. {
  377. if ($this->getNbResults() < $position) {
  378. throw new OutOfBoundsException(sprintf('Item requested at position %d, but there are only %d items.', $position, $this->getNbResults()));
  379. }
  380. return (int) ceil($position / $this->getMaxPerPage());
  381. }
  382. }