Skip to content

Controllers

Controllers are plain PHP classes. You define routes by placing attributes on methods — no base class, no interface required.

use Antares\Router\Attributes\Get;
class UserController
{
#[Get('/users')]
public function index(): array
{
return ['users' => []];
}
}

Register the controller in your RouteServiceProvider:

$router->register(UserController::class);

That’s all. The router scans the class via reflection and picks up every annotated method.


Segments wrapped in {} are extracted and injected by name. The value is cast to whatever type you declare:

#[Get('/users/{id}')]
public function show(int $id): array
{
return ['id' => $id];
}

Supported types: int, float, bool, string.


Any scalar parameter that isn’t a route segment is resolved from the query string:

#[Get('/users')]
public function index(int $page = 1, int $perPage = 15): array
{
return ['page' => $page, 'perPage' => $perPage];
}

GET /users?page=2&perPage=50 injects $page = 2 and $perPage = 50.


Type-hint a #[Dto] class to have the request body decoded, hydrated, and validated automatically. If validation fails, a 422 response is returned before your method is ever called:

#[Post('/users', 201)]
public function store(CreateUserRequest $request): UserResponse
{
return new UserResponse(id: 1, name: $request->name);
}

See DTOs for how to define request DTOs.


Type-hint ServerRequestInterface to get the raw PSR-7 request:

use Psr\Http\Message\ServerRequestInterface;
#[Get('/debug')]
public function debug(ServerRequestInterface $request): array
{
return ['method' => $request->getMethod()];
}

Type-hint UploadedFileInterface and name the parameter to match the form field:

use Psr\Http\Message\UploadedFileInterface;
#[Post('/avatars', 201)]
public function upload(UploadedFileInterface $avatar): array
{
$avatar->moveTo('/storage/avatars/' . uniqid() . '.jpg');
return ['uploaded' => true];
}

The return value of your method determines the response:

Return typeResult
arrayJSON response with the route’s status code
#[ResponseDto] objectSerialized to JSON via the Serializer
ResponseInterfaceReturned as-is
null / voidEmpty body with the route’s status code
#[Get('/ping')]
public function ping(): array
{
return ['pong' => true];
}
#[Get('/users/{id}')]
public function show(int $id): UserResponse
{
return new UserResponse(id: $id, name: 'John');
}

See Response DTOs for how to define and shape serialized responses.

#[Delete('/users/{id}', 204)]
public function destroy(int $id): void
{
// 204 No Content
}
use Nyholm\Psr7\Response;
#[Get('/health')]
public function health(): Response
{
return new Response(200, [], 'OK');
}

Any non-builtin type that isn’t a DTO, guard, or PSR type is resolved from the container. This is how you inject services:

class PostController
{
public function __construct(
private readonly PostRepository $posts,
) {}
#[Get('/posts')]
public function index(): array
{
return $this->posts->all();
}
}

The container autowires PostRepository and any of its own dependencies automatically.