OpenAPI-first PHP framework for type-safe REST APIs. Framework-agnostic Β· PSR-7/15/11/3 Β· PHP 7.2+
OpenAPI specs drift from code. $_POST['name'] has no type. Nobody knows which endpoints are secured. You maintain two
things β the code and the docs β and they never quite agree.
apivalk makes your PHP classes the single source of truth. Define a property once β automatic validation, type casting, OpenAPI 3.0 generation, and full IDE autocompletion.
- π Code is the spec β
getDocumentation()on your request/response classes drives validation, type casting, and OpenAPI generation from one definition. No annotation parsing, no separate YAML. - π Zero route registration β drop a controller into your directory.
ClassLocatorauto-discovers it and caches the route index. No config files to update. - β‘ Resource CRUD β one
AbstractResourcedeclaration generates five typed CRUD endpoints with full OpenAPI coverage. ~15 hand-authored classes collapse into one resource + five thin controllers. - π Security built in β JWT (JWK-based), scope enforcement, and three route security levels out of the box.
- π§ Typed everything β by the time
__invoke()runs, input is sanitized, validated, and cast. You get$request->body()->name, not$_POST['name']. - π‘ Full IDE autocompletion β
DocBlockGeneratorrewrites your request classes with typed@methodannotations and generatesShape/classes per bag.$request->body()->,$request->sorting()->,$request->filtering()->all autocomplete with correct types in PhpStorm and VS Code β zero hand-written boilerplate.
composer require apivalk/apivalkPHP 7.2+,
ext-json,ext-mbstringβ full installation guide β
<?php
declare(strict_types=1);
use apivalk\apivalk\Apivalk;
use apivalk\apivalk\ApivalkConfiguration;
use apivalk\apivalk\ApivalkExceptionHandler;
use apivalk\apivalk\Cache\FilesystemCache;
use apivalk\apivalk\Middleware\RequestValidationMiddleware;
use apivalk\apivalk\Middleware\SanitizeMiddleware;
use apivalk\apivalk\Router\Router;
use apivalk\apivalk\Util\ClassLocator;
require __DIR__ . '/vendor/autoload.php';
$classLocator = new ClassLocator(__DIR__ . '/src/Http/Controller', 'App\\Http\\Controller');
$router = new Router($classLocator, new FilesystemCache(__DIR__ . '/var/cache'));
$configuration = new ApivalkConfiguration(
$router,
null, // default: JsonRenderer
[ApivalkExceptionHandler::class, 'handle']
);
$configuration->getMiddlewareStack()->add(new SanitizeMiddleware());
$configuration->getMiddlewareStack()->add(new RequestValidationMiddleware());
$apivalk = new Apivalk($configuration);
$response = $apivalk->run();
$apivalk->getRenderer()->render($response);Every controller in
src/Http/Controlleris auto-discovered on first boot and cached. No routes to register. β Configure Apivalk
Every endpoint is a Controller + Request + Response triplet. The Request defines the shape β it drives validation and OpenAPI. The Response defines the output schema.
// Controller β owns the route and the business logic
final class ReadPetController extends AbstractApivalkController
{
public static function getRoute(): Route
{
return Route::get('/v1/pets/{id}')->description('Get a pet by ID');
}
public static function getRequestClass(): string { return ReadPetRequest::class; }
public static function getResponseClasses(): array { return [ReadPetResponse::class, NotFoundApivalkResponse::class]; }
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
$pet = $this->petRepo->find($request->path()->id); // id is cast to int automatically
return $pet ? new ReadPetResponse($pet) : new NotFoundApivalkResponse('Pet not found');
}
}
// Request β declares the input shape; drives validation + OpenAPI
class ReadPetRequest extends AbstractApivalkRequest
{
public static function getDocumentation(): ApivalkRequestDocumentation
{
$doc = new ApivalkRequestDocumentation();
$doc->addPathProperty(new IntegerProperty('id', 'Pet ID'));
return $doc;
}
}
// Response β declares the output shape; drives OpenAPI schema
class ReadPetResponse extends AbstractApivalkResponse
{
public static function getStatusCode(): int { return self::HTTP_200_OK; }
public static function getDocumentation(): ApivalkResponseDocumentation
{
$doc = new ApivalkResponseDocumentation();
$doc->addProperty(new IntegerProperty('id', 'Pet ID'));
$doc->addProperty(new StringProperty('name', 'Pet name'));
return $doc;
}
public function toArray(): array { return ['id' => $this->pet['id'], 'name' => $this->pet['name']]; }
}RequestValidationMiddleware returns 422 with field-level errors
automatically. β Controllers Β· Requests Β· Responses
Auto-discovery, filesystem route caching, fluent builder (Route::get/post/put/patch/delete), automatic 404/405
handling, path parameters via {name} syntax. β Routing docs
Run DocBlockGenerator once (or as a CI step) and your request classes get full IDE support β no hand-written
boilerplate.
Before:
class ReadPetRequest extends AbstractApivalkRequest { /* empty */ }After (auto-generated):
/**
* @method ParameterBag|Shape\ReadPetPathShape path()
* @method ParameterBag|Shape\ReadPetBodyShape body()
* @method SortBag|Shape\ReadPetSortingShape sorting()
* @method FilterBag|Shape\ReadPetFilteringShape filtering()
* @method Paginator|null paginator()
*/
class ReadPetRequest extends AbstractApivalkRequest { /* still empty */ }$request->path()->id, $request->body()->name, $request->sorting()->createdAt β all autocomplete with their correct
types. Works for resource controllers too: DocBlockGenerator emits @property annotations on AbstractResource
subclasses and generates typed list request classes (AnimalListRequest) with fully wired sort/filter/paginator shapes.
$generator = new DocBlockGenerator();
$generator->run('/src/Http/Controller', 'App\\Http\\Controller');β DocBlock generator docs Β· Generate how-to
Onion-style PSR-15 pipeline. Built in: SanitizeMiddleware, RequestValidationMiddleware, AuthenticationMiddleware,
SecurityMiddleware, RateLimitMiddleware. Trivial to extend with your
own. β Middleware docs Β· Custom middleware
Three route security levels β public, authenticated-only, scoped. JwtAuthenticator supports JWK endpoints out of the
box. Missing scope β 403 Forbidden. No token β 401 Unauthorized. Custom authenticators supported via
AuthenticatorInterface. β Security docs Β· JWT how-to Β· API key how-to
OpenAPIGenerator introspects every controller's request and response classes and emits a complete OpenAPI 3.0 spec β
including pagination envelopes, X-RateLimit-* headers, locale headers, and per-operation security requirements. No
annotations. Run it as a bin/ script and drop the JSON behind Swagger
UI. β OpenAPI generator Β· Generate how-to
Declare an AbstractResource once β identifier, properties, filters, sortings β and get five fully typed, validated,
OpenAPI-documented CRUD endpoints with matching response envelopes. Only __invoke() is yours to
write. β Resources Β· Resource CRUD how-to
Three strategies per route: Pagination::page(), Pagination::offset(), Pagination::cursor(). apivalk handles query
param validation, paginator hydration, and JSON envelope (data + pagination). All shapes documented in OpenAPI
automatically. β Pagination docs Β· Pagination how-to
Declare allowed sort fields and filter types on the route. Sorting defaults are applied when order_by is omitted β
$request->sorting() is always populated. Undeclared filter keys are silently
ignored. β Sorting Β· Filtering
Per-route IP-based rate limiting. X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset on every response;
Retry-After on 429. Documented in OpenAPI
automatically. β Rate limiting
Locale resolved from Accept-Language on every request, Content-Language set on every response. Both headers
documented in OpenAPI. Zero boilerplate in
controllers. β Localization docs
Pass any PSR-11 container β PHP-DI, Symfony DI, or your own. Apivalk uses it to resolve controllers, enabling full
constructor injection. Without a container it falls back to
new ControllerClass(). β Configuration
BadRequestApivalkResponse (400) Β· UnauthorizedApivalkResponse (401) Β· ForbiddenApivalkResponse (403) Β·
NotFoundApivalkResponse (404) Β· MethodNotAllowedApivalkResponse (405) Β· BadValidationApivalkResponse (422) Β·
TooManyRequestsApivalkResponse (429) Β· InternalServerErrorApivalkResponse (500)
make build || docker compose build
make composer || docker compose run --rm php72 composer install
make test || docker compose run --rm php72 composer test-unit && docker compose run --rm php72 composer test-integration # PHPUnit unit & integration test suite
make phpstan || docker compose run --rm php72 composer phpstan # PHPStan
make ci # will run composer, test and phpstanOwn PHP 7.2+ setup? Docker is optional β DDEV, Lando, or native all work. PHPStan runs at level 6; new code must not add violations (a baseline covers pre-existing issues). β Contributing guide
π docs.apivalk.com Β· π apivalk.com Β· π * Issues*
Β© 2025 apivalk. MIT License. Maintainer: Dominic Poppe.