Skip to main content

The Five Controllers

Base classHTTPDefault response
AbstractCreateResourceControllerPOSTResourceCreatedResponse (201)
AbstractViewResourceControllerGETResourceViewResponse (200)
AbstractUpdateResourceControllerPATCHResourceUpdatedResponse (200)
AbstractDeleteResourceControllerDELETEDeletedApivalkResponse (200)
AbstractListResourceControllerGETResourceListResponse (200)
All five are generic over the resource type: @template TResource of AbstractResource. Declare the concrete type via the @extends annotation so static analysis and IDE autocompletion work.

What You Write

Every resource controller implements three things:
  1. getResourceClass(): string — return your resource class name.
  2. A route declaration method — see the table below.
  3. __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse — your business logic.
The base class provides getRequestClass(), getResponseClasses(), and getEmptyResource(). Create and Update also expose a getResource() helper — see below.

Route declaration: buildRoute()

All five resource controllers use buildRoute() — never implement getRoute() directly. The base class provides a final getRoute() that calls your buildRoute() and auto-injects extras:
ControllerAuto-injected into your buildRoute() result
Create / View / Update / Delete->tags() from the resource
List->tags(), ->filtering(), ->sorting() from the resource
You only declare the URL, path parameters, authorization, pagination, and rate limit in buildRoute().

Examples

Create

namespace App\Http\Controller\Animal;

use apivalk\apivalk\Http\Controller\Resource\AbstractCreateResourceController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Http\Response\Resource\ResourceCreatedResponse;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Security\RouteAuthorization;
use App\Domain\Animal\AnimalRepository;
use App\Resource\AnimalResource;

/**
 * @extends AbstractCreateResourceController<AnimalResource>
 */
final class CreateAnimalController extends AbstractCreateResourceController
{
    /** @var AnimalRepository */
    private $repository;

    public function __construct(AnimalRepository $repository)
    {
        $this->repository = $repository;
    }

    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    protected static function buildRoute(): Route
    {
        return Route::post('/api/v1/animals')
            ->description('Create animal')
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['animal'], ['animal:create']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        $animal = $this->getResource($request); // typed AnimalResource, body only
        $animal->animal_uuid = $this->repository->persist($animal);

        return new ResourceCreatedResponse($animal);
    }
}

View

namespace App\Http\Controller\Animal;

use apivalk\apivalk\Documentation\Property\StringProperty;
use apivalk\apivalk\Http\Controller\Resource\AbstractViewResourceController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Http\Response\NotFoundApivalkResponse;
use apivalk\apivalk\Http\Response\Resource\ResourceViewResponse;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Security\RouteAuthorization;
use App\Domain\Animal\AnimalRepository;
use App\Resource\AnimalResource;

/**
 * @extends AbstractViewResourceController<AnimalResource>
 */
final class ViewAnimalController extends AbstractViewResourceController
{
    /** @var AnimalRepository */
    private $repository;

    public function __construct(AnimalRepository $repository)
    {
        $this->repository = $repository;
    }

    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    protected static function buildRoute(): Route
    {
        return Route::get('/api/v1/animals/{animal_uuid}')
            ->pathProperty(new StringProperty('animal_uuid', 'Animal UUID'))
            ->description('Get animal')
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['animal'], ['animal:read']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        $uuid = $request->path()->animal_uuid;
        $row  = $this->repository->findByUuid($uuid);

        if ($row === null) {
            return new NotFoundApivalkResponse();
        }

        return new ResourceViewResponse(AnimalResource::byArray($row));
    }
}

Update

namespace App\Http\Controller\Animal;

use apivalk\apivalk\Documentation\Property\StringProperty;
use apivalk\apivalk\Http\Controller\Resource\AbstractUpdateResourceController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Http\Response\Resource\ResourceUpdatedResponse;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Security\RouteAuthorization;
use App\Domain\Animal\AnimalRepository;
use App\Resource\AnimalResource;

/**
 * @extends AbstractUpdateResourceController<AnimalResource>
 */
final class UpdateAnimalController extends AbstractUpdateResourceController
{
    /** @var AnimalRepository */
    private $repository;

    public function __construct(AnimalRepository $repository)
    {
        $this->repository = $repository;
    }

    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    protected static function buildRoute(): Route
    {
        return Route::patch('/api/v1/animals/{animal_uuid}')
            ->pathProperty(new StringProperty('animal_uuid', 'Animal UUID'))
            ->description('Update animal')
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['animal'], ['animal:update']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        // body fields + animal_uuid from path (matched by property name)
        $animal = $this->getResource($request);

        $this->repository->update($animal);

        return new ResourceUpdatedResponse($animal);
    }
}

Delete

namespace App\Http\Controller\Animal;

use apivalk\apivalk\Documentation\Property\StringProperty;
use apivalk\apivalk\Http\Controller\Resource\AbstractDeleteResourceController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Http\Response\DeletedApivalkResponse;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Security\RouteAuthorization;
use App\Domain\Animal\AnimalRepository;
use App\Resource\AnimalResource;

/**
 * @extends AbstractDeleteResourceController<AnimalResource>
 */
final class DeleteAnimalController extends AbstractDeleteResourceController
{
    /** @var AnimalRepository */
    private $repository;

