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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ $RECYCLE.BIN/
.env
.vagrant
Vagrantfile
user_guide_src/venv/
.python-version
user_guide_src/.python-version

#-------------------------
# Temporary Files
Expand Down
4 changes: 4 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ deptrac:
API:
- Format
- HTTP
- Database
- Model
- Pager
- URI
Cache:
- I18n
Controller:
Expand Down
151 changes: 150 additions & 1 deletion system/API/ResponseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@

namespace CodeIgniter\API;

use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Format\Format;
use CodeIgniter\Format\FormatterInterface;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Model;
use Throwable;

/**
* Provides common, more readable, methods to provide
Expand Down Expand Up @@ -321,7 +326,7 @@ protected function format($data = null)
// if we don't have a formatter, make one
$this->formatter ??= $format->getFormatter($mime);

$asHtml = $this->stringAsHtml ?? false;
$asHtml = property_exists($this, 'stringAsHtml') ? $this->stringAsHtml : false;

if (
($mime === 'application/json' && $asHtml && is_string($data))
Expand Down Expand Up @@ -360,4 +365,148 @@ protected function setResponseFormat(?string $format = null)

return $this;
}

// --------------------------------------------------------------------
// Pagination Methods
// --------------------------------------------------------------------

/**
* Paginates the given model or query builder and returns
* an array containing the paginated results along with
* metadata such as total items, total pages, current page,
* and items per page.
*
* The result would be in the following format:
* [
* 'data' => [...],
* 'meta' => [
* 'page' => 1,
* 'perPage' => 20,
* 'total' => 100,
* 'totalPages' => 5,
* ],
* 'links' => [
* 'self' => '/api/items?page=1&perPage=20',
* 'first' => '/api/items?page=1&perPage=20',
* 'last' => '/api/items?page=5&perPage=20',
* 'prev' => null,
* 'next' => '/api/items?page=2&perPage=20',
* ]
* ]
*/
protected function paginate(BaseBuilder|Model $resource, int $perPage = 20): ResponseInterface
{
try {
assert($this->request instanceof IncomingRequest);

$page = max(1, (int) ($this->request->getGet('page') ?? 1));

// If using a Model we can use its built-in paginate method
if ($resource instanceof Model) {
$data = $resource->paginate($perPage, 'default', $page);
$pager = $resource->pager;

$meta = [
'page' => $pager->getCurrentPage(),
'perPage' => $pager->getPerPage(),
'total' => $pager->getTotal(),
'totalPages' => $pager->getPageCount(),
];
} else {
// Query Builder, we need to handle pagination manually
$offset = ($page - 1) * $perPage;
$total = (clone $resource)->countAllResults();
$data = $resource->limit($perPage, $offset)->get()->getResultArray();

$meta = [
'page' => $page,
'perPage' => $perPage,
'total' => $total,
'totalPages' => (int) ceil($total / $perPage),
];
}

$links = $this->buildLinks($meta);

$this->response->setHeader('Link', $this->linkHeader($links));
$this->response->setHeader('X-Total-Count', (string) $meta['total']);

return $this->respond([
'data' => $data,
'meta' => $meta,
'links' => $links,
]);
} catch (DatabaseException $e) {
log_message('error', lang('RESTful.cannotPaginate') . ' ' . $e->getMessage());

return $this->failServerError(lang('RESTful.cannotPaginate'));
} catch (Throwable $e) {
log_message('error', lang('RESTful.paginateError') . ' ' . $e->getMessage());

return $this->failServerError(lang('RESTful.paginateError'));
}
}

/**
* Builds pagination links based on the current request URI and pagination metadata.
*
* @param array<string, int> $meta Pagination metadata (page, perPage, total, totalPages)
*
* @return array<string, string|null> Array of pagination links with relations as keys
*/
private function buildLinks(array $meta): array
{
assert($this->request instanceof IncomingRequest);

/** @var URI $uri */
$uri = current_url(true);
$query = $this->request->getGet();

$set = static function ($page) use ($uri, $query, $meta): string {
$params = $query;
$params['page'] = $page;

// Ensure perPage is in the links if it's not default
if (! isset($params['perPage']) && $meta['perPage'] !== 20) {
$params['perPage'] = $meta['perPage'];
}

return (string) (new URI((string) $uri))->setQuery(http_build_query($params));
};

$totalPages = max(1, (int) $meta['totalPages']);
$page = (int) $meta['page'];

return [
'self' => $set($page),
'first' => $set(1),
'last' => $set($totalPages),
'prev' => $page > 1 ? $set($page - 1) : null,
'next' => $page < $totalPages ? $set($page + 1) : null,
];
}

/**
* Formats the pagination links into a single Link header string
* for middleware/machine use.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
* @see https://datatracker.ietf.org/doc/html/rfc8288
*
* @param array<string, string|null> $links Pagination links with relations as keys
*
* @return string Formatted Link header value
*/
private function linkHeader(array $links): string
{
$parts = [];

foreach (['self', 'first', 'prev', 'next', 'last'] as $rel) {
if ($links[$rel] !== null && $links[$rel] !== '') {
$parts[] = "<{$links[$rel]}>; rel=\"{$rel}\"";
}
}

return implode(', ', $parts);
}
}
2 changes: 2 additions & 0 deletions system/Language/en/RESTful.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
// RESTful language settings
return [
'notImplemented' => '"{0}" action not implemented.',
'cannotPaginate' => 'Unable to retrieve paginated data.',
'paginateError' => 'An error occurred while paginating results.',
];
Loading
Loading