Creating a Controller
Every controller must extendAbstractApivalkController and implement two methods:
getRoute(): Defines the path and HTTP method.__invoke(): The main execution logic.
__invoke() carries the documentation as well. The request class is read from its parameter, the response classes from the new expressions its body returns, so neither is declared a second time.
Example
Typing $request
Name your own request class in __invoke():
AbstractApivalkController does not declare __invoke() as an abstract method. If it did, every controller would be pinned to that signature: PHP allows an override to widen a parameter type, never to narrow it, so __invoke(GetPetRequest $request) would be a fatal error in every PHP version. With no inherited signature there is nothing to be compatible with.
The contract is kept as a @method annotation on the base class, so static analysis still knows controllers are callable.
What is lost is the compile-time guarantee that __invoke() exists at all. Two runtime checks take its place: RouteCacheFactory rejects such a controller while indexing routes, and ApivalkControllerFactory rejects it when instantiating one, both with a LogicException naming the class. The first catches it when the route cache is built, the second on every request, so a controller that reaches dispatch without __invoke() fails with that message rather than “object is not callable” from inside the middleware stack.
Be aware that your IDE will not flag the missing method while you write it, the way it did when the base class declared it abstract. That is the price for naming your own request class.
ApivalkRequestInterface still works as the parameter type if you prefer it, for instance in a controller that handles several request shapes. An abstract request class, or no type hint at all, behaves the same way: none of them names something to instantiate, so the request class falls back to ResourceRequest, which carries what the route declares and nothing else.
Three signatures are rejected instead, each with a LogicException naming the controller:
__invoke()with no parameter.- A builtin parameter type such as
string. - A class that does not implement
ApivalkRequestInterface.
Documented Responses
ControllerResponseScanner reads the response classes out of the __invoke() body while the OpenAPI document is generated, never per request.
The scan is syntactic. It reads the source of __invoke() and looks for new expressions. It does not follow method calls, does not resolve return types, and does not run anything.
What the scanner finds
What it misses
Each of these is valid PHP, answers the client correctly, and documents nothing:2xx at all, carrying only the error responses derived from its route.
Move the construction into __invoke(). Where a collaborator assembles the data, let it return the data and build the response at the exit:
__invoke() the scan finds no response class in at all. That is a LogicException naming the controller, rather than publishing a spec that is quietly wrong. An anonymous class has no name to put in a document and is skipped, so returning one is caught by the same rule, but only when it was the sole response.
Note how little that catches. A controller that builds every response through a helper fails loudly. One that builds some of them that way, typically the success response with the error branches returned inline, scans clean and ships an incomplete document.
The error responses the framework produces around your controller are added on top of these, derived from the route. See OpenAPI Generator.
Dependency Injection
If you configured a PSR-11 container, you can use constructor injection:Route Discovery
Apivalk usesClassLocator to scan your controller directory. You don’t need to register routes manually; the framework discovers them automatically by calling getRoute() on any class extending AbstractApivalkController.
Pagination
For endpoints returning lists of data, see the Pagination guide.Resource Controllers (CRUD)
If your endpoint follows a standard RESTful CRUD pattern against a single entity, you don’t need to author a controller class like the one above by hand. Apivalk ships five abstract base controllers —AbstractCreateResourceController, AbstractViewResourceController, AbstractUpdateResourceController, AbstractDeleteResourceController, AbstractListResourceController — that derive their route and OpenAPI documentation from an AbstractResource declaration.
A minimal list controller looks like this: