Routing in BowPHP
Introductionβ
Routing is an essential part of developing a web application. It links a URL to a specific action in the application's controller.
In BowPHP, the routing system is built into the application instance and exposed in your route files through the $router variable.
The application's routes are defined in the app.php file, located in the routes folder of your project.
URL Mappingβ
BowPHP's routing system lets you match a URL to a controller or an action. This is done by associating a URL with an HTTP method (GET, POST, PUT, DELETE, etc.) and defining the action to execute when a request matches that route.
Routing Structureβ
Routing in BowPHP is written as follows:
$router->[method](url, action);
method: The HTTP verb (GET,POST,PUT,DELETE,OPTIONS,PATCH), always written in lowercase.url: The URL to match.action: The action to execute. This can be a closure (anonymous function), an array of closures, or the name of a controller.
Routing Exampleβ
With a Closure:β
$router->get('/', function () {
return 'Hello, World!';
});
A closure is an anonymous function that can be defined on the fly and used in BowPHP routing. For more information, see the closures documentation.
With an array of functions (callables):β
$callables = [];
$callables[] = function () {
echo 'Hello World';
return true; // Return true to execute the next function
};
$callables[] = function () {
echo 'Bien merci';
};
$router->get('/', $callables);
The first function must return true to allow the next one to run. If it returns false, the execution of the following functions will stop.
With a controller:β
$router->get('/', 'UserController::action');
With a controller and a middleware:β
$router->get('/', ['middleware' => 'ip', 'controller' => 'UserController::action']);
Using a controller with the __invoke method:β
If the controller implements the magic method __invoke, you can use it directly as a function:
$router->get('/', UserController::class);
List of Mapping Methodsβ
Here are the main routing methods in BowPHP:
$router->get($url, $action);
$router->post($url, $action);
$router->put($url, $action);
$router->delete($url, $action);
$router->options($url, $action);
$router->patch($url, $action);
$router->match(['GET', 'POST'], $url, $action);
$router->any($url, $action);
Routing Examples with Different HTTP Methodsβ
GET: To retrieve data
$router->get('/', function () {
return 'Hello, World!';
});
POST: To send data to the server
$router->post('/', function () {
return 'Data posted';
});
PUT: To update existing resources
$router->put('/', function () {
return 'Resource updated';
});
DELETE: To delete a resource
$router->delete('/', function () {
return 'Resource deleted';
});
PATCH: To apply partial changes to a resource
$router->patch('/', function () {
return 'Partial update applied';
});
OPTIONS: To retrieve the available options for a resource
$router->options('/', function () {
return 'Options retrieved';
});
Browsers do not natively support the DELETE, PUT, and PATCH methods. To work around this limitation, you can add a hidden field to your HTML forms:
<input type="hidden" name="_method" value="DELETE">
This allows BowPHP to understand that you want to reach the route associated with the DELETE method.
Multiple Mappingβ
match - Mapping with Multiple HTTP Methodsβ
If you want to match multiple HTTP methods (such as GET and POST) to the same URL, you can use the match method.
$router->match(['GET', 'POST'], '/users', function () {
// Code here
});
You can also handle the different request types within a single action:
use Bow\Http\Request;
$router->match(['GET', 'POST'], '/users', function (Request $request) {
if ($request->isPost()) {
// Handling for the POST request
} else {
// Handling for the GET request
}
});
any - Mapping with Any HTTP Methodβ
If you want any HTTP method to match a URL, you can use any:
$router->any('/', function () {
// Code here
});
Route Groups with prefixβ
When you have several routes that share a similar prefix (for example, /users, /users/:id, /users/:id/delete), you can use the prefix method to organize your code more cleanly.
$router->prefix('/users', function () use ($router) {
$router->get('/', function () {
// Code to retrieve all users
});
$router->get('/:id', function ($id) {
// Code to retrieve a user by ID
});
$router->get('/:id/delete', function ($id) {
// Code to delete a user
});
});
Avoid nesting prefix groups: the current prefix is reset after the callback runs.
Subdomain Constraintsβ
BowPHP lets you restrict routes to a specific domain or subdomain.
Constraint on an individual route with withDomainβ
Use the withDomain method chained on the Route object:
$router->get('/dashboard', function () {
return 'Admin Dashboard';
})->withDomain('admin.example.com');
Constraint on a group of routes with $router->domainβ
The $router->domain() method applies a domain constraint to all the
routes defined in its callback. Handy for isolating an API, an admin
area, or a tenant subdomain without repeating withDomain() everywhere.
$router->domain('api.example.com', function () use ($router) {
$router->get('/users', 'ApiController::users');
$router->get('/posts', 'ApiController::posts');
});
The pattern accepts dynamic parameters (:tenant, <tenant>) and
wildcards (*), just like withDomain():
$router->domain(':tenant.example.com', function () use ($router) {
$router->get('/', fn($tenant) => "Bienvenue, {$tenant}");
});
The domain pattern is only active while the callback runs. Any
route defined after the call to domain() reverts to an empty domain.
Static subdomainβ
$router->get('/dashboard', function () {
return 'Admin Dashboard';
})->withDomain('admin.example.com');
Dynamic subdomain with a parameterβ
You can capture part of the subdomain as a parameter using the :param or <param> notation:
$router->get('/dashboard', function ($tenant) {
return "Dashboard pour le tenant : $tenant";
})->withDomain(':tenant.example.com');
The $tenant parameter will be automatically injected into your action with the value extracted from the subdomain.
Using a wildcardβ
To accept any subdomain, use *:
$router->get('/', function () {
return 'Bienvenue';
})->withDomain('*.example.com');
Subdomain constraints are especially useful for:
- Multi-tenant applications (
client1.app.com,client2.app.com) - Separating the admin and public interfaces (
admin.site.comvswww.site.com) - Versioned APIs (
v1.api.example.com,v2.api.example.com)
Customizing Routesβ
BowPHP lets you customize your routes in several ways, including:
- Capturing variables in the URL
- Adding validation criteria on the captured variables
- Naming routes for easier management
- Attaching middlewares to specific routes
- Combining different actions (middleware, controller)
Capturing Variables in the URLβ
To capture variables in the URL, use the :variable_name notation. These variables will then be passed to the corresponding action.
$router->get('/:name', function ($name) {
return 'Bonjour ' . $name;
});
You can also use optional parameters:
$router->get('/users/:id?', function ($id = null) {
return $id;
});
Adding Validation Criteria on Variablesβ
When you capture variables, you can validate them using the where method to ensure the data complies with a given rule.
Example with a single variable:β
$router->get('/:name', function ($name) {
return 'Bonjour ' . $name;
})->where('name', '[a-z]+');
Example with multiple variables:β
$router->get('/:name/:lastname/:number', function ($name, $lastname, $number) {
return sprintf('Bonjour %s %s, votre numΓ©ro est %s', $name, $lastname, $number);
})->where([
'name' => '[a-z]+',
'lastname' => '[a-z]+',
'number' => '\d+'
]);
The where method lets you define validation rules for each variable captured in the URL.
Naming Routesβ
To make managing routes easier in large applications, you can assign them names. This makes it easy to reference a route in your code.
$router->get('/:name', function ($name) {
return 'Bonjour ' . $name;
})->name('greeting');
Using Middlewaresβ
A middleware is a function or a set of functions that run between the request and the action to be executed. You can attach one or more middlewares to a route.
Example of attaching a middleware:β
$router->get('/:name', [
'middleware' => 'auth',
'controller' => 'UserController::show'
]);
// Or with the `middleware()` method:
$router->get('/:name', function ($name) {
return 'Bonjour ' . $name;
})->middleware('auth');
Composing Actionsβ
BowPHP also lets you combine a controller and a middleware for a specific route. Here is an example:
$router->get('/:name', [
'middleware' => 'auth',
'controller' => 'UserController::show'
]);
Using the route Methodβ
The route method in BowPHP lets you define a route in a simpler and more flexible way. It accepts an array of parameters, which allows you to customize the route in more detail.
Prototype of the route methodβ
$router->route([
'path' => '/',
'method' => "GET",
'handler' => 'handler'
]);
List of options available in routeβ
| Parameter | Type | Description |
|---|---|---|
path | String | The URL path to match |
method | String, Array | HTTP method (GET, POST, PUT, DELETE, OPTIONS, PATCH) |
handler | String, Closure, Array | Action to execute |
middleware | String, Array | Middleware(s) to apply before executing the action |
where | String, Array | Constraint(s) applied to the variables in the URL |
The route() method returns void and does not allow chaining ->name() directly. To name a route, use the HTTP methods (get, post, etc.) followed by ->name(...).
Example of using route with a closureβ
$router->route([
'path' => '/',
'method' => "GET",
'handler' => function () {
return "Hello, Bow";
}
]);
Example of using route with a controllerβ
$router->route([
'path' => '/',
'method' => "GET",
'handler' => 'HomeController::action'
]);
For more information about controllers, see the Controllers section.
Example of using route with a controller and a middlewareβ
$router->route([
'path' => '/',
'method' => "GET",
'handler' => 'HomeController::show',
'middleware' => ['auth']
]);
To learn more about middlewares, see the Middlewares section.
Example of using route with a controller, a middleware, and a constraintβ
$router->route([
'path' => '/:name',
'method' => "GET",
'handler' => 'HomeController::show',
'middleware' => ['auth'],
'where' => ['name' => '[a-z]+']
]);
This method is especially useful when you need a more concise syntax and want more control over your route's parameters.
Router-level Middlewareβ
In addition to the ->middleware() method chained on a route, the router exposes
its own middleware() method, which returns a new router instance
with the middlewares already applied. Handy for sharing middlewares
across several routes without a prefix group.
$router->middleware('auth')->get('/profile', 'UserController::show');
$router->middleware(['auth', 'admin'])->prefix('/admin', function () use ($router) {
$router->get('/users', 'AdminController::users');
$router->get('/posts', 'AdminController::posts');
});
The middlewares passed to Router::middleware() can be:
- A string (alias registered in the Kernel, e.g.
'auth') - An FQCN class (which will be instantiated and its
process()called) - An array combining both
Handling Error Codesβ
The code() method lets you register a global handler for a specific HTTP
code (404, 500, etc.). The handler is called when the framework emits
a response with that code.
$router->code(404, function () {
return view('errors.404');
});
$router->code(500, ['ErrorController::serverError']);
$router->code(403, [
'middleware' => 'log-forbidden',
'controller' => 'ErrorController::forbidden',
]);
code() accepts the same handler types as the verb methods: closure,
Controller::method string, or array with controller / middleware.
Attribute-based Routingβ
For projects on PHP 8+, BowPHP lets you declare routes directly on controller classes using attributes. No need to maintain a central route file.
Declarationβ
use Bow\Router\Attributes\{Controller, Get, Post, Put, Delete};
#[Controller(prefix: '/api/users', middleware: ['auth'], name: 'users.')]
final class UserController
{
#[Get('/', name: 'index')]
public function index() { /* ... */ }
#[Get('/:id', name: 'show', where: ['id' => '\d+'])]
public function show(int $id) { /* ... */ }
#[Post('/', name: 'store', middleware: ['validate'])]
public function store(Request $request) { /* ... */ }
#[Put('/:id', name: 'update')]
public function update(int $id, Request $request) { /* ... */ }
#[Delete('/:id', name: 'destroy')]
public function destroy(int $id) { /* ... */ }
}
Registrationβ
In your route file, call register() with the controller (or an
array of controllers):
$router->register(UserController::class);
// Or several at once
$router->register([
UserController::class,
PostController::class,
CommentController::class,
]);
Available attributesβ
| Attribute | Target | Description |
|---|---|---|
#[Controller] | Class | Prefix, middlewares, and name prefix shared by all routes |
#[Get] | Method | GET route |
#[Post] | Method | POST route |
#[Put] | Method | PUT route |
#[Patch] | Method | PATCH route |
#[Delete] | Method | DELETE route |
#[Options] | Method | OPTIONS route |
#[Route] | Method | Generic multi-method route: #[Route('/path', methods: ['GET', 'POST'])] |
Each method attribute is repeatable: the same method can serve multiple paths or verbs.
Composing namesβ
If the #[Controller] declares a name prefix (name: 'users.'), it is
concatenated verbatim to each route's name. For example:
#[Controller(name: 'users.')]
class UserController {
#[Get('/', name: 'index')] // -> final name: "users.index"
public function index() { /* ... */ }
#[Get('/:id', name: 'show')] // -> final name: "users.show"
public function show() { /* ... */ }
}
The separator (., :, empty) is entirely up to you β add it to the
prefix if you want one.
Only the methods declared on the controller itself are scanned. The
#[Route] attributes carried by a parent class are not re-registered
on the subclass β this avoids duplicate routes if you share code
via inheritance.
Is something missing?
If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.