Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Phaseolies/Database/Entity/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
InteractsWithAggregateFucntion,
CollectsRelations,
InteractsWithNestedRelations,
InteractsWithConditionBinding
InteractsWithConditionBinding,
InteractsWithCursorPagination
};
use Phaseolies\Utilities\Casts\CastToDate;
use Phaseolies\Support\Facades\URL;
Expand All @@ -35,6 +36,7 @@ class Builder
use CollectsRelations;
use InteractsWithNestedRelations;
use InteractsWithConditionBinding;
use InteractsWithCursorPagination;

/**
* Holds the PDO instance for database connectivity.
Expand Down
1 change: 1 addition & 0 deletions src/Phaseolies/Database/Entity/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Builder
use Grammar;
use InteractsWithBuilderAggregateFucntion;
use InteractsWithConditionBinding;
use InteractsWithCursorPagination;

/**
* Holds the PDO instance for database connectivity.
Expand Down
113 changes: 113 additions & 0 deletions src/Phaseolies/Database/Entity/Query/InteractsWithCursorPagination.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace Phaseolies\Database\Entity\Query;

trait InteractsWithCursorPagination
{
/**
* Cursor pagination with forward only support
*
* @param int $perPage
* @param string $cursorColumn
* @param string $direction asc|desc
* @param string|null $cursor
* @return array
*/
public function cursorPaginate(int $perPage = 15, string $cursorColumn = 'id', string $direction = 'asc', ?string $cursor = null): array
{
$direction = strtolower($direction) === 'desc' ? 'desc' : 'asc';
$operator = $direction === 'asc' ? '>' : '<';
$cursorValue = $cursor ? $this->decodeCursor($cursor) : null;

$query = clone $this;

if ($cursorValue !== null) {
$query->where($cursorColumn, $operator, $cursorValue);
}

$query->orderBy($cursorColumn, strtoupper($direction));
$query->limit($perPage + 1);

$results = $query->get();
$items = $results->all();

$hasMore = count($items) > $perPage;

if ($hasMore) {
array_pop($items);
}

$nextCursor = null;
if ($hasMore && !empty($items)) {
$last = end($items);
$nextCursor = $this->encodeCursor($this->getCursorValue($last, $cursorColumn));
}

$path = $this->getCurrentPath();
$nextUrl = $nextCursor
? "{$path}?cursor={$nextCursor}&per_page={$perPage}&direction={$direction}"
: null;

return [
'data' => $items,
'per_page' => $perPage,
'next_cursor' => $nextCursor,
'next_page_url' => $nextUrl,
'has_more' => $hasMore,
'direction' => $direction,
];
}

/**
* Encode a cursor value to a URL-safe base64 string
*
* @param mixed $value
* @return string
*/
protected function encodeCursor(mixed $value): string
{
return base64_encode(json_encode(['v' => $value]));
}

/**
* Decode a cursor string back to its original value
*
* @param string $cursor
* @return mixed
*/
protected function decodeCursor(string $cursor): mixed
{
try {
$decoded = json_decode(base64_decode($cursor), true);
return $decoded['v'] ?? null;
} catch (\Throwable $e) {
return null;
}
}

/**
* Extract the cursor column value from a model or array item
*
* @param mixed $item
* @param string $column
* @return mixed
*/
protected function getCursorValue(mixed $item, string $column): mixed
{
if (is_array($item)) {
return $item[$column] ?? null;
}

return $item->{$column} ?? null;
}

/**
* Get current request path
*
* @return string
*/
protected function getCurrentPath(): string
{
return \Phaseolies\Support\Facades\URL::current();
}
}
5 changes: 3 additions & 2 deletions src/Phaseolies/Error/Handlers/JsonErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ public function handle(Throwable $exception): void

if ($exception instanceof HttpResponseException) {
$responseErrors = $exception->getValidationErrors();
$statusCode = $exception->getStatusCode() ?: 500;

$statusCode = (int) $exception->getStatusCode() ?: 500;
$renderer->render($exception, $statusCode, $responseErrors);
} else {
$statusCode = $exception->getCode() ?: 500;
$statusCode = (int) $exception->getCode() ?: 500;
$renderer->render($exception, $statusCode);
}

Expand Down
Loading