Skip to content

Instantly share code, notes, and snippets.

@bluefirex
Last active August 29, 2017 21:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bluefirex/cdfff8e76cc3c90898e62db1d86a5770 to your computer and use it in GitHub Desktop.
Save bluefirex/cdfff8e76cc3c90898e62db1d86a5770 to your computer and use it in GitHub Desktop.
Paginated Results in a JSON API
<?php
namespace Adepto\Asgard\API;
use Adepto\Asgard\Common\PaginatedResults;
use Purl\Url;
use Psr\Http\Message\ServerRequestInterface;
/**
* Represents a request on a paginated result set.
*
* @author bluefirex
* @version 1.0
*/
class PaginatedRequest {
protected $request;
protected $currentPage;
protected $perPage;
public function __construct(ServerRequestInterface $request) {
$this->request = $request;
$this->currentPage = max(1, $request->getQueryParams()['page'] ?? 1);
$this->perPage = $request->getQueryParams()['per_page'] ?? null;
}
/**
* Get the original request.
*
* @return ServerRequestInterface
*/
public function getRequest(): ServerRequestInterface {
return $this->request;
}
/**
* Get the currently requested page number
*
* @return int
*/
public function getCurrentPage(): int {
return $this->currentPage;
}
/**
* How many items per page were requested?
*
* @param int|integer $default Default value to fall back to, if no specific number was requested
*
* @return int
*/
public function getPerPage(int $default = 25): int {
return $this->perPage ?? $default;
}
/**
* Format results for a response. This will include meta information (page count,
* total count, etc.) as well as links to the previous and next pages.
*
* @param PaginatedResults $results Original Result Set
* @param array $formattedResults Formatted Result Set as array
* @param string $resultsKey Key under which the formatted result set should appear.
*
* @return array
*/
public function formatResultsForResponse(PaginatedResults $results, array $formattedResults, string $resultsKey = 'results'): array {
// Prepare URLs
list($previousPage, $nextPage) = $this->getURLsForResults($this->getRequest(), $results, $this->getCurrentPage());
return [
$resultsKey => $formattedResults,
'pages' => $results->getPages(),
'total' => $results->getTotal(),
'next_page' => $nextPage,
'previous_page' => $previousPage,
'current_page' => $this->getCurrentPage(),
];
}
/**
* Get the correct URLs for getting page-links in a paginated result set.
*
* @param ServerRequestInterface $request Original Request
* @param PaginatedResults $results Results to be applied on
* @param int|integer $currentPage Current Page, defaults to 1
*
* @return array previous_page_link, next_page_link
*/
protected function getURLsForResults(ServerRequestInterface $request, PaginatedResults $results, int $currentPage = 1) {
$thisURL = new Url($request->getUri());
if ($currentPage < $results->getPages()) {
$nextPage = clone $thisURL;
$nextPage->query->page = $currentPage + 1;
$nextPage = self::urlToPathWithQuery($nextPage);
} else {
$nextPage = null;
}
if ($currentPage > 1) {
$previousPage = clone $thisURL;
$previousPage->query->page = $currentPage - 1;
$previousPage = self::urlToPathWithQuery($previousPage);
} else {
$previousPage = null;
}
return [ $previousPage, $nextPage ];
}
protected function urlToPathWithQuery($url) {
if (!$url instanceof Url) {
$url = new Url($url);
}
$url->set('scheme', null)
->set('port', null)
->set('host', null);
$url = str_replace('://', '', $url);
return $url;
}
}
<?php
namespace Adepto\Asgard\Common;
/**
* PaginatedResults
* Represents a collection of results in a paginated fashion.
*
* @author bluefirex
* @version 1.0
*/
class PaginatedResults {
protected $results;
protected $perPage;
protected $pages;
protected $total;
/**
* Read carefully on how to use this:
* Always supply $results, which is an array of your actual results.
* Always supply $perPage, which tells the result how many results per page are shown or fetched.
* Only supply $pages if you do NOT have all results in $results but only fetched a subset and only
* want to pass that subset. If you do NOT supply $pages, it will automatically calculate the number based
* on the count of $results and items displayed per page.
*
* @param array $results Actual Results, either all or a subset
* @param int|integer $perPage Number of results per page
* @param int|null $pages How many pages there are. Read method description for this!
* @param int|null $total How many items there are in total if this wasn't paginated
*/
public function __construct(array $results = [], int $perPage = 1, int $pages = null, int $total = null) {
$this->results = $results;
$this->perPage = $perPage;
$this->pages = $pages ?? ceil(count($results) / $perPage);
$this->total = $total;
}
/**
* Get the results. This will be an array containing objects or arrays defined
* by the function using this.
*
* @return array
*/
public function getResults(): array {
return $this->results;
}
/**
* @chainable
* Transform the results in-place using $callback. This will MODIFY the originals.
*
* @param callable $callback Callback, receives one item at a time
*
* @return PaginatedResults
*/
public function transformResults(callable $callback): PaginatedResults {
$this->results = $this->getTransformedResults($callback);
return $this;
}
/**
* Get transformed results using $callback without modifying the originals
*
* @param callable $callback Callback, receives one item at a time
*
* @return array
*/
public function getTransformedResults(callable $callback): array {
return array_map($callback, $this->results);
}
/**
* Get the subset of results for page number $page.
* This will only return logical results if ALL results have been passed to the constructor.
*
* @param int $page Page Number to get the results for.
*
* @return array
*/
public function getPaginatedResults(int $page): array {
if ($page * $this->perPage > count($this->results)) {
return [];
}
return array_slice($this->results, $page * $this->perPage - $this->perPage, $this->perPage);
}
/**
* Get the number of results per page.
*
* @return int
*/
public function getResultsPerPage(): int {
return $this->perPage;
}
/**
* Get the number of pages.
*
* @return int
*/
public function getPages() {
return $this->pages;
}
/**
* Get the total number of results spanned across all pages
*
* @return int
*/
public function getTotal() {
return $this->total;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment