agashe / sigmaphp-router
PHP Router
Installs: 45
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/agashe/sigmaphp-router
Requires (Dev)
- phpunit/phpunit: ^9.5
README
A fast and simple router for PHP , you can use for your projects to provide user friendly URLs , you can build your app in a simple functional style , to write RESTfull API services or to build a fully functional MVC.
Features
- Support placeholder parameters e.g
{name} - Use optional parameters with the route
- Auto parsing for the GET query parameters
- Support all HTTP methods GET, POST, PUT ... etc
- Routes can accept multiple HTTP methods
- Support
anymethod , so the route can accept all HTTP methods - Routes Validation using regex expressions
- Actions can be implemented as regular functions or controller's method
- Support for Single Action Controllers
- Middlewares , that can be run before your route
- Route Groups which support middlewares , controllers and prefix
- URL generation using the route name
- Default page not found (404) handler and you can define custom handler
- Custom action runners , for advanced customization
- HTTP method override
Installation
composer require agashe/sigmaphp-router
Configurations
Depending on your server , you will need to use one of the provided server configs , including with the router config files for 4 types of servers : Apache , Nginx , Lighttpd and IIS.
To use the router with any of these servers , just copy the corresponding config file from the configs folder to the root folder of your project.
For example , in case you are using Apache server , copy the apache_htaccess file and rename it to the proper name .htaccess
Please note : all of the provided config files , assume that the main entry point to your application is index.php located in the root path of your project's directory. If you have different setup for your project , check please the config and make sure it's pointing to the correct path.
So if the index.php is located under the public folder.
Then in the .htaccess file , change index.php to public/index.php , and the same goes for other servers.
Documentation
Table of Contents
- Basic Setup
- Base Path
- HTTP Methods
- Parameters
- Validation
- Actions
- Middlewares
- Route Groups
- Page not found handling
- URL Generation
- Action Runners
Basic Setup
In order to start using SigmaPHP-Router in your app , in the main entry point of your app (say for example index.php ), you first need define the routes array , then pass that array to the constructor and finally call the the run() method.
<?php
require 'vendor/autoload.php';
use SigmaPHP\Router\Router;
$routes = [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get',
'controller' => UserController::class,
'action' => 'profile',
],
];
// initialize the router
$router = new Router($routes);
// fire the router
$router->run();
Alternatively you can save your routes in a separated file and load that file in your app. Even better you can have multiple route files each serves a different purpose.
web.php
<?php
return [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get',
'controller' => UserController::class,
'action' => 'profile',
],
];
api.php
<?php
return [
[
'name' => 'api.users.profile',
'path' => '/api/v1/users/profile',
'method' => 'get',
'controller' => UserApiController::class,
'action' => 'profileJson',
],
];
and finally in index.php :
<?php
require 'vendor/autoload.php';
use SigmaPHP\Router\Router;
$webRoutes = require('web.php');
$apiRoutes = require('api.php');
// initialize the router
$router = new Router(array_merge($webRoutes, $apiRoutes));
// fire the router
$router->run();
To define a new route , you simply append an route array to the routes array , a valid route should at least have a path and an action :
$routes = [
[
'path' => '/users/profile',
'action' => 'get_user_profile',
],
];
The route's name is optional , put it is recommended to give your routes a name , so it becomes easier to work with them , like generating URL using the route's name.
Base Path
In case your application exists in sub-folder of your domain for example http://localhost/my-app , you can set the root path in the router constructor , using the second parameter:
<?php
require 'vendor/autoload.php';
use SigmaPHP\Router\Router;
$routes = [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get',
'controller' => UserController::class,
'action' => 'profile',
],
];
// define app base path
const BASE_PATH = '/my-app';
// initialize the router
$router = new Router($routes, BASE_PATH);
// fire the router
$router->run();
Further more the method getBaseUrl could be used to get the base URL from the Router , including the HTTP/S , base path and full domain :
print $router->getBaseUrl();
// will print : 'http://localhost/'
HTTP Methods
SigmaPHP-Router support all HTTP methods GET, POST, PUT, DELETE ... etc , a single route can support one or more HTTP methods :
$routes = [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get,post',
'controller' => UserController::class,
'action' => 'profile',
],
];
Also SigmaPHP-Router provides a special method type any , which allows the route to accept all of the HTTP methods :
$routes = [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'any',
'controller' => UserController::class,
'action' => 'profile',
],
];
If no HTTP was provided , then the HTP method for the route will be automatically set to GET.
Parameters
Route parameters follow the placeholder style (like Laravel) :
$routes = [
[
'name' => 'admin.users.address',
'path' => '/admin/users/{user_id}/addresses/{address_id}',
'method' => 'get',
'controller' => AdminPanelUserController::class,
'action' => 'getUserAddressDetails',
],
];
..... In AdminPanelUserController.php
public function getUserAddressDetails($userId, $addressId) {
...
}
Also optional parameters can be used by adding ? to the parameter , but this option can only be used with last parameter of your route :
$routes = [
[
'name' => 'products.list',
'path' => '/products/{id?}',
'method' => 'get',
'controller' => ProductController::class,
'action' => 'list',
],
];
So the id in this route can be omitted , so calling /products or /products/15 are fine.
and lastly don't forget to handle the optional parameter in your action , by adding a default value for the action:
..... In ProductController.php
public function list($id = null) {
...
}
In addition to the route parameters , we also have the query parameters , so automatically this will parsed and accessible through the global PHP variable $_GET , so assume in the example above you made the following call :
curl http://localhost/products?page=2&per_page=10
// so , later in your ProductController inside the list method , you can access these parameters :
public function list($id = null)
{
echo $_GET['page']; // 2
echo $_GET['per_page']; // 10
}
Validation
A validation rules can be added to your routes , in form of regular expressions :
$routes = [
[
'name' => 'orders.show',
'path' => '/orders/details/{order_id}',
'method' => 'get',
'controller' => OrderController::class,
'action' => 'show',
'validation' => [
'order_id' => '[0-9]+'
]
],
];
In the example above the router will match only order_id that only contains digits , so something like /orders/details/abcd won't be matched and the router will return 404 - page not found.
Actions
In SigmaPHP-Router an action is the handler which will be executed to process the route functionality.
Actions are divided into 2 categories , first controller based , which simply are classes that contain multiple methods , usually the controller responsible for handling tasks in which grouped by the same model or functionality like PostController or UserLoginController.
As we saw in the previous examples , we need to pass the controller name and the method name :
$routes = [
[
'name' => 'orders.show',
'path' => '/orders/details/{order_id}',
'method' => 'get',
'controller' => OrderController::class,
'action' => 'show',
],
];
We use the special constant ::class in order to get the full name including the namespace of the controller. you can instead write the full path if you prefer , for example :
$routes = [
[
'name' => 'orders.show',
'path' => '/orders/details/{order_id}',
'method' => 'get',
'controller' => App\Web\Controllers\OrderController,
'action' => 'show',
],
];
The second type of actions are functions based , and in this case you just add the function name without controller , and the router simply will call that function , so for example :
$routes = [
[
'name' => 'about_page',
'path' => '/about',
'method' => 'get',
'action' => 'create_about_page',
],
];
.... somewhere in your application define the function and call it either in the same index.php or another file
// pages.php
<?php
function create_about_page() {
print "About Us";
}
Finally SigmaPHP-Router also has support for Single Action Controllers , so no need to pass action name ,
and the router will automatically search for the PHP magic method __invoke() to run :
$routes = [
[
'name' => 'notification.send_email',
'path' => '/notification/send-email',
'method' => 'post',
'controller' => SendEmailController::class,
],
];
And in the SendEmailController :
// SendEmailController.php
<?php
class SendEmailController
{
public function __invoke()
{
// .... some code to send email
}
}
Middlewares
Usually in any application you will need to run some checks before allowing the user to perform the action , like for example check if is he logged in , has the proper permissions , and so on.
So out of the box SigmaPHP-Router provides the ability to call middlewares before the execution of your route's action.
$routes = [
[
'name' => 'orders.create',
'path' => '/orders',
'method' => 'post',
'middlewares' => [
[AuthMiddleware::class, 'handler'],
[UserCanCreateOrderMiddleware::class, 'check'],
],
'controller' => OrderController::class,
'action' => 'create'
],
];
In case of class based middlewares , the router will require the middleware class name and the name of the method that will be executed.
In addition the middlewares could be written as regular functions , and in this case we pass an array of functions name :
$routes = [
[
'name' => 'orders.create',
'path' => '/orders',
'method' => 'post',
'middlewares' => ['is_user_auth', 'check_user_permissions'],
'controller' => OrderController::class,
'action' => 'create'
],
];
Creating the middleware classes/functions is completely depending on your application , so in your middleware you could have something similar to :
<?php
class AuthMiddleware
{
public function handler()
{
session_start();
if (empty($_SESSION['user])) {
header('Location: http://example.com');
exit();
}
}
}
Route Groups
Group routes is an essential feature for any router , so you can apply certain prefix or middleware to a group of routes.
To create a new route group , use the following schema :
$routes = [
[
'group' => 'api',
'prefix' => '/api/v1/',
'controller' => UserApiController::class,
'middlewares' => [
[AuthMiddleware::class, 'handler'],
[UserAccountIsActiveMiddleware::class, 'handler'],
],
'routes' => [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get',
'middlewares' => [
[CheckIfUserCanEditProfileMiddleware::class, 'handler'],
],
'action' => 'profile'
],
]
],
];
The only items required for a group is the group name and the routes array. The name will be added to all of its routes , so in the example above , the final route name will be : api.users.profile and the route path : /api/v1/users/profile
prefix , controller and middlewares are all optional , a routes group could either has prefix controller , middlewares , all or non of them.
For the routes definition , nothing changed all features are implemented as regular routes. The only thing wroth mention , is that we can override the controller option in the group , by specifying teh controller in the route.
Please Note : SigmaPHP-Router doesn't support sub-groups yet , so you can't define a routes group inside another routes group !
Page not found handling
By default in case the requested URI didn't match , the router will return 404 HTTP status code , with simple message 404 , The Requested URL Was Not Found
You change this default behavior by passing a custom handler name as an argument to the method setPageNotFoundHandler
<?php
require 'vendor/autoload.php';
use SigmaPHP\Router\Router;
$routes = [
[
'name' => 'users.profile',
'path' => '/users/profile',
'method' => 'get',
'controller' => UserController::class,
'action' => 'profile',
],
];
// initialize the router
$router = new Router($routes);
// set custom 404 (Page Not Found) handler
$router->setPageNotFoundHandler('my_custom_404_handler');
// fire the router
$router->run();
.... and somewhere in your code , you define that function :
function my_custom_404_handler() {
http_response_code(404);
echo "<h1>My custom message</h1>";
exit();
}
So now you can add your custom 404 message , page design or redirect the user another route.
And as usual you can instead of using function handler , you can use a class , so you pass class name and the method name :
// set custom 404 (Page Not Found) handler
$router->setPageNotFoundHandler([
MyCustomPageNotFoundHandler::class, 'handle'
]);
URL Generation
In a lot of cases , you will need a way to create an URL for your models , for example a link to show order details , SigmaPHP-Router provides a method called url , which accept a route name and parameters array. and generate the URL , let's see an example :
$routes = [
[
'name' => 'order.items.details',
'path' => '/order/{order_id}/items/{item_id?}',
'method' => 'get',
'controller' => OrderController::class,
'action' => 'getOrderItems',
],
];
So to generate an URL for the route above :
$orderId = 5;
$itemId = 10;
$itemsURL = $router->url('order.items.details', [
'order_id' => $orderId,
'item_id' => $itemId,
]);
..... if we print $itemURL :
http://localhost/order/5/items/10
The router will automatically add the host and check if the https is enabled.
Also in case your route doesn't require parameters , or accept optional parameters , you can skip the second parameter.
$routes = [
[
'name' => 'print_terms_of_usage',
'path' => '/terms-of-usage',
'method' => 'get',
'action' => 'print_terms_of_usage',
],
];
$pageURL = $router->url('print_terms_of_usage');
..... if we print $pageURL :
http://localhost/terms-of-usage
Action Runners
Action runner is the execution unit inside SigmaPHP-Router , it's the last step in the route execution cycle , starting from matching to applying middlewares and other validations , we finally reach to the execution phase , in this phase the runner will take care of executing the action either by calling the action function or a method defined in class.
But in some cases , writing our custom runner might be a necessity , for example if we want to preform some logic before the execution start , for example add some logs before or after the execution or handle dependency injection.
By default SigmaPHP-Router ships with the DefaultRunner , to register your own runner , all what you have to do is to define and new class that implements the RunnerInterface :
<?php
namespace SigmaPHP\Router\Interfaces;
/**
* Runner Interface
*/
interface RunnerInterface
{
/**
* Execute the route's action.
*
* @param array $route
* @return void
*/
public function execute($route);
}
A simple interface that only has one method execute which only accepts the route as a parameter.
<?php
namespace MyApp\Util;
use SigmaPHP\Router\Interfaces\RunnerInterface;
class MyCustomRunner implements RunnerInterface
{
/**
* Execute the route's action.
*
* @param array $route
* @return void
*/
public function execute($route)
{
// here we define our execution logic , save logs
// connect to DB , inject dependencies or use call_user_func
// to call actions ... etc
}
}
Please note , that inside the execute method we have three main items defined in the route array , that we should use to execute the route :
// first is the controller name and path , which also
// could be empty if we are just calling standalone actions
// example : \MyApp\Controllers\MainController
$route['controller']
// then the action name , which again could belong to a
// controller or just a name of a function defined in our app
$route['action']
// finally the parameters array , which contains all of the
// parameters extracted form the url and this array could be empty
// if no parameters were provided
// example : ['user_id' => 5, 'order_id' => 10]
$route['parameters']
// using these information we could implement our logic like so :
call_user_func($route['action'],...$route['parameters']);
Finally we can register our custom runner using the setActionRunner method :
// initialize the router
$router = new Router($webRoutes);
// set the runner before starting the router
$router->setActionRunner(MyCustomRunner::class);
// run the router
$router->run();
The setActionRunner method also has one optional parameter
called parameters which of type array and can be used to pass arguments to the runner constructor :
// initialize the router
$router = new Router($webRoutes);
// pass the parameters array
$router->setActionRunner(MyCustomRunner::class, [$service]);
// run the router
$router->run();
And in some cases depending on how complex our project , we can control the runners based some conditions like environment variables ... etc
// initialize the router
$router = new Router($webRoutes);
// check the .env and set the runner
if ($_ENV['environment'] == 'dev') {
$router->setActionRunner(DevRunner::class);
}
else if ($_ENV['environment'] == 'prod') {
$router->setActionRunner(ProdRunner::class);
}
else {
$router->setActionRunner(DefaultRunner::class);
}
// run the router
$router->run();
HTTP Method Override
One of the limitations in HTML forms that only GET and POST methods are supported , so if we have a some routes in our application that implements other methods like PUT and DELETE , it will become nearly impossible to use these in HTML forms unless we add POST to the array of allowed methods for each route.
One popular solution for this problem is the HTTP Method Override , this is a mechanism which could be applied to GET and POST requests to change the HTTP method to the desired one.
Several implementations is out there like using X-HTTP-Method-Override header , or passing _method as a query parameter. Among these implementations the one that SigmaPHP-Router is using , is the _method hidden field. This basically only targets the HTML forms with POST method , where the developer will add an hidden input field with name _method and the desired HTTP method as value.
By default this feature is disabled. To enable this feature , all what you have to do , is just to call the enableHttpMethodOverride() method before starting the router.
// initialize the router
$router = new Router([
[
'name' => 'updateProduct',
'path' => '/products',
'method' => 'put',
'controller' => ProductController::class,
'action' => 'update',
]
]);
// enable HTTP method override
$router->enableHttpMethodOverride();
// run the router
$router->run();
Finally in your HTML form :
<form action="/products" method="POST">
<input type="hidden" name="_method" value="PUT" />
... other fields
<button type="submit">Save</button>
</form>
Examples
$routes = [
[
'name' => 'home',
'path' => '/',
'method' => 'get',
'action' => 'home_page',
],
[
'name' => 'contact_us.show',
'path' => '/contact-us',
'method' => 'get',
'controller' => ContactUsController::class,
'action' => 'showContactUsForm'
],
[
'name' => 'contact_us.submit',
'path' => '/contact-us',
'method' => 'post',
'controller' => ContactUsController::class,
'action' => 'submitContactUsForm'
],
[
'group' => 'posts',
'prefix' => 'posts/',
'middlewares' => [
[AuthMiddleware::class, 'handler'],
[UserIsActiveMiddleware::class, 'handler'],
[UserCanControlPostsMiddleware::class, 'handler'],
],
'routes' => [
[
'name' => 'list',
'path' => '/{id?}',
'method' => 'get',
'controller' => PostController::class,
'action' => 'index',
'validation' => [
'id' => '[0-9]+'
]
],
[
'name' => 'create',
'path' => '/create',
'method' => 'get,post',
'controller' => PostController::class,
'action' => 'create'
],
[
'name' => 'update',
'path' => '/update/{id}',
'method' => 'get,patch',
'controller' => PostController::class,
'action' => 'update',
'validation' => [
'id' => '[0-9]+'
]
],
[
'name' => 'delete',
'path' => '/{id}',
'method' => 'delete',
'controller' => PostController::class,
'action' => 'delete',
'validation' => [
'id' => '[0-9]+'
]
'middlewares' => [
[CheckPostIsNotPublishedMiddleware::class, 'handler'],
],
],
]
]
];
License
(SigmaPHP-Router) released under the terms of the MIT license.