    public function __construct(AnimalRepository $repository)
    {
        $this->repository = $repository;
    }

    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    protected static function buildRoute(): Route
    {
        return Route::delete('/api/v1/animals/{animal_uuid}')
            ->pathProperty(new StringProperty('animal_uuid', 'Animal UUID'))
            ->description('Delete animal')
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['animal'], ['animal:delete']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        $this->repository->delete($request->path()->animal_uuid);

        return new DeletedApivalkResponse();
    }
}

List

Implement buildRoute() instead of getRoute(). Tags, filters, and sortings from the resource are injected automatically — declare only the URL, authorization, pagination, and rate limit.
namespace App\Http\Controller\Animal;

use apivalk\apivalk\Http\Controller\Resource\AbstractListResourceController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Http\Response\Pagination\PagePaginationResponse;
use apivalk\apivalk\Http\Response\Resource\ResourceListResponse;
use apivalk\apivalk\Router\Route\Pagination\Pagination;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Security\RouteAuthorization;
use App\Domain\Animal\AnimalRepository;
use App\Resource\AnimalResource;

/**
 * @extends AbstractListResourceController<AnimalResource>
 */
final class ListAnimalController extends AbstractListResourceController
{
    /** @var AnimalRepository */
    private $repository;

    public function __construct(AnimalRepository $repository)
    {
        $this->repository = $repository;
    }

    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    // tags(), availableFilters(), availableSortings() are injected from AnimalResource automatically
    protected static function buildRoute(): Route
    {
        return Route::get('/api/v1/animals')
            ->description('List animals')
            ->pagination(Pagination::page()->setMaxLimit(50))
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['animal'], ['animal:read']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        $paginator = $request->paginator();
        $filters   = $request->filtering();
        $sorting   = $request->sorting();

        $rows  = $this->repository->findPage($filters, $sorting, $paginator);
        $total = $this->repository->countForFilters($filters);

        $resources = [];
        foreach ($rows as $row) {
            $resources[] = AnimalResource::byArray($row);
        }

        $totalPages = (int)\ceil($total / $paginator->getLimit());

        return new ResourceListResponse(
            $resources,
            new PagePaginationResponse(
                $paginator->getPage(),
                $paginator->getLimit(),
                $paginator->getPage() < $totalPages,
                $totalPages
            )
        );
    }
}

Nested Resources

Because buildRoute() is fully explicit, nested URLs are straightforward — add more path segments and declare each path parameter:
/**
 * GET /api/v1/users/{user_uuid}/authenticators/{authenticator_uuid}
 *
 * @extends AbstractViewResourceController<AuthenticatorResource>
 */
final class ViewUserAuthenticatorController extends AbstractViewResourceController
{
    public static function getResourceClass(): string
    {
        return AuthenticatorResource::class;
    }

    protected static function buildRoute(): Route
    {
        return Route::get('/api/v1/users/{user_uuid}/authenticators/{authenticator_uuid}')
            ->pathProperty(new StringProperty('user_uuid', 'User UUID'))
            ->pathProperty(new StringProperty('authenticator_uuid', 'Authenticator UUID'))
            ->description('Get user authenticator')
            ->routeAuthorization(new RouteAuthorization('BearerAuth', ['user'], ['user:read']));
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        $userUuid          = $request->path()->user_uuid;
        $authenticatorUuid = $request->path()->authenticator_uuid;

        $row = $this->repository->findForUser($userUuid, $authenticatorUuid);

        if ($row === null) {
            return new NotFoundApivalkResponse();
        }

        return new ResourceViewResponse(AuthenticatorResource::byArray($row));
    }
}
The same pattern extends to any depth. Each ->pathProperty() call documents the parameter in OpenAPI and registers it for validation.

getResource() Helper

AbstractCreateResourceController and AbstractUpdateResourceController expose a typed $this->getResource($request) method that returns a fully populated TResource. Create — body only. Path parameters are intentionally excluded because the identifier doesn’t exist yet (the server generates it after persisting). Update — body fields first, then any path parameter whose name matches a resource property is automatically set. If your route has {animal_uuid} and AnimalResource declares an animal_uuid property, $resource->animal_uuid is already populated after calling getResource(). For nested resources with multiple path params (e.g. {user_uuid}/{authenticator_uuid}), only the ones whose names appear as resource properties are set — parent-scope params are ignored. If you need the raw path value directly without the helper, $request->path()->key always works.

What the Base Class Provides

  • getRequestClass() — returns ResourceRequest::class by default (a shared, empty request class).
  • getResponseClasses() — the mode-appropriate success response + BadRequestApivalkResponse + ForbiddenApivalkResponse.
  • getEmptyResource() — instantiates the resource class returned by getResourceClass(), useful inside buildRoute().

IDE Autocomplete via the DocBlock Generator

After running the docblock generator, a typed request class is generated for each controller that has path parameters, sorting, filtering, or pagination (e.g. AnimalViewRequest, AnimalUpdateRequest, AnimalListRequest). To get full IDE autocomplete in __invoke, add a @param annotation with the generated class:
/**
 * @param AnimalViewRequest $request
 */
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
    $uuid = $request->path()->animal_uuid; // autocompletes
    // ...
}
For Update, the same annotation gives you both typed $request->path() and typed $this->getResource($request):
/**
 * @param AnimalUpdateRequest $request
 */
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
    $animal = $this->getResource($request);
    // $animal->animal_uuid  ← from path (auto-merged)
    // $animal->name         ← from body
}
For List, annotate with AnimalListRequest to get typed sorting(), filtering(), and paginator().