Skip to main content
Version: 5.x

Routing in BowPHP

Introduction​

About routing

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!';
});
What is a closure?

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);
Warning

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';
});
Important note

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​

The match method

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
});
});
Important limitation

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}");
});
Reset after the callback

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');
Use cases

Subdomain constraints are especially useful for:

  • Multi-tenant applications (client1.app.com, client2.app.com)
  • Separating the admin and public interfaces (admin.site.com vs www.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+'
]);
Variable validation

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​

ParameterTypeDescription
pathStringThe URL path to match
methodString, ArrayHTTP method (GET, POST, PUT, DELETE, OPTIONS, PATCH)
handlerString, Closure, ArrayAction to execute
middlewareString, ArrayMiddleware(s) to apply before executing the action
whereString, ArrayConstraint(s) applied to the variables in the URL
Important

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'
]);
Learn more

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']
]);
Learn more

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]+']
]);
Advantage of the route method

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​

AttributeTargetDescription
#[Controller]ClassPrefix, middlewares, and name prefix shared by all routes
#[Get]MethodGET route
#[Post]MethodPOST route
#[Put]MethodPUT route
#[Patch]MethodPATCH route
#[Delete]MethodDELETE route
#[Options]MethodOPTIONS route
#[Route]MethodGeneric 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.

Inherited methods

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.