Skip to main content

Creating a Controller

Every controller must extend AbstractApivalkController and implement four key methods:
  1. getRoute(): Defines the path and HTTP method.
  2. getRequestClass(): Specifies the AbstractApivalkRequest class used for this endpoint.
  3. getResponseClasses(): Lists the possible AbstractApivalkResponse classes this controller can return.
  4. __invoke(): The main execution logic.

Example

namespace App\Http\Controller\Pet;

use apivalk\apivalk\Http\Controller\AbstractApivalkController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Http\Method\GetMethod;
use App\Http\Request\Pet\GetPetRequest;
use App\Http\Response\Pet\GetPetResponse;
use App\Http\Response\Common\NotFoundResponse;

class GetPetController extends AbstractApivalkController
{
    public static function getRoute(): Route
    {
        return new Route('/pet/{id}', new GetMethod());
    }

    public static function getRequestClass(): string
    {
        return GetPetRequest::class;
    }

    public static function getResponseClasses(): array
    {
        return [
            GetPetResponse::class,
            NotFoundResponse::class,
        ];
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        // $request is already populated and validated!
        $id = $request->path()->id;

        // Access populated sorting. SortBag is iterable — iterate directly.
        // The bag is always populated: if the user does not provide order_by,
        // it contains the default sorting from the route. Iteration order is
        // user-requested sorts first (in submission order), then route defaults
        // as tiebreakers — perfect for building an ORDER BY clause.
        foreach ($request->sorting() as $field => $sort) {
            $direction = $sort->isAsc() ? 'ASC' : 'DESC';
        }

        // Or directly access a specific field via the magic getter. If the user
        // did not pass order_by for status, this returns the route's default Sort.
        // Use $sort->isRequested() to discriminate user intent from defaults.
        $isAsc = $request->sorting()->status->isAsc();

        $pet = $this->petRepository->find($id);

        if (!$pet) {
            return new NotFoundResponse('Pet not found');
        }

        return new GetPetResponse($pet);
    }
}

Dependency Injection

If you configured a PSR-11 container, you can use constructor injection:
public function __construct(PetRepository $petRepository)
{
    $this->petRepository = $petRepository;
}

Route Discovery

Apivalk uses ClassLocator 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, request class, response classes, and OpenAPI documentation from an AbstractResource declaration. A minimal list controller looks like this:
use apivalk\apivalk\Http\Controller\Resource\AbstractListResourceController;
use App\Resource\AnimalResource;

/**
 * @extends AbstractListResourceController<AnimalResource>
 */
final class ListAnimalController extends AbstractListResourceController
{
    public static function getResourceClass(): string
    {
        return AnimalResource::class;
    }

    public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
    {
        // fetch, paginate, return ResourceListResponse
    }
}
See the full Resources section for how to declare a resource and wire up all five CRUD endpoints.