> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apivalk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Resource Controllers

> Five abstract controllers — Create, View, Update, Delete, List — turn an `AbstractResource` into a full CRUD surface.

## The Five Controllers

| Base class                         | HTTP     | Default response                |
| ---------------------------------- | -------- | ------------------------------- |
| `AbstractCreateResourceController` | `POST`   | `ResourceCreatedResponse` (201) |
| `AbstractViewResourceController`   | `GET`    | `ResourceViewResponse` (200)    |
| `AbstractUpdateResourceController` | `PATCH`  | `ResourceUpdatedResponse` (200) |
| `AbstractDeleteResourceController` | `DELETE` | `DeletedApivalkResponse` (200)  |
| `AbstractListResourceController`   | `GET`    | `ResourceListResponse` (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:

| Controller                      | Auto-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

```php theme={null}
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

```php theme={null}
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

```php theme={null}
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

```php theme={null}
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.

```php theme={null}
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:

```php theme={null}
/**
 * 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:

```php theme={null}
/**
 * @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)`:

```php theme={null}
/**
 * @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()`.

## Related

* [Defining a Resource](/resources/resource)
* [Resource Responses](/resources/responses)
