Routing

Concept Core uses League Route on top of PSR-7. Routes are defined in PHP files; a single shared instance of Concept\Core\Http\Routing\Router (implements RouterInterface) serves the entire application.

Context: request lifecycle, config/routes.php.

Loading route files

In config/routes.php you define the file list (routes.list) and optionally route interceptors (routes.interceptors). Loaded by HttpServiceProvider when RouterInterface is first created:

return [
    'routes' => [
        'interceptors' => [
            // AclRouteAuthorization::class, // RouteInterceptorInterface
        ],
        'list' => [
            dirname(__DIR__) . '/routes/web.php',
        ],
    ],
];

Additional files from components are loaded during ComponentsServiceProvider::boot() — a component returns the path to routes.php via ComponentInterface::routes() See structure with components.

Each route file has access to the $router variable (type League\Route\Router / RouterInterface):

<?php declare(strict_types=1);

use League\Route\Router;

/** @var Router $router */

$router->get('/path', $handler);

Global route interceptors (ACL, policies) are registered in routes.interceptors — see the dedicated section Route interceptors.

Handlers

League Route supports several callable forms:

Controller [Class, method]

The recommended approach — a controller method resolved from the DI container:

use Concept\App\Controllers\IndexController;

$router->get('/', [IndexController::class, 'index'])->setName('home');

The controller instance and method arguments are injected by RouteStrategy (FormRequest, ServerRequestInterface, URL parameters, Caster). More details — Controllers.

Closure

use Concept\Core\Http\Contracts\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;

$router->get('/health', function (ResponseFactoryInterface $response): ResponseInterface {
    return $response->json(['status' => 'ok']);
});

For simple endpoints; for complex logic prefer a controller.

HTTP methods

Router methodHTTP
get()GET
post()POST
put()PUT
patch()PATCH
delete()DELETE
map()multiple methods

URL parameters

Segments in curly braces end up in request attributes and handler arguments:

$router->get('/admin/users/edit/{id:number}', [UserController::class, 'edit']);

The :number suffix is a League Route constraint (digits only). Others: :alpha, :alphanum, :segment, etc. Without a suffix — any segment ({token}).

In the controller, a parameter with the same name and type (via CasterInterface):

public function edit(int $id): ResponseInterface

Route groups

Shared URI prefix and middleware for a group:

use League\Route\RouteGroup;
use Concept\Components\AuthAdmin\Middlewares\AuthMiddleware;

$router->group('/admin', function (RouteGroup $route) {
    $route->get('/', [AdminController::class, 'index'])->setName('admin.home');
    $route->get('/logout', [AdminController::class, 'logout'])->setName('admin.logout');
})->lazyMiddleware(AuthMiddleware::class);

URLs /admin and /admin/logout pass through AuthMiddleware. Nested groups are supported.

Middleware on routes

Global — for all routes in the file (usually at the top of routes/web.php). The minimal skeleton does not attach middleware

$router->lazyMiddleware(VerifyCsrfTokenMiddleware::class);
$router->lazyMiddleware(ShareViewDataMiddleware::class);

On a group or route — the chain is extended for matches in that scope:

$router->post('/projects/store', [ProjectController::class, 'store'])
    ->setName('projects.store')
    ->lazyMiddleware(ActiveUserMiddleware::class);

lazyMiddleware — the class is resolved from the container at request time. More details — Middleware.

Named routes

Each route can be given a name for URL generation:

$router->get('/login', [CabinetController::class, 'showLogin'])->setName('cabinet.login');

In PHP via UrlGeneratorInterface:

// path from root: /admin/users/edit/5
$path = $urlGenerator->uri('admin.user.edit', ['id' => 5]);

// full URL: https://example.com/admin/users/edit/5
$fullUrl = $urlGenerator->url('admin.user.edit', ['id' => 5]);

$origin = $urlGenerator->base();

Redirect by name — ResponseFactoryInterface::redirectByName():

return $this->response->redirectByName('admin.users');

In Twig — AppExtension in skeleton:

{{ uri('cabinet.login') }}
{{ url('admin.user.edit', { id: user.id }) }}
{{ base_url() }}

Component routes

A component returns the absolute path to routes.php via ComponentInterface::routes(). Example: src/Components/AuthAdmin/routes.php with prefixes /admin, /admin/users.

All routes end up in one Router — avoid duplicate names (setName) across files.

Listing routes (CLI)

php bin/console.php route:list
php bin/console.php route:list --full-middleware
php bin/console.php route:list -F

The route:list command prints method, URI, name, handler, and middleware. Requires ConsoleServiceProvider and registration of RouteListCommand in config/commands.php (already present in skeleton).

Tips

  • Keep public routes in routes/web.php, modular ones in Component/routes.php.
  • Always name routes that appear in templates or redirects.
  • POST/PUT/PATCH/DELETE with forms — together with VerifyCsrfTokenMiddleware and a CSRF field.
  • Protected areas — group() + auth middleware; ACL — route interceptors.