apivalk / apivalk
A Lightweight, Framework-Agnostic REST API Ecosystem for PHP. Built for speed, precision, and type-safe development.
Requires
- php: >=7.2
- ext-json: *
- ext-mbstring: *
- firebase/php-jwt: ^6.4
- psr/container: ^1.1.1 || ^2.0.1
- psr/log: ^1.1
Requires (Dev)
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^8.5
README
Apivalk ๐ฆ
OpenAPI-first PHP framework for type-safe REST APIs. Framework-agnostic ยท PSR-7/15/11/3 ยท PHP 7.2+
The Problem
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.
Why Apivalk? ๐ค
- ๐ 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.
Installation
composer require apivalk/apivalk
PHP 7.2+,
ext-json,ext-mbstringโ full installation guide โ
Quick Start
Bootstrap
<?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
Define an Endpoint
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
Features
๐ฃ๏ธ Routing
Auto-discovery, filesystem route caching, fluent builder (Route::get/post/put/patch/delete), automatic 404/405 handling, path parameters via {name} syntax. โ Routing docs
๐ก IDE Autocompletion via DocBlock Generator
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
๐ง Middleware Pipeline
Onion-style PSR-15 pipeline. Built in: SanitizeMiddleware, RequestValidationMiddleware, AuthenticationMiddleware, SecurityMiddleware, RateLimitMiddleware. Trivial to extend with your own. โ Middleware docs ยท Custom middleware
๐ Security & Authorization
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
๐ OpenAPI 3.0 Generation
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
๐ฆ Resource CRUD
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
๐ Pagination
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
๐ข Sorting & Filtering
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
โฑ๏ธ Rate Limiting
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
๐ Localization
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
โ๏ธ Dependency Injection
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
Built-in Error Responses
BadRequestApivalkResponse (400) ยท UnauthorizedApivalkResponse (401) ยท ForbiddenApivalkResponse (403) ยท NotFoundApivalkResponse (404) ยท MethodNotAllowedApivalkResponse (405) ยท BadValidationApivalkResponse (422) ยท TooManyRequestsApivalkResponse (429) ยท InternalServerErrorApivalkResponse (500)
Contributing & Local Development
docker compose build docker compose run --rm php72 composer install docker compose run --rm php72 composer test # PHPUnit docker compose run --rm php72 composer phpstan # PHPStan level 6
Own 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.
