Preskočiť na obsah

Príklad: endpointy z atribútov ​

API používateľov z príkladu DI rozšírenia, ale endpointy sú zapísané priamo na triedach handlerov atribútmi. Sekcia api: nemá zoznam endpoints:, iba discovery:.

Od verzie 4.0.0

Atribúty a api: discovery: sú nové vo verzii 4.0.0.

PožiadavkaStatusAtribút
GET /api/v1/hello200#[Get(1, 'hello')], #[Authorization(false)]
GET /api/v1/users bez tokenu401#[Get(1, 'users')], predvolená autorizácia
GET /api/v1/users s Authorization: Bearer secret-token200#[Get(1, 'users')]
GET /api/v1/users/detail?id=2200#[Get(1, 'users', 'detail')]
GET /api/v1/users/detail400chýba id, problem detail
POST /api/v1/users name=Carol201, 201, potom 429#[Post(1, 'users')], #[RateLimit('strict')]
OPTIONS /api/v1/users200CORS preflight, token netreba
GET /api/v1/nothing404žiadny atribút

Konfigurácia ​

yaml
extensions:
	api: Tomaj\NetteApi\DI\NetteApiExtension

parameters:
	apiTokens:
		secret-token: '*'

services:
	router: App\RouterFactory::create
	# a normal application service, handlers get it through autowiring
	- App\Model\UserRepository
	# a handler that needs a scalar argument is a normal service, its attributes are read as well
	- App\Api\HelloHandler('Hello from attributes')

