Príklad: stránkovanie
GET /api/v1/products?page=2&per_page=5 vráti jednu stránku z 23 produktov s odkazmi na predchádzajúcu a ďalšiu stránku.
| Požiadavka | Odpoveď |
|---|---|
/api/v1/products | 200, stránka 1, 10 položiek, odkaz next |
?page=3&per_page=10 | 200, 3 položky, bez next |
?per_page=7 | 400 Field contains not available value(s) |
?page=0 alebo ?page=abc | 400 z handlera |
?page=9 | 404, posledná stránka je 3 |
Handler
<?php
declare(strict_types=1);
namespace App\Api;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Output\JsonOutput;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Validation\InputType;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
final class ProductListHandler extends BaseHandler
{
private const TOTAL = 23;
private const ERROR_SCHEMA = '{"type":"object","required":["status","message"],"properties":{"status":{"const":"error"},"message":{"type":"string"}}}';
public function summary(): string
{
return 'List products page by page';
}
public function params(): array
{
return [
// InputType::INTEGER turns "2" into 2 before the handler sees it
(new GetInputParam('page', InputType::INTEGER))->setDefault(1)->setDescription('From 1'),
// only these values pass, anything else answers 400
(new GetInputParam('per_page', InputType::INTEGER))->setDefault(10)->setAvailableValues([5, 10, 20]),
];
}
public function outputs(): array
{
// checked after handle() unless APP_ENV is production, see EnvConfigurator in the config
return [
new JsonOutput(200, (string) json_encode([
'type' => 'object',
'required' => ['items', 'page', 'per_page', 'total', 'links'],
'properties' => [
'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['id', 'name']]],
'page' => ['type' => 'integer', 'minimum' => 1],
'per_page' => ['type' => 'integer'],
'total' => ['type' => 'integer'],
'links' => ['type' => 'object'],
],
])),
// every code the handler returns needs an output, else the validation answers 500
new JsonOutput(400, self::ERROR_SCHEMA),
new JsonOutput(404, self::ERROR_SCHEMA),
];
}
public function handle(array $params): ResponseInterface
{
// a value that is not a number arrives as null, a range check is up to the handler
$page = $params['page'];
$perPage = $params['per_page'];
if (!is_int($page) || $page < 1) {
return new JsonApiResponse(400, ['status' => 'error', 'message' => 'page must be a whole number from 1']);
}
$pages = max(1, (int) ceil(self::TOTAL / $perPage));
if ($page > $pages) {
return new JsonApiResponse(404, ['status' => 'error', 'message' => "Page $page does not exist, last page is $pages"]);
}
$items = [];
for ($id = ($page - 1) * $perPage + 1; $id <= min(self::TOTAL, $page * $perPage); $id++) {
$items[] = ['id' => $id, 'name' => "Product $id"];
}
$links = ['self' => $this->link($page, $perPage)];
if ($page > 1) {
$links['prev'] = $this->link($page - 1, $perPage);
}
if ($page < $pages) {
$links['next'] = $this->link($page + 1, $perPage);
}
return new JsonApiResponse(200, [
'items' => $items,
'page' => $page,
'per_page' => $perPage,
'total' => self::TOTAL,
'links' => $links,
]);
}
private function link(int $page, int $perPage): string
{
return '/api/v1/products?' . http_build_query(['page' => $page, 'per_page' => $perPage]);
}
}Služby
application:
mapping:
Api: Tomaj\NetteApi\Presenters\*Presenter
services:
router: App\RouterFactory::create
apiPresenter:
factory: Tomaj\NetteApi\Presenters\ApiPresenter
# validates output schemas unless APP_ENV is production
- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
- Tomaj\NetteApi\Error\DefaultErrorHandler
apiDecider:
factory: Tomaj\NetteApi\ApiDecider
setup:
# GET /api/v1/products?page=2&per_page=5
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'products'), App\Api\ProductListHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())Test
#!/usr/bin/env bash
# End-to-end check: typed query params, allowed values, prev/next links and an output schema.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
serve 8093
request "$BASE/api/v1/products"
expect_status 200 "defaults"
expect_body '"page":1,"per_page":10,"total":23' "defaults"
expect_body '"links":{"self":"/api/v1/products?page=1&per_page=10","next":"/api/v1/products?page=2&per_page=10"}' "defaults"
ok "without params the defaults apply and only a next link is given"
request "$BASE/api/v1/products?page=3&per_page=10"
expect_body '"items":[{"id":21,"name":"Product 21"},{"id":22,"name":"Product 22"},{"id":23,"name":"Product 23"}]' "last page"
expect_body '"prev":"/api/v1/products?page=2&per_page=10"}' "last page"
ok "the last page has three items and only a prev link"
request "$BASE/api/v1/products?page=2&per_page=5"
expect_body '{"id":6,"name":"Product 6"}' "per_page 5"
ok "the string 5 from the URL arrives as the integer 5"
request "$BASE/api/v1/products?per_page=7"
expect_status 400 "per_page not allowed"
expect_body '"per_page":["Field contains not available value(s)"]' "per_page not allowed"
ok "a per_page outside 5, 10, 20 answers 400"
request "$BASE/api/v1/products?page=0"
expect_status 400 "page 0"
expect_body 'page must be a whole number from 1' "page 0"
request "$BASE/api/v1/products?page=abc"
expect_status 400 "page abc"
ok "page 0 or abc answers 400 from the handler"
request "$BASE/api/v1/products?page=9"
expect_status 404 "past the end"
expect_body 'last page is 3' "past the end"
ok "a page past the end answers 404"Časté otázky
Prečo sa page=abc dostane až do handlera?
Hodnota, ktorá nie je číslo, sa pre parameter INTEGER v 3.x zmení na null a predvolená hodnota sa vtedy nepoužije. Handler to overí. Pozrite si Vstupné parametre.
Prečo má každý stavový kód výstup?
Validácia výstupov prijme odpoveď iba vtedy, keď jej kódu a schéme zodpovedá niektorý výstup. Bez výstupov pre 400 a 404 by sa tieto odpovede vo vývoji zmenili na 500. Pozrite si Výstupy.
Porovnávajú sa povolené hodnoty ako čísla?
setAvailableValues([5, 10, 20]) overí hodnotu, ktorá prišla v query, takže per_page=7 odpovie 400 ešte pred handlerom.
Prečo odkazy v tele a nie hlavička Link?
Funguje oboje. Telo sa v konzole ľahšie číta; pre hlavičku si vložte Nette\Http\IResponse ako v príklade verzií.
Ako stránkovať databázu?
per_page použite ako LIMIT, (page - 1) * per_page ako OFFSET a jeden COUNT(*) pre celkový počet.