Preskočiť na obsah

Príklad: JSON a surové telo ​

POST /api/v1/orders prijme JSON objednávku overenú schémou. PUT /api/v1/products/import prijme CSV ako surové telo.

Otvoriť diagram v plnej veľkosti
TeloOdpoveď
platná objednávka201 {"customer":"Ada","lines":2,"pieces":5}
žiadne400, missing data
{"customer":400 Syntax error
quantity: 0 alebo neznáme pole400 s chybami schémy
CSV s hlavičkou sku,price200 {"imported":2,"products":[...]}
prázdny PUT400 Field is required
CSV s inou hlavičkou422 z handlera

Schéma ​

json
{
    "type": "object",
    "properties": {
        "customer": {"type": "string", "minLength": 1},
        "items": {
            "type": "array",
            "minItems": 1,
            "items": {
                "type": "object",
                "properties": {
                    "sku": {"type": "string"},
                    "quantity": {"type": "integer", "minimum": 1}
                },
                "required": ["sku", "quantity"],
                "additionalProperties": false
            }
        },
        "note": {"type": "string"}
    },
    "required": ["customer", "items"],
    "additionalProperties": false
}

JSON handler ​

$params['order'] je dekódované pole; handler beží iba vtedy, keď schéma prešla.

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\JsonInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class OrderCreateHandler extends BaseHandler
{
    public function summary(): string
    {
        return 'Create an order from a JSON body';
    }

    public function params(): array
    {
        return [
            // the whole request body, decoded to an array and checked against the schema
            (new JsonInputParam('order', (string) file_get_contents(__DIR__ . '/schema/order.json')))
                ->setRequired()
                ->addExample('default', ['customer' => 'Ada', 'items' => [['sku' => 'book', 'quantity' => 2]]]),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        // already valid here: customer is a string, every item has sku and quantity >= 1
        $order = $params['order'];
        $pieces = array_sum(array_column($order['items'], 'quantity'));

        return new JsonApiResponse(201, [
            'customer' => $order['customer'],
            'lines' => count($order['items']),
            'pieces' => $pieces,
        ]);
    }
}

Handler pre surové telo ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\RawInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class CsvImportHandler extends BaseHandler
{
    public function summary(): string
    {
        return 'Import products from a CSV body';
    }

    public function params(): array
    {
        return [
            // the body as a string, nette-api only checks that it is not empty
            (new RawInputParam('csv'))->setRequired()->addExample('default', "sku,price\nbook,12.5\npen,1.2"),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $lines = array_values(array_filter(array_map('trim', explode("\n", $params['csv']))));
        $header = str_getcsv(array_shift($lines), escape: '');
        if ($header !== ['sku', 'price']) {
            // the format is checked here, in the handler, and answered with your own code
            return new JsonApiResponse(422, ['status' => 'error', 'message' => 'The first line must be sku,price']);
        }

        $products = [];
        foreach ($lines as $line) {
            [$sku, $price] = str_getcsv($line, escape: '') + [1 => '0'];
            $products[] = ['sku' => $sku, 'price' => (float) $price];
        }

        return new JsonApiResponse(200, ['imported' => count($products), 'products' => $products]);
    }
}

Služby ​

yaml
application:
	mapping:
		Api: Tomaj\NetteApi\Presenters\*Presenter

services:
	router: App\RouterFactory::create
	apiPresenter:
		factory: Tomaj\NetteApi\Presenters\ApiPresenter
	- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
	- Tomaj\NetteApi\Error\DefaultErrorHandler

	apiDecider:
		factory: Tomaj\NetteApi\ApiDecider
		setup:
			# POST /api/v1/orders with a JSON body
			- addApi(Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'orders'), App\Api\OrderCreateHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())
			# PUT /api/v1/products/import with a CSV body
			- addApi(Tomaj\NetteApi\EndpointIdentifier('PUT', 1, 'products', 'import'), App\Api\CsvImportHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: a JSON body validated by a schema, and a raw CSV body parsed by the handler.
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 8091
JSON=(-H 'Content-Type: application/json')

request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":2},{"sku":"pen","quantity":3}]}' "$BASE/api/v1/orders"
expect_status 201 "valid order"
expect_body '{"customer":"Ada","lines":2,"pieces":5}' "valid order"
ok "a valid JSON body reaches the handler as an array"

request -X POST "${JSON[@]}" "$BASE/api/v1/orders"
expect_status 400 "empty body"
expect_body '"detail":{"order":["missing data"]}' "empty body"
ok "an empty body answers 400 missing data"

request -X POST "${JSON[@]}" -d '{"customer":' "$BASE/api/v1/orders"
expect_status 400 "broken JSON"
expect_body '"order":["Syntax error"]' "broken JSON"
ok "broken JSON answers 400 with the json_decode error"

request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":0}]}' "$BASE/api/v1/orders"
expect_status 400 "schema"
expect_body 'quantity' "schema"
ok "a body that breaks the schema answers 400 with the failing field"

request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":1}],"coupon":"FREE"}' "$BASE/api/v1/orders"
expect_status 400 "additional property"
ok "an unknown property answers 400 because of additionalProperties: false"

request -X PUT -H 'Content-Type: text/csv' --data-binary $'sku,price\nbook,12.5\npen,1.2\n' "$BASE/api/v1/products/import"
expect_status 200 "csv"
expect_body '{"imported":2,"products":[{"sku":"book","price":12.5},{"sku":"pen","price":1.2}]}' "csv"
ok "a CSV body arrives as a string and the handler parses it"

request -X PUT -H 'Content-Type: text/csv' --data-binary $'name,cost\nbook,1\n' "$BASE/api/v1/products/import"
expect_status 422 "wrong csv header"
ok "the handler answers 422 for a CSV with the wrong header"

request -X PUT "$BASE/api/v1/products/import"
expect_status 400 "empty csv"
expect_body '"csv":["Field is required"]' "empty csv"
ok "an empty raw body answers 400"

Časté otázky ​

Musí klient poslať Content-Type: application/json? ​

Nie, JsonInputParam číta php://input bez ohľadu na hlavičku. Pošlite ju aj tak, konzola aj OpenAPI to robia.

Môže mať jeden endpoint JSON telo aj query parametre? ​

Áno, pridajte GetInputParam vedľa JsonInputParam. Zmysel má iba jeden parameter tela na endpoint, telo sa číta raz.

Ako odmietnem neznáme polia? ​

"additionalProperties": false v schéme, ako v order.json. Pozrite si Vstupné parametre.

Kde overím veci, ktoré schéma nevie? ​

V handleri, ako kontrolu hlavičky CSV, ktorá odpovie 422. Každý kód, ktorý vraciate, potrebuje výstup, pozrite si Výstupy.

Ukáže sa schéma v OpenAPI? ​

Áno, JSON schéma JsonInputParam sa stane telom požiadavky endpointu. Pozrite si OpenAPI.

Vydané pod licenciou MIT.