diff --git a/src/Phaseolies/Database/Entity/Builder.php b/src/Phaseolies/Database/Entity/Builder.php index 1cdd940..b83ead7 100644 --- a/src/Phaseolies/Database/Entity/Builder.php +++ b/src/Phaseolies/Database/Entity/Builder.php @@ -15,7 +15,8 @@ InteractsWithAggregateFucntion, CollectsRelations, InteractsWithNestedRelations, - InteractsWithConditionBinding + InteractsWithConditionBinding, + InteractsWithCursorPagination }; use Phaseolies\Utilities\Casts\CastToDate; use Phaseolies\Support\Facades\URL; @@ -35,6 +36,7 @@ class Builder use CollectsRelations; use InteractsWithNestedRelations; use InteractsWithConditionBinding; + use InteractsWithCursorPagination; /** * Holds the PDO instance for database connectivity. diff --git a/src/Phaseolies/Database/Entity/Query/Builder.php b/src/Phaseolies/Database/Entity/Query/Builder.php index d6a5ba3..3437e0e 100644 --- a/src/Phaseolies/Database/Entity/Query/Builder.php +++ b/src/Phaseolies/Database/Entity/Query/Builder.php @@ -14,6 +14,7 @@ class Builder use Grammar; use InteractsWithBuilderAggregateFucntion; use InteractsWithConditionBinding; + use InteractsWithCursorPagination; /** * Holds the PDO instance for database connectivity. diff --git a/src/Phaseolies/Database/Entity/Query/InteractsWithCursorPagination.php b/src/Phaseolies/Database/Entity/Query/InteractsWithCursorPagination.php new file mode 100644 index 0000000..900da60 --- /dev/null +++ b/src/Phaseolies/Database/Entity/Query/InteractsWithCursorPagination.php @@ -0,0 +1,113 @@ +' : '<'; + $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(); + } +} diff --git a/src/Phaseolies/Error/Handlers/JsonErrorHandler.php b/src/Phaseolies/Error/Handlers/JsonErrorHandler.php index f99a024..e230031 100644 --- a/src/Phaseolies/Error/Handlers/JsonErrorHandler.php +++ b/src/Phaseolies/Error/Handlers/JsonErrorHandler.php @@ -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); }