Routing

Mark a controller and give it a shared path prefix, then use concise HTTP method attributes on its handlers:

use GustavPHP\Gustav\Attribute\{Body, Controller, Delete, Get, Param, Patch, Post};

#[Controller('/dogs')]
final readonly class DogsController
{
    #[Get]
    public function list(): array
    {
        return [];
    }

    #[Post]
    public function create(#[Body] CreateDogInput $input): DogOutput
    {
        // ...
    }

    #[Patch('/{dog}')]
    public function update(#[Param('dog')] int $id): DogOutput
    {
        // ...
    }

    #[Delete('/{dog}')]
    public function delete(#[Param('dog')] int $id): bool
    {
        // ...
    }
}

#[Get], #[Post], #[Put], #[Patch], #[Delete], #[Head], and #[Options] are available. The generic #[Route] attribute accepts a Router\Method when a less common HTTP method is required.

The controller prefix and method path are joined. Both default to the root path, so #[Controller('/dogs')] with #[Get] registers GET /dogs.

Path parameters

Surround one complete path segment with braces and bind it with #[Param]:

#[Get('/{dog}')]
public function show(#[Param('dog')] int $id): DogOutput
{
    return $this->dogs->find($id);
}

The external placeholder and PHP argument can have different names. Multiple parameters work the same way:

#[Get('/{dog}/collars/{collar}')]
public function collar(
    #[Param('dog')] int $dogId,
    #[Param('collar')] int $collarId,
): CollarOutput {
    // ...
}

Placeholder names must start with a letter or underscore and contain only letters, numbers, and underscores. Unknown or repeated placeholders fail startup. Static routes take precedence over parameter routes, so /dogs/new is matched before /dogs/{dog}.

Named routes and URL generation

Give a route a stable name and inject UrlGeneratorInterface wherever links or redirects are built:

use GustavPHP\Gustav\Attribute\{Controller, Get, Param};
use GustavPHP\Gustav\Router\UrlGeneratorInterface;

#[Controller('/dogs')]
final readonly class DogsController
{
    public function __construct(private UrlGeneratorInterface $urls)
    {
    }

    #[Get('/{dog}', name: 'dogs.show')]
    public function show(#[Param('dog')] int $id): DogOutput
    {
        // ...
    }

    #[Get('/featured')]
    public function featured(): array
    {
        return [
            'url' => $this->urls->generate(
                'dogs.show',
                ['dog' => 42],
                ['ref' => 'featured'],
            ),
        ];
    }
}

The generated value is /dogs/42?ref=featured. Path values are URL-encoded; missing and unknown parameters throw immediately. Route names are unique across the application and are validated during startup.

HEAD, OPTIONS, and method errors

A HEAD request uses the matching GET handler when no explicit #[Head] handler exists, then removes the response body while preserving its status and headers. Gustav answers OPTIONS automatically with status 204 and an Allow header unless the route declares an explicit #[Options] handler.

When a path exists for another method, Gustav returns 405 with every allowed method, including inferred HEAD and OPTIONS. An unknown path returns 404.

Invalid routes

Gustav validates routes when the application starts. Startup fails when routes contain:

  • duplicate or ambiguous paths;
  • duplicate route names;
  • invalid or unknown placeholders;
  • non-public handlers; or
  • invalid request input or response types.