Preskočiť na obsah

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.

Otvoriť diagram v plnej veľkosti
PožiadavkaOdpoveď
/api/v1/products200, stránka 1, 10 položiek, odkaz next
?page=3&per_page=10200, 3 položky, bez next
?per_page=7400 Field contains not available value(s)
?page=0 alebo ?page=abc400 z handlera
?page=9404, posledná stránka je 3

Handler ​

php
<?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 ​

yaml
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 ​

bash
#!/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.

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.

Vydané pod licenciou MIT.