Skip to content

marko/routing

Routes live on the methods they handle. Conflicts are caught at boot time with clear error messages. Override vendor routes cleanly via Preferences, or disable them explicitly with #[DisableRoute].

Terminal window
composer require marko/routing

Add route attributes to controller methods:

ProductController.php
use Marko\Routing\Attributes\Get;
use Marko\Routing\Attributes\Post;
use Marko\Routing\Http\Response;
class ProductController
{
#[Get('/products')]
public function index(): Response
{
return new Response('Product list');
}
#[Get('/products/{id}')]
public function show(
int $id,
): Response {
return new Response("Product $id");
}
#[Post('/products')]
public function store(): Response
{
return new Response('Created', 201);
}
}

Route parameters are automatically passed to method arguments.

#[Get('/path')]
#[Post('/path')]
#[Put('/path')]
#[Patch('/path')]
#[Delete('/path')]
AdminController.php
use Marko\Routing\Attributes\Middleware;
class AdminController
{
#[Get('/admin/dashboard')]
#[Middleware(AuthMiddleware::class)]
public function dashboard(): Response
{
return new Response('Admin dashboard');
}
}

Middleware classes implement MiddlewareInterface:

AuthMiddleware.php
use Marko\Routing\Http\Request;
use Marko\Routing\Http\Response;
use Marko\Routing\MiddlewareInterface;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(
Request $request,
callable $next,
): Response {
if (!$this->isAuthenticated($request)) {
return new Response('Unauthorized', 401);
}
return $next($request);
}
}

Use Preferences to replace a vendor’s controller:

MyPostController.php
use Marko\Core\Attributes\Preference;
use Marko\Routing\Attributes\Get;
use Vendor\Blog\PostController;
#[Preference(replaces: PostController::class)]
class MyPostController extends PostController
{
#[Get('/blog')] // Your route takes over
public function index(): Response
{
return new Response('My custom blog');
}
}

Explicitly remove an inherited route:

MyPostController.php
use Marko\Routing\Attributes\DisableRoute;
#[Preference(replaces: PostController::class)]
class MyPostController extends PostController
{
#[DisableRoute] // Removes /blog/{slug} route
public function show(
string $slug,
): Response {
// Method still exists but has no route
}
}

If two modules define the same route, Marko throws RouteConflictException at boot:

Route conflict detected for GET /products
Defined in:
- Vendor\Catalog\ProductController::index()
- App\Store\ProductController::list()
Resolution: Use #[Preference] to extend one controller,
or use #[DisableRoute] to remove one route.
#[Get(path: '/path', name: 'route.name')]
#[Post(path: '/path')]
#[Put(path: '/path')]
#[Patch(path: '/path')]
#[Delete(path: '/path')]
#[DisableRoute]
#[Middleware(MiddlewareClass::class)]
class Request
{
public function getMethod(): string;
public function getPath(): string;
public function getQueryParams(): array;
public function getBody(): string;
}
class Response
{
public function __construct(
string $content = '',
int $status = 200,
array $headers = [],
);
public function getContent(): string;
public function getStatus(): int;
public function getHeaders(): array;
}
interface MiddlewareInterface
{
public function handle(Request $request, callable $next): Response;
}