Skip to main content
Version: CANARY 🚧

Routing in BowPHP

Introduction​

About routing

Routing is an essential part of developing a web application. It connects a URL to a specific action in the application's controller.

In BowPHP, the routing system is integrated 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 documentation on closures.

With an array of functions (callables):​

$callables = [];

$callables[] = function () {
echo 'Hello World';
return true; // Return true to run 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 __invoke method, 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 modifications 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 in 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 in a cleaner way.

$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 routes defined within 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 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 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 are then 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 conforms to a certain 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 route management 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 intervene between the request and the action to execute. 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');

Action Composition​

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, allowing 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 you to chain ->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 that 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 via attributes. No more need to maintain a central routes 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 routes 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: a single method can serve multiple paths or verbs.

Name Composition​

If the #[Controller] declares a name prefix (name: 'users.'), it is concatenated verbatim to the name of each route. 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 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 through 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.