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.
| Telo | Odpoveď |
|---|---|
| platná objednávka | 201 {"customer":"Ada","lines":2,"pieces":5} |
| žiadne | 400, missing data |
{"customer": | 400 Syntax error |
quantity: 0 alebo neznáme pole | 400 s chybami schémy |
CSV s hlavičkou sku,price | 200 {"imported":2,"products":[...]} |
| prázdny PUT | 400 Field is required |
| CSV s inou hlavičkou | 422 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.