Skip to content

Commit

Permalink
Merge branch 'release/4.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Aug 26, 2024
2 parents c4f86c7 + a30eb76 commit 35ccb63
Show file tree
Hide file tree
Showing 11 changed files with 2,333 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. This projec

## Unreleased

## [4.2.0] - 2024-08-26

### Added

- [#37](https://github.com/laravel-json-api/eloquent/pull/37) Add Eloquent cursor pagination implementation.

## [4.1.0] - 2024-06-26

### Added
Expand Down
93 changes: 93 additions & 0 deletions src/Pagination/Cursor/Cursor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/*
* Copyright 2024 Cloud Creativity Limited
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Eloquent\Pagination\Cursor;

use InvalidArgumentException;

final readonly class Cursor
{
/**
* Cursor constructor.
*
* @param string|null $before
* @param string|null $after
* @param int|null $limit
*/
public function __construct(
private ?string $before = null,
private ?string $after = null,
private ?int $limit = null
) {
if (is_int($this->limit) && 1 > $this->limit) {
throw new InvalidArgumentException('Expecting a limit that is 1 or greater.');
}
}

/**
* @return bool
*/
public function isBefore(): bool
{
return !is_null($this->before);
}

/**
* @return string|null
*/
public function getBefore(): ?string
{
return $this->before;
}

/**
* @return bool
*/
public function isAfter(): bool
{
return !is_null($this->after) && !$this->isBefore();
}

/**
* @return string|null
*/
public function getAfter(): ?string
{
return $this->after;
}

/**
* Set a limit, if no limit is set on the cursor.
*
* @param int $limit
* @return Cursor
*/
public function withDefaultLimit(int $limit): self
{
if ($this->limit === null) {
return new self(
before: $this->before,
after: $this->after,
limit: $limit,
);
}

return $this;
}

/**
* @return int|null
*/
public function getLimit(): ?int
{
return $this->limit;
}
}
178 changes: 178 additions & 0 deletions src/Pagination/Cursor/CursorBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php
/*
* Copyright 2024 Cloud Creativity Limited
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Eloquent\Pagination\Cursor;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use LaravelJsonApi\Contracts\Schema\ID;
use LaravelJsonApi\Core\Schema\IdParser;

final class CursorBuilder
{
/**
* @var string
*/
private readonly string $keyName;

/**
* @var string
*/
private string $direction;

/**
* @var int|null
*/
private ?int $defaultPerPage = null;

/**
* @var bool
*/
private bool $withTotal;

/**
* @var bool
*/
private bool $keySort = true;

/**
* @var CursorParser
*/
private readonly CursorParser $parser;

/**
* CursorBuilder constructor.
*
* @param Builder|Relation $query the column to use for the cursor
* @param ID $id
* @param string|null $key the key column that the before/after cursors related to
*/
public function __construct(
private readonly Builder|Relation $query,
private readonly ID $id,
?string $key = null
) {
$this->keyName = $key ?: $this->id->key();
$this->parser = new CursorParser(IdParser::make($this->id), $this->keyName);
}

/**
* Set the default number of items per-page.
*
* If null, the default from the `Model::getPage()` method will be used.
*
* @return $this
*/
public function withDefaultPerPage(?int $perPage): self
{
$this->defaultPerPage = $perPage;

return $this;
}

/**
* @param bool $keySort
* @return $this
*/
public function withKeySort(bool $keySort = true): self
{
$this->keySort = $keySort;

return $this;
}

/**
* Set the query direction.
*
* @return $this
*/
public function withDirection(string $direction): self
{
if (\in_array($direction, ['asc', 'desc'])) {
$this->direction = $direction;

return $this;
}

throw new \InvalidArgumentException('Unexpected query direction.');
}

/**
* @param bool $withTotal
* @return $this
*/
public function withTotal(bool $withTotal): self
{
$this->withTotal = $withTotal;

return $this;
}

/**
* @param array<string> $columns
*/
public function paginate(Cursor $cursor, array $columns = ['*']): CursorPaginator
{
$cursor = $cursor->withDefaultLimit($this->getDefaultPerPage());

$this->applyKeySort();

$total = $this->getTotal();
$laravelPaginator = $this->query->cursorPaginate(
$cursor->getLimit(),
$columns,
'cursor',
$this->parser->decode($cursor),
);
$paginator = new CursorPaginator($this->parser, $laravelPaginator, $cursor, $total);

return $paginator->withCurrentPath();
}

/**
* @return void
*/
private function applyKeySort(): void
{
if (!$this->keySort) {
return;
}

if (
empty($this->query->getQuery()->orders)
|| collect($this->query->getQuery()->orders)
->whereIn('column', [$this->keyName, $this->query->qualifyColumn($this->keyName)])
->isEmpty()
) {
$this->query->orderBy($this->keyName, $this->direction);
}
}

/**
* @return int|null
*/
private function getTotal(): ?int
{
return $this->withTotal ? $this->query->count() : null;
}

/**
* @return int
*/
private function getDefaultPerPage(): int
{
if (is_int($this->defaultPerPage)) {
return $this->defaultPerPage;
}

return $this->query->getModel()->getPerPage();
}
}
Loading

0 comments on commit 35ccb63

Please sign in to comment.