The Five Controllers
| Base class | HTTP | Path | Response (default) |
|---|---|---|---|
AbstractCreateResourceController | POST | /{base}/{plural} | ResourceCreatedResponse (201) |
AbstractViewResourceController | GET | /{base}/{plural}/{id} | ResourceViewResponse (200) |
AbstractUpdateResourceController | PATCH | /{base}/{plural}/{id} | ResourceUpdatedResponse (200) |
AbstractDeleteResourceController | DELETE | /{base}/{plural}/{id} | ResourceDeletedResponse (200) |
AbstractListResourceController | GET | /{base}/{plural} | ResourceListResponse (200) |
{base} comes from $resource->getBaseUrl(), {plural} from $resource->getPluralName(), {id} from $resource->getIdentifierProperty()->getPropertyName().
All five are generic over the resource type: @template TResource of AbstractResource. When you subclass, declare your concrete resource so static analysis propagates the type through.
What You Write
A resource controller is one small class — agetResourceClass() method plus your business logic. The @extends Abstract...<YourResource> annotation is what gives you IDE autocompletion and static analysis on the typed helpers the base class exposes.
Example: Create
AbstractCreateResourceController provides $this->getResource($request) which builds a typed resource instance from the validated request body (and path, for Update). Because of the @template TResource, $customer below is known to be a CustomerResource — your IDE and PHPStan both know it.
AbstractUpdateResourceController has the same getResource($request) helper; AbstractView/Delete/ListResourceController don’t need it since they don’t hydrate from a body.
Example: Update
AbstractUpdateResourceController gives you both helpers: $this->getResource($request) hydrates a typed resource from the validated body and the identifier in the path, and $this->getResourceIdentifier($request) returns the raw identifier on its own (useful if you need to look up the existing row first, merge, and persist).
Example: View
AbstractView/Delete/UpdateResourceController expose $this->getResourceIdentifier($request) — a typed convenience for pulling the identifier path parameter (defined by $resource->getIdentifierProperty()) out of the request. It throws InvalidArgumentException if the parameter is missing, so the validation middleware has already ensured it’s present by the time you reach __invoke().
AbstractDeleteResourceController follows the same pattern — call $this->getResourceIdentifier($request), delete the row, return a ResourceDeletedResponse.
Example: List
getRoute()— built viaRoute::resource($this->getEmptyResource(), static::getMode()), wired with the resource’s filters / sortings / pagination hook.getRequestClass()— returnsResourceRequest::class(a shared, empty request class).getResponseClasses()— the mode-specific response + standard error responses.getMode()— the constant for Create / View / Update / Delete / List.getDescription()— e.g."Create animal","List animals".
What You Can Override
pagination(): ?Pagination— onAbstractListResourceController. Defaultnull(no pagination).routeAuthorization(): ?RouteAuthorization— declare required scopes/permissions.rateLimit(): ?RateLimitInterface— per-endpoint rate limit.getDescription(): string— override the auto-generated description if needed.configureRoute(Route $route): void— for exotic tweaks.AbstractListResourceControlleruses this internally to wire filters/sortings/pagination; if you override it onList, call back into the parent or re-wire manually.
The Shared ResourceRequest
All resource controllers return ResourceRequest::class from getRequestClass(). This is intentional: the runtime documentation (body fields for create/update, path id for view/update/delete, filters/sortings/pagination for list) is built from the resource declaration, not from per-mode request classes. You never need to author a resource request class yourself.
The docblock generator can emit an optional per-resource AnimalListRequest subclass purely for IDE autocompletion — that file only carries @method annotations, no runtime behavior.
Registering Controllers
Resource controllers are ordinary controllers — theClassLocator finds them automatically during route cache building. No manual registration.