AbstractCursorPaginator.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. <?php
  2. namespace Illuminate\Pagination;
  3. use ArrayAccess;
  4. use Closure;
  5. use Exception;
  6. use Illuminate\Contracts\Support\Htmlable;
  7. use Illuminate\Database\Eloquent\Model;
  8. use Illuminate\Database\Eloquent\Relations\Pivot;
  9. use Illuminate\Http\Resources\Json\JsonResource;
  10. use Illuminate\Support\Arr;
  11. use Illuminate\Support\Collection;
  12. use Illuminate\Support\Str;
  13. use Illuminate\Support\Traits\ForwardsCalls;
  14. use Illuminate\Support\Traits\Tappable;
  15. use Stringable;
  16. use Traversable;
  17. /**
  18. * @mixin \Illuminate\Support\Collection
  19. */
  20. abstract class AbstractCursorPaginator implements Htmlable, Stringable
  21. {
  22. use ForwardsCalls, Tappable;
  23. /**
  24. * All of the items being paginated.
  25. *
  26. * @var \Illuminate\Support\Collection
  27. */
  28. protected $items;
  29. /**
  30. * The number of items to be shown per page.
  31. *
  32. * @var int
  33. */
  34. protected $perPage;
  35. /**
  36. * The base path to assign to all URLs.
  37. *
  38. * @var string
  39. */
  40. protected $path = '/';
  41. /**
  42. * The query parameters to add to all URLs.
  43. *
  44. * @var array
  45. */
  46. protected $query = [];
  47. /**
  48. * The URL fragment to add to all URLs.
  49. *
  50. * @var string|null
  51. */
  52. protected $fragment;
  53. /**
  54. * The cursor string variable used to store the page.
  55. *
  56. * @var string
  57. */
  58. protected $cursorName = 'cursor';
  59. /**
  60. * The current cursor.
  61. *
  62. * @var \Illuminate\Pagination\Cursor|null
  63. */
  64. protected $cursor;
  65. /**
  66. * The paginator parameters for the cursor.
  67. *
  68. * @var array
  69. */
  70. protected $parameters;
  71. /**
  72. * The paginator options.
  73. *
  74. * @var array
  75. */
  76. protected $options;
  77. /**
  78. * The current cursor resolver callback.
  79. *
  80. * @var \Closure
  81. */
  82. protected static $currentCursorResolver;
  83. /**
  84. * Get the URL for a given cursor.
  85. *
  86. * @param \Illuminate\Pagination\Cursor|null $cursor
  87. * @return string
  88. */
  89. public function url($cursor)
  90. {
  91. // If we have any extra query string key / value pairs that need to be added
  92. // onto the URL, we will put them in query string form and then attach it
  93. // to the URL. This allows for extra information like sortings storage.
  94. $parameters = is_null($cursor) ? [] : [$this->cursorName => $cursor->encode()];
  95. if (count($this->query) > 0) {
  96. $parameters = array_merge($this->query, $parameters);
  97. }
  98. return $this->path()
  99. .(str_contains($this->path(), '?') ? '&' : '?')
  100. .Arr::query($parameters)
  101. .$this->buildFragment();
  102. }
  103. /**
  104. * Get the URL for the previous page.
  105. *
  106. * @return string|null
  107. */
  108. public function previousPageUrl()
  109. {
  110. if (is_null($previousCursor = $this->previousCursor())) {
  111. return null;
  112. }
  113. return $this->url($previousCursor);
  114. }
  115. /**
  116. * The URL for the next page, or null.
  117. *
  118. * @return string|null
  119. */
  120. public function nextPageUrl()
  121. {
  122. if (is_null($nextCursor = $this->nextCursor())) {
  123. return null;
  124. }
  125. return $this->url($nextCursor);
  126. }
  127. /**
  128. * Get the "cursor" that points to the previous set of items.
  129. *
  130. * @return \Illuminate\Pagination\Cursor|null
  131. */
  132. public function previousCursor()
  133. {
  134. if (is_null($this->cursor) ||
  135. ($this->cursor->pointsToPreviousItems() && ! $this->hasMore)) {
  136. return null;
  137. }
  138. if ($this->items->isEmpty()) {
  139. return null;
  140. }
  141. return $this->getCursorForItem($this->items->first(), false);
  142. }
  143. /**
  144. * Get the "cursor" that points to the next set of items.
  145. *
  146. * @return \Illuminate\Pagination\Cursor|null
  147. */
  148. public function nextCursor()
  149. {
  150. if ((is_null($this->cursor) && ! $this->hasMore) ||
  151. (! is_null($this->cursor) && $this->cursor->pointsToNextItems() && ! $this->hasMore)) {
  152. return null;
  153. }
  154. if ($this->items->isEmpty()) {
  155. return null;
  156. }
  157. return $this->getCursorForItem($this->items->last(), true);
  158. }
  159. /**
  160. * Get a cursor instance for the given item.
  161. *
  162. * @param \ArrayAccess|\stdClass $item
  163. * @param bool $isNext
  164. * @return \Illuminate\Pagination\Cursor
  165. */
  166. public function getCursorForItem($item, $isNext = true)
  167. {
  168. return new Cursor($this->getParametersForItem($item), $isNext);
  169. }
  170. /**
  171. * Get the cursor parameters for a given object.
  172. *
  173. * @param \ArrayAccess|\stdClass $item
  174. * @return array
  175. *
  176. * @throws \Exception
  177. */
  178. public function getParametersForItem($item)
  179. {
  180. return collect($this->parameters)
  181. ->filter()
  182. ->flip()
  183. ->map(function ($_, $parameterName) use ($item) {
  184. if ($item instanceof JsonResource) {
  185. $item = $item->resource;
  186. }
  187. if ($item instanceof Model &&
  188. ! is_null($parameter = $this->getPivotParameterForItem($item, $parameterName))) {
  189. return $parameter;
  190. } elseif ($item instanceof ArrayAccess || is_array($item)) {
  191. return $this->ensureParameterIsPrimitive(
  192. $item[$parameterName] ?? $item[Str::afterLast($parameterName, '.')]
  193. );
  194. } elseif (is_object($item)) {
  195. return $this->ensureParameterIsPrimitive(
  196. $item->{$parameterName} ?? $item->{Str::afterLast($parameterName, '.')}
  197. );
  198. }
  199. throw new Exception('Only arrays and objects are supported when cursor paginating items.');
  200. })->toArray();
  201. }
  202. /**
  203. * Get the cursor parameter value from a pivot model if applicable.
  204. *
  205. * @param \ArrayAccess|\stdClass $item
  206. * @param string $parameterName
  207. * @return string|null
  208. */
  209. protected function getPivotParameterForItem($item, $parameterName)
  210. {
  211. $table = Str::beforeLast($parameterName, '.');
  212. foreach ($item->getRelations() as $relation) {
  213. if ($relation instanceof Pivot && $relation->getTable() === $table) {
  214. return $this->ensureParameterIsPrimitive(
  215. $relation->getAttribute(Str::afterLast($parameterName, '.'))
  216. );
  217. }
  218. }
  219. }
  220. /**
  221. * Ensure the parameter is a primitive type.
  222. *
  223. * This can resolve issues that arise the developer uses a value object for an attribute.
  224. *
  225. * @param mixed $parameter
  226. * @return mixed
  227. */
  228. protected function ensureParameterIsPrimitive($parameter)
  229. {
  230. return is_object($parameter) && method_exists($parameter, '__toString')
  231. ? (string) $parameter
  232. : $parameter;
  233. }
  234. /**
  235. * Get / set the URL fragment to be appended to URLs.
  236. *
  237. * @param string|null $fragment
  238. * @return $this|string|null
  239. */
  240. public function fragment($fragment = null)
  241. {
  242. if (is_null($fragment)) {
  243. return $this->fragment;
  244. }
  245. $this->fragment = $fragment;
  246. return $this;
  247. }
  248. /**
  249. * Add a set of query string values to the paginator.
  250. *
  251. * @param array|string|null $key
  252. * @param string|null $value
  253. * @return $this
  254. */
  255. public function appends($key, $value = null)
  256. {
  257. if (is_null($key)) {
  258. return $this;
  259. }
  260. if (is_array($key)) {
  261. return $this->appendArray($key);
  262. }
  263. return $this->addQuery($key, $value);
  264. }
  265. /**
  266. * Add an array of query string values.
  267. *
  268. * @param array $keys
  269. * @return $this
  270. */
  271. protected function appendArray(array $keys)
  272. {
  273. foreach ($keys as $key => $value) {
  274. $this->addQuery($key, $value);
  275. }
  276. return $this;
  277. }
  278. /**
  279. * Add all current query string values to the paginator.
  280. *
  281. * @return $this
  282. */
  283. public function withQueryString()
  284. {
  285. if (! is_null($query = Paginator::resolveQueryString())) {
  286. return $this->appends($query);
  287. }
  288. return $this;
  289. }
  290. /**
  291. * Add a query string value to the paginator.
  292. *
  293. * @param string $key
  294. * @param string $value
  295. * @return $this
  296. */
  297. protected function addQuery($key, $value)
  298. {
  299. if ($key !== $this->cursorName) {
  300. $this->query[$key] = $value;
  301. }
  302. return $this;
  303. }
  304. /**
  305. * Build the full fragment portion of a URL.
  306. *
  307. * @return string
  308. */
  309. protected function buildFragment()
  310. {
  311. return $this->fragment ? '#'.$this->fragment : '';
  312. }
  313. /**
  314. * Load a set of relationships onto the mixed relationship collection.
  315. *
  316. * @param string $relation
  317. * @param array $relations
  318. * @return $this
  319. */
  320. public function loadMorph($relation, $relations)
  321. {
  322. $this->getCollection()->loadMorph($relation, $relations);
  323. return $this;
  324. }
  325. /**
  326. * Load a set of relationship counts onto the mixed relationship collection.
  327. *
  328. * @param string $relation
  329. * @param array $relations
  330. * @return $this
  331. */
  332. public function loadMorphCount($relation, $relations)
  333. {
  334. $this->getCollection()->loadMorphCount($relation, $relations);
  335. return $this;
  336. }
  337. /**
  338. * Get the slice of items being paginated.
  339. *
  340. * @return array
  341. */
  342. public function items()
  343. {
  344. return $this->items->all();
  345. }
  346. /**
  347. * Transform each item in the slice of items using a callback.
  348. *
  349. * @param callable $callback
  350. * @return $this
  351. */
  352. public function through(callable $callback)
  353. {
  354. $this->items->transform($callback);
  355. return $this;
  356. }
  357. /**
  358. * Get the number of items shown per page.
  359. *
  360. * @return int
  361. */
  362. public function perPage()
  363. {
  364. return $this->perPage;
  365. }
  366. /**
  367. * Get the current cursor being paginated.
  368. *
  369. * @return \Illuminate\Pagination\Cursor|null
  370. */
  371. public function cursor()
  372. {
  373. return $this->cursor;
  374. }
  375. /**
  376. * Get the query string variable used to store the cursor.
  377. *
  378. * @return string
  379. */
  380. public function getCursorName()
  381. {
  382. return $this->cursorName;
  383. }
  384. /**
  385. * Set the query string variable used to store the cursor.
  386. *
  387. * @param string $name
  388. * @return $this
  389. */
  390. public function setCursorName($name)
  391. {
  392. $this->cursorName = $name;
  393. return $this;
  394. }
  395. /**
  396. * Set the base path to assign to all URLs.
  397. *
  398. * @param string $path
  399. * @return $this
  400. */
  401. public function withPath($path)
  402. {
  403. return $this->setPath($path);
  404. }
  405. /**
  406. * Set the base path to assign to all URLs.
  407. *
  408. * @param string $path
  409. * @return $this
  410. */
  411. public function setPath($path)
  412. {
  413. $this->path = $path;
  414. return $this;
  415. }
  416. /**
  417. * Get the base path for paginator generated URLs.
  418. *
  419. * @return string|null
  420. */
  421. public function path()
  422. {
  423. return $this->path;
  424. }
  425. /**
  426. * Resolve the current cursor or return the default value.
  427. *
  428. * @param string $cursorName
  429. * @return \Illuminate\Pagination\Cursor|null
  430. */
  431. public static function resolveCurrentCursor($cursorName = 'cursor', $default = null)
  432. {
  433. if (isset(static::$currentCursorResolver)) {
  434. return call_user_func(static::$currentCursorResolver, $cursorName);
  435. }
  436. return $default;
  437. }
  438. /**
  439. * Set the current cursor resolver callback.
  440. *
  441. * @param \Closure $resolver
  442. * @return void
  443. */
  444. public static function currentCursorResolver(Closure $resolver)
  445. {
  446. static::$currentCursorResolver = $resolver;
  447. }
  448. /**
  449. * Get an instance of the view factory from the resolver.
  450. *
  451. * @return \Illuminate\Contracts\View\Factory
  452. */
  453. public static function viewFactory()
  454. {
  455. return Paginator::viewFactory();
  456. }
  457. /**
  458. * Get an iterator for the items.
  459. *
  460. * @return \ArrayIterator
  461. */
  462. public function getIterator(): Traversable
  463. {
  464. return $this->items->getIterator();
  465. }
  466. /**
  467. * Determine if the list of items is empty.
  468. *
  469. * @return bool
  470. */
  471. public function isEmpty()
  472. {
  473. return $this->items->isEmpty();
  474. }
  475. /**
  476. * Determine if the list of items is not empty.
  477. *
  478. * @return bool
  479. */
  480. public function isNotEmpty()
  481. {
  482. return $this->items->isNotEmpty();
  483. }
  484. /**
  485. * Get the number of items for the current page.
  486. *
  487. * @return int
  488. */
  489. public function count(): int
  490. {
  491. return $this->items->count();
  492. }
  493. /**
  494. * Get the paginator's underlying collection.
  495. *
  496. * @return \Illuminate\Support\Collection
  497. */
  498. public function getCollection()
  499. {
  500. return $this->items;
  501. }
  502. /**
  503. * Set the paginator's underlying collection.
  504. *
  505. * @param \Illuminate\Support\Collection $collection
  506. * @return $this
  507. */
  508. public function setCollection(Collection $collection)
  509. {
  510. $this->items = $collection;
  511. return $this;
  512. }
  513. /**
  514. * Get the paginator options.
  515. *
  516. * @return array
  517. */
  518. public function getOptions()
  519. {
  520. return $this->options;
  521. }
  522. /**
  523. * Determine if the given item exists.
  524. *
  525. * @param mixed $key
  526. * @return bool
  527. */
  528. public function offsetExists($key): bool
  529. {
  530. return $this->items->has($key);
  531. }
  532. /**
  533. * Get the item at the given offset.
  534. *
  535. * @param mixed $key
  536. * @return mixed
  537. */
  538. public function offsetGet($key): mixed
  539. {
  540. return $this->items->get($key);
  541. }
  542. /**
  543. * Set the item at the given offset.
  544. *
  545. * @param mixed $key
  546. * @param mixed $value
  547. * @return void
  548. */
  549. public function offsetSet($key, $value): void
  550. {
  551. $this->items->put($key, $value);
  552. }
  553. /**
  554. * Unset the item at the given key.
  555. *
  556. * @param mixed $key
  557. * @return void
  558. */
  559. public function offsetUnset($key): void
  560. {
  561. $this->items->forget($key);
  562. }
  563. /**
  564. * Render the contents of the paginator to HTML.
  565. *
  566. * @return string
  567. */
  568. public function toHtml()
  569. {
  570. return (string) $this->render();
  571. }
  572. /**
  573. * Make dynamic calls into the collection.
  574. *
  575. * @param string $method
  576. * @param array $parameters
  577. * @return mixed
  578. */
  579. public function __call($method, $parameters)
  580. {
  581. return $this->forwardCallTo($this->getCollection(), $method, $parameters);
  582. }
  583. /**
  584. * Render the contents of the paginator when casting to a string.
  585. *
  586. * @return string
  587. */
  588. public function __toString()
  589. {
  590. return (string) $this->render();
  591. }
  592. }