api:
	# error details are on unless APP_ENV=production
	outputConfigurator: Tomaj\NetteApi\Output\Configurator\EnvConfigurator('APP_ENV', 'production')

	errors:
		format: problemDetails
		typeBaseUri: https://api.example.com/problems/

	# without cors the extension sends no CORS headers
	cors:
		allowedOrigins: [https://app.example.com]
		preflight: true

	logging:
		maskHeaders: true

	# named services, #[Authorization] and #[RateLimit] use them by name
	authorizations:
		token: Tomaj\NetteApi\Authorization\BearerTokenAuthorization(Tomaj\NetteApi\Misc\StaticTokenRepository(%apiTokens%))
	rateLimits:
		strict: App\FileRateLimit(storageDir: %tempDir%, limit: 2)

	# every endpoint needs the token unless it says otherwise
	defaults:
		authorization: token

	# every class in app/Api with #[Get], #[Post], ... becomes an endpoint
	discovery:
		paths: [%appDir%/Api]
		namespace: App\Api
  • discovery: paths a namespace povedia rozšíreniu, aby prečítalo triedy v app/Api.
  • HelloHandler('Hello from attributes') je registrovaný v services:, lebo potrebuje reťazec. Jeho atribúty sa prečítajú zo služby, druhý handler sa nevytvorí.
  • authorizations: token a rateLimits: strict sú mená, ktoré používajú atribúty.

Verejný endpoint ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Attribute\Authorization;
use Tomaj\NetteApi\Attribute\Get;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

// public: GET /api/v1/hello
#[Get(1, 'hello')]
#[Authorization(false)]
final class HelloHandler extends BaseHandler
{
    // the message comes from the service in config/common.neon
    public function __construct(private readonly string $message)
    {
        parent::__construct();
    }

    public function handle(array $params): ResponseInterface
    {
        return new JsonApiResponse(200, ['message' => $this->message]);
    }
}

Endpoint s obmedzením požiadaviek ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Attribute\Post;
use Tomaj\NetteApi\Attribute\RateLimit;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\PostInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

// POST /api/v1/users, at most 2 calls per minute
#[Post(1, 'users')]
#[RateLimit('strict')]
final class UserCreateHandler extends BaseHandler
{
    public function params(): array
    {
        return [
            (new PostInputParam('name'))->setRequired(),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        return new JsonApiResponse(201, ['id' => 3, 'name' => $params['name']]);
    }
}

Handler so závislosťou ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use App\Model\UserRepository;
use Tomaj\NetteApi\Attribute\Get;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

// GET /api/v1/users, protected by api.defaults.authorization
#[Get(1, 'users')]
final class UsersListHandler extends BaseHandler
{
    // UserRepository is autowired, the handler is a normal service
    public function __construct(private readonly UserRepository $users)
    {
        parent::__construct();
    }

    public function handle(array $params): ResponseInterface
    {
        return new JsonApiResponse(200, ['users' => $this->users->all()]);
    }
}

Endpoint s akciou ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use App\Model\UserRepository;
use Tomaj\NetteApi\Attribute\Get;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

// GET /api/v1/users/detail?id=1
#[Get(1, 'users', 'detail')]
final class UserDetailHandler extends BaseHandler
{
    public function __construct(private readonly UserRepository $users)
    {
        parent::__construct();
    }

    public function params(): array
    {
        return [
            (new GetInputParam('id'))->setRequired(),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $user = $this->users->find((int) $params['id']);
        if ($user === null) {
            return new JsonApiResponse(404, ['status' => 'error', 'message' => 'User not found']);
        }
        return new JsonApiResponse(200, $user);
    }
}

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: endpoints come from attributes on the handler classes.
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
rm -rf temp/cache temp/rate-* log
serve 8098
P='https://api.example.com/problems/'

request "$BASE/api/v1/hello"
expect_status 200 "public endpoint"
expect_body '{"message":"Hello from attributes"}' "service argument"
ok "GET /api/v1/hello is public and the handler service got its argument"

request "$BASE/api/v1/users"
expect_status 401 "default authorization"
expect_header 'Content-Type: application/problem+json' "errors.format"
expect_body "\"type\":\"${P}unauthorized\"" "errors.typeBaseUri"
ok "defaults.authorization protects GET /api/v1/users, the error is a problem detail"

request -H 'Authorization: Bearer secret-token' "$BASE/api/v1/users"
expect_status 200 "token"
expect_body '{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}' "autowired repository"
ok "with the token the handler answers with users from an autowired service"

request -H 'Authorization: Bearer secret-token' "$BASE/api/v1/users/detail?id=2"
expect_status 200 "action"
expect_body '{"id":2,"name":"Bob"}' "detail"
ok "GET /api/v1/users/detail is a separate endpoint (#[Get(1, 'users', 'detail')])"

request -H 'Authorization: Bearer secret-token' "$BASE/api/v1/users/detail"
expect_status 400 "params"
expect_body "\"type\":\"${P}validation-error\"" "validation problem"
ok "a missing param answers 400 problem"

request -H 'Origin: https://app.example.com' "$BASE/api/v1/hello"
expect_header 'Access-Control-Allow-Origin: https://app.example.com' "allowed origin"
request -H 'Origin: https://evil.example.com' "$BASE/api/v1/hello"
no_header 'Access-Control-Allow-Origin' "other origin"
ok "cors.allowedOrigins answers only the listed origin"

request -X OPTIONS -H 'Origin: https://app.example.com' "$BASE/api/v1/users"
expect_status 200 "preflight"
expect_header 'Access-Control-Allow-Methods' "preflight methods"
ok "cors.preflight answers OPTIONS for every endpoint without a token"

for i in 1 2; do
    request -X POST -H 'Authorization: Bearer secret-token' -d 'name=Carol' "$BASE/api/v1/users"
    expect_status 201 "create $i"
done
expect_body '{"id":3,"name":"Carol"}' "created"
request -X POST -H 'Authorization: Bearer secret-token' -d 'name=Carol' "$BASE/api/v1/users"
expect_status 429 "rate limit"
expect_body "\"type\":\"${P}rate-limited\"" "429 problem"
ok "#[RateLimit('strict')] lets 2 POST requests through, the third one gets 429"

request "$BASE/api/v1/nothing"
expect_status 404 "unknown"
expect_body "\"type\":\"${P}not-found\"" "404 problem"
ok "an unknown endpoint answers 404 problem"

# the compiled container only calls addApi(), the handlers are created when they are used
grep -q "addApi" temp/cache/nette.configurator/*.php || { echo "fail: no addApi() in the compiled container" >&2; exit 1; }
ok "the compiled container registers the endpoints with addApi()"

Časté otázky ​

Ako ho spustím? ​

composer install a ./e2e.sh v examples/attributes. Test spustí vstavaný PHP server na porte 8098.

Ako pridám endpoint? ​

Napíšte handler do app/Api/ s #[Get(1, 'something')]. Neon konfigurácia sa nemení; v debug režime sa kontajner prebuduje sám.

Čím sa líši od príkladu DI rozšírenia? ​

Iba tým, kde sú endpointy zapísané. Handlery, formát chýb, CORS a obmedzenie požiadaviek sú rovnaké, rovnako aj skompilovaný kontajner.

Čo sa stane pri preklepe v atribúte? ​

Kontajner sa neskompiluje. Skúste #[Authorization('tokne')]: správa uvedie endpoint aj triedu s atribútom.

Dá sa chrániť iba časť endpointov jedného handlera? ​

Nie, #[Authorization] platí pre celú triedu. Napíšte dva handlery, ako tu UsersListHandler a UserCreateHandler.

Vydané pod licenciou MIT.