Skip to content

Guards & Resolvers

A guard authenticates an incoming request and returns a typed value — usually the authenticated user or tenant — which is then injected directly into your controller method. If the guard throws, the request is rejected before anything else runs.

A resolver extracts and shapes request data into a typed value — query params, headers, or anything that isn’t about authentication.


A guard implements the Guard interface. It can have constructor dependencies — the container autowires them:

use Antares\Http\Guards\Guard;
use Antares\Exceptions\HttpException;
use Psr\Http\Message\ServerRequestInterface;
final class JwtGuard implements Guard
{
public function __construct(
private readonly JwtService $jwt,
) {}
public function resolve(ServerRequestInterface $request): AuthUser
{
$header = $request->getHeaderLine('Authorization');
$token = ltrim($header, 'Bearer ');
$user = $this->jwt->decode($token);
if ($user === null) {
throw new HttpException(401, 'Unauthorized');
}
return $user;
}
}

Apply #[Guards(GuardClass::class)] to the parameter that should receive the resolved value:

use Antares\Http\Attributes\Guards;
class PostController
{
#[Post('/posts', 201)]
public function store(
#[Guards(JwtGuard::class)] AuthUser $user,
CreatePostRequest $request,
): PostResponse {
return new PostResponse(
id: 1,
title: $request->title,
authorId: $user->id,
);
}
}

The guard runs first. If it throws, the DTO is never hydrated and the controller method is never called.


Different routes can use different guards. You can also stack multiple guards on a single method:

class ReportController
{
#[Get('/reports')]
public function index(
#[Guards(TenantGuard::class)] Tenant $tenant,
#[Guards(JwtGuard::class)] AuthUser $user,
): array {
return ['tenant' => $tenant->id, 'user' => $user->id];
}
}

Guards run in the order they appear in the parameter list.


Guards are a natural place to enforce roles. Throw an HttpException with 403 if the resolved user does not have the required access:

final class AdminGuard implements Guard
{
public function __construct(
private readonly JwtService $jwt,
) {}
public function resolve(ServerRequestInterface $request): AuthUser
{
$user = $this->jwt->decode(
ltrim($request->getHeaderLine('Authorization'), 'Bearer ')
);
if ($user === null) {
throw new HttpException(401, 'Unauthorized');
}
if (!$user->isAdmin()) {
throw new HttpException(403, 'Forbidden');
}
return $user;
}
}
#[Delete('/users/{id}', 204)]
public function destroy(
#[Guards(AdminGuard::class)] AuthUser $user,
int $id,
): void {
// only admins reach here
}

Guards always receive the ServerRequestInterface and are designed to authenticate based on request data — headers, cookies, tokens. If you need logic that has nothing to do with the current request, that belongs in a plain service resolved by the container, not a guard.


A resolver extracts and shapes request data into a typed value — query params, headers, or anything that isn’t about authentication. Unlike guards, resolvers never throw auth errors; they just parse and return.

use Antares\Http\Resolvers\Resolver;
use Psr\Http\Message\ServerRequestInterface;
final class PaginationResolver implements Resolver
{
public function resolve(ServerRequestInterface $request): Pagination
{
$params = $request->getQueryParams();
return new Pagination(
page: (int) ($params['page'] ?? 1),
perPage: (int) ($params['per_page'] ?? 15),
);
}
}

Apply #[Inject(ResolverClass::class)] to the parameter:

use Antares\Http\Attributes\Inject;
class PostController
{
#[Get('/posts')]
public function index(
#[Guards(JwtGuard::class)] AuthUser $user,
#[Inject(PaginationResolver::class)] Pagination $page,
): array {
...
}
}

Resolvers can have constructor dependencies — the container autowires them just like guards.


#[Guards]#[Inject]
InterfaceGuardResolver
PurposeAuth / access controlAnything
ThrowsHttpException to rejectUp to you
ReturnsAuthenticated principalWhatever you need