Routing
Glueful’s router supports explicit route files, route groups, middleware, named routes, and PHP attributes.
Most applications start with routes/api.php in glueful/api-skeleton.
Basic Routes
use Glueful\Http\Response;
$router->get('/welcome', function () {
return Response::success(['message' => 'Welcome']);
});
$router->get('/users', [UserController::class, 'index']);
$router->post('/users', [UserController::class, 'store']);
HTTP Methods
$router->get('/users', ...);
$router->post('/users', ...);
$router->put('/users/{id}', ...);
$router->patch('/users/{id}', ...);
$router->delete('/users/{id}', ...);
$router->head('/status', ...);
$router->options('/users', ...);
OPTIONS is answered automatically for CORS preflight (a 204 with an Allow
header listing the methods registered for that path), so you only need
$router->options(...) when you want to take over preflight with your own
handler — an explicit OPTIONS route takes precedence over the automatic
response.
Route Parameters
$router->get('/users/{id}', function (int $id) {
return Response::success(['id' => $id]);
});
Parameter Constraints
$router->get('/users/{id}', [UserController::class, 'show'])
->where('id', '\d+');
$router->get('/posts/{year}/{month}', [PostController::class, 'index'])
->where([
'year' => '\d{4}',
'month' => '\d{2}',
]);
Route Groups
Groups are the normal way to organize versioned APIs:
use Glueful\Routing\Router;
$router->group(['prefix' => 'v1'], function (Router $router) {
$router->get('/users', [UserController::class, 'index']);
$router->post('/users', [UserController::class, 'store']);
});
That produces:
GET /v1/usersPOST /v1/users
Group Middleware
$router->group([
'prefix' => 'v1/admin',
'middleware' => ['auth']
], function (Router $router) {
$router->get('/dashboard', [AdminController::class, 'dashboard']);
});
Route Middleware
$router->get('/profile', [ProfileController::class, 'show'])
->middleware('auth');
$router->post('/upload', [UploadController::class, 'store'])
->middleware(['auth', 'rate_limit:10,60']);
Common middleware you will see in the framework:
authcsrfrate_limit:attempts,window
Named Routes
Routes can be named:
$route = $router->get('/users/{id}', [UserController::class, 'show'])
->where('id', '\d+')
->name('users.show');
To generate a URL, use the route object:
$url = $route->generateUrl(['id' => 123]);
// /users/123
Controller Resolution
When you register [Controller::class, 'method'], the router resolves the controller through the container. That means constructor injection works as long as the controller is container-resolvable.
In the API skeleton, app providers are enabled through config/serviceproviders.php and implemented in app/Providers.
Attribute Routing
Glueful also supports attributes.
Class-level prefix:
use Glueful\Routing\Attributes\Controller;
use Glueful\Routing\Attributes\Get;
use Glueful\Http\Response;
#[Controller(prefix: '/v1/users')]
class UserController
{
#[Get('/{id}', where: ['id' => '\d+'])]
public function show(int $id): Response
{
return Response::success(['id' => $id]);
}
}
Shorthand method attributes are available for each verb —
#[Get], #[Post], #[Put], #[Patch], #[Delete], #[Options] — under
Glueful\Routing\Attributes.
Generic route attribute (one or more methods):
use Glueful\Routing\Attributes\Route;
class HealthController
{
#[Route('/healthz', methods: 'GET', name: 'healthz')]
public function index(): Response
{
return Response::success(['ok' => true]);
}
// The methods array accepts GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS.
#[Route('/resource/{id}', methods: ['PUT', 'PATCH'], where: ['id' => '\d+'])]
public function replaceOrUpdate(int $id): Response
{
return Response::success(['id' => $id]);
}
}
Documenting Endpoints (OpenAPI)
Glueful generates its OpenAPI spec from docblock annotations above each route (parsed by CommentsDocGenerator). Build the spec — and a browsable UI — with:
php glueful generate:openapi --ui
The generated docs are only as good as these blocks, so annotate routes as you add them:
/**
* @route GET /articles/{id}
* @summary Get an article
* @description Returns a single article by id.
* @tag Articles
* @requiresAuth true
* @queryParam fields:string="Comma-separated fields to return"
* @queryParam include:string="Related resources to expand" {required}
* @response 200 application/json "Article found" {
* success:boolean="true",
* message:string="Success message",
* data:{
* id:string="Article id",
* title:string="Article title"
* }
* }
* @response 404 "Article not found"
*/
$router->get('/articles/{id}', [ArticleController::class, 'show'])->where('id', '\d+');
The tags:
| Tag | Purpose |
|---|---|
@route METHOD /path | Required — the parser keys off this; no @route, no operation. |
@summary / @description / @tag | Free text; @tag groups operations in the spec. |
@requiresAuth true | Marks the operation as secured. |
@queryParam name:type="desc" | A query parameter. Append {required} to mark it required. type ∈ string|integer|number|boolean|array|object. |
@requestBody field:type="desc" … | Request body fields; {required=field1,field2} marks required ones. |
@response CODE [contentType] "desc" { schema } | A response; the optional { schema } uses the same field:type="desc" syntax and nests with field:{ … }. |
Path parameters auto-derive from {name} in the route URL (type string, required) — you don't write a tag for them. The operationId is generated for you; don't hand-write one.
@queryParamrequires framework 1.50.2+. On older versions, query params use the positional@param name query type true|false "desc"form — still parsed, but it overloads the reserved@paramtag and trips IDE/Intelephense warnings (P1133), which is exactly why@queryParamexists.
Route Cache Commands
The currently documented route-cache commands are:
php glueful route:cache:status
php glueful route:cache:clear
Do not rely on a route:cache command unless it has been added to your installed framework version.
Closure handlers are not cached
The route cache compiles handlers to a serializable form, and closures cannot be serialized. When the router detects any route that uses a closure handler, it skips writing the cache entirely (and logs which routes are responsible), and on load it discards a cache file that still contains closure placeholders. The routes keep working — they are just resolved live on every request, with no compiled-cache speedup.
This is fine for prototyping, but for production routing performance use a cacheable handler instead of a closure:
// Not cacheable — resolved live on every request:
$router->get('/welcome', fn() => Response::success(['ok' => true]));
// Cacheable — array or string handler:
$router->get('/welcome', [WelcomeController::class, 'index']);
Attribute-defined routes always use [Controller::class, 'method'] handlers, so
they are always cacheable.