phputil / router
ExpressJS-like router for PHP
Requires
- php: >=7.4
- ext-json: *
- ext-mbstring: *
- ext-pcre: *
Requires (Dev)
- kahlan/kahlan: ^5.2
- phpstan/phpstan: ^1.9
README
phputil/router
ExpressJS-like router for PHP
- No third-party dependencies
- Unit-tested
- Mockable - it's easy to create automated tests for your API
👉 Do NOT use it in production yet - just for toy projects.
Installation
Requires PHP 7.4+
composer require phputil/router
👉 You may also like to install phputil/cors.
Notes
- Unlike ExpressJS,
phputil/router
needs an HTTP server to run (if the request is not mocked). You can use the HTTP server of your choice, such asphp -S localhost:80
, Apache, Nginx or http-server.- See Server Configuration for more information.
- If you are using Apache or Nginx, you may need to inform the
rootURL
parameter when callinglisten()
. Example:// Sets the 'rootURL' to where the index.php is located. $app->listen( [ 'rootURL' => dirname( $_SERVER['PHP_SELF'] ) ] );
Examples
Hello World
require_once 'vendor/autoload.php'; use \phputil\router\Router; $app = new Router(); $app->get( '/', function( $req, $res ) { $res->send( 'Hello World!' ); } ); $app->listen();
Using parameters
require_once 'vendor/autoload.php'; use \phputil\router\Router; $app = new Router(); $app->get( '/', function( $req, $res ) { $res->send( 'Hi, Anonymous' ); } ) ->get( '/:name', function( $req, $res ) { $res->send( 'Hi, ' . $req->param( 'name' ) ); } ) ->get( '/json/:name', function( $req, $res ) { $res->json( [ 'hi' => $req->param( 'name' ) ] ); } ); $app->listen();
Middleware per route
require_once 'vendor/autoload.php'; use \phputil\router\Router; $middlewareIsAdmin = function( $req, $res, &$stop ) { session_start(); $isAdmin = isset( $_SESSION[ 'admin' ] ) && $_SESSION[ 'admin' ]; if ( $isAdmin ) { return; // Access allowed } $stop = true; $res->status( 403 )->send( 'Admin only' ); // Forbidden }; $app = new Router(); $app->get( '/admin', $middlewareIsAdmin, function( $req, $res ) { $res->send( 'Hello, admin' ); } ); $app->listen();
ℹ To help us with an example, just submit a Pull Request or open an Issue with the code.
Features
- [✔] Support to standard HTTP methods (
GET
,POST
,PUT
,DELETE
,HEAD
,OPTIONS
) andPATCH
. - [✔] Route parameters
- e.g.
$app->get('/customers/:id', function( $req, $res ) { $res->send( $req->param('id') ); } );
- e.g.
- [✔] URL groups
- e.g.
$app->route('/customers/:id')->get('/emails', $cbGetEmails );
- e.g.
- [✔] Global middlewares
- e.g.
$app->use( function( $req, $res, &$stop ) { /*...*/ } );
- e.g.
- [✔] Middlewares per URL group
- e.g.
$app->route( '/admin' )->use( $middlewareIsAdmin )->get( '/', function( $req, $res ) { /*...*/ } );
- e.g.
- [✔] Middlewares per route
- e.g.
$app->get( '/', $middleware1, $middleware2, function( $req, $res ) { /*...*/ } );
- e.g.
- [✔] Request cookies
- e.g.
$app->get('/', function( $req, $res ) { $res->send( $req->cookie('sid') ); } );
- e.g.
- [✔] Extra: Can mock HTTP requests for testing, without the need to running an HTTP server.
- [🕑] (soon) Deal with
multipart/form-data
onPUT
andPATCH
Known Middlewares
- phputil/cors - CORS Middleware
- phputil/csrf - Anti CSRF Middleware
ℹ Did you create a useful middleware? Open an Issue for including it here.
API
This library does not aim to cover the entire ExpressJS API. However, feel free to contribute to this project and add more features.
Types:
Middleware
In phputil/router
, a middleware is a function that:
- Perform some action (e.g., set response headers, verify permissions) before a route is evaluated.
- Can stop the router, optionally setting a response.
Syntax:
function ( HttpRequest $req, HttpResponse $res, bool &$stop = false )
where:
$req
allows to get all the request headers and data.$res
allows to set all the response headers and data.$stop
allows to stop the router, when set totrue
.
Router
Class that represents a router.
get
Method that deals with a GET
HTTP request.
function get( string $route, callable ...$callbacks )
where:
$route
is a route (path).$callbacks
can receive none, one or more middleware functions and one route handler - which must be the last function.
A route handler has the following syntax:
function ( HttpRequest $req, HttpResponse $res )
where:
$req
allows to get all the request headers and data.$res
allows to set all the response headers and data.
Examples:
use \phputil\router\HttpRequest; use \phputil\router\HttpResponse; $app-> get( '/hello', function( HttpRequest $req, HttpResponse $res ) { $res->send( 'Hello!' ); } ) get( '/world', // Middleware function( HttpRequest $req, HttpResponse $res, bool &$stop ) { if ( $req->header( 'Origin' ) === 'http://localhost' ) { $res->status( 200 )->send( 'World!' ); $stop = true; } }, // Route handler function( HttpRequest $req, HttpResponse $res ) { $res->status( 400 )->send( 'Error: not in http://localhost :(' ); } );
post
Method that deals with a POST
HTTP request. Same syntax as get's.
put
Method that deals with a PUT
HTTP request. Same syntax as get's.
delete
Method that deals with a DELETE
HTTP request. Same syntax as get's.
head
Method that deals with a HEAD
HTTP request. Same syntax as get's.
option
Method that deals with a OPTION
HTTP request. Same syntax as get's.
patch
Method that deals with a PATCH
HTTP request. Same syntax as get's.
all
Method that deals with any HTTP request. Same syntax as get's.
group
Alias to the method route.
route
Method that adds a route group, where you can register one or more HTTP method handlers.
Example:
$app-> route( '/employees' ) ->get( '/emails', function( $req, $res ) { /* GET /employees/emails */ } ) ->get( '/phone-numbers', function( $req, $res ) { /* GET /employees/phone-numbers */ } ) ->post( '/children', function( $req, $res ) { /* POST /employees/children */ } ) ->end() // Finishes the group and back to "/" ->get( '/customers', function( $req, $res ) { /* GET /customers */ } ) ;
end
Method that finishes a route group and returns to the group parent.
Example:
$app-> route( '/products' ) ->get( '/colors', function( $req, $res ) { /* GET /products/colors */ } ) ->route( '/suppliers' ) ->get( '/emails', function( $req, $res ) { /* GET /products/suppliers/emails */ } ) ->end() // Finishes "/suppliers" and back to "/products" ->get( '/sizes', function( $req, $res ) { /* GET /products/sizes */ } ) ->end() // Finishes "/products" and back to "/" ->get( '/sales', function( $req, $res ) { /* GET /sales */ } ) ;
use
Method that adds a middleware to be evaluated before the routes declared after it.
Example:
$app ->use( $myMiddlewareFunction ) ->get( '/hello', $sayHelloFunction ); // Executes after the middleware
listen
Method that executes the router.
function listen( array|RouterOptions $options = [] ): void
Options are:
rootURL
is a string that sets the root URL. Example:dirname( $_SERVER['PHP_SELF'] )
. By default it is''
.req
is an object that implements the interfaceHttpRequest
, which retrieves all the headers and data from a HTTP request. Changing it is only useful if you want to unit test your API - see Mocking an HTTP request. By default, it will receive an object from the classRealHttpRequest
.res
is an object that implements the interfaceHttpResponse
. You probably won't need to change its value. By default, it will receive an object from the classRealHttpResponse
.
Example:
// Sets the 'rootURL' to where the index.php is located. $app->listen( [ 'rootURL' => dirname( $_SERVER['PHP_SELF'] ) ] );
You can also use an instance of RouterOptions
for setting the options:
use phputil\router\RouterOptions; // Sets the 'rootURL' to where the index.php is located. $app->listen( ( new RouterOptions() )->withRootURL( dirname( $_SERVER['PHP_SELF'] ) ) );
RouterOptions
withRootURL
withRootURL( string $url ): RouterOptions
withReq
withReq( HttpRequest $req ): RouterOptions
withRes
withRes( HttpResponse $res ): RouterOptions
HttpRequest
Interface that represents an HTTP request.
API:
interface HttpRequest { /** Returns the current URL or `null` on failure. */ function url(): ?string; /** Returns the current URL without any queries. E.g. `/foo?bar=10` -> `/foo` */ function urlWithoutQueries(): ?string; /** Returns the URL queries. E.g. `/foo?bar=10&zoo=A` -> `['bar'=>'10', 'zoo'=>'A']` */ function queries(): array; /** Returns all HTTP request headers */ function headers(): array; /** Returns the header with the given case-insensitive name, or `null` if not found. */ function header( $name ): ?string; /** Returns the raw body or `null` on failure. */ function rawBody(): ?string; /** * Returns the converted content, depending on the `Content-Type` header: * - For `x-www-form-urlencoded`, it returns an `array`; * - For `application/json`, it returns an `object` or an `array` (depending on the content). * - Otherwise it returns a `string`, or `null` on failure. */ function body(); /** Returns the HTTP request method or `null` on failure. */ function method(): ?string; /** Returns all cookies as an array (map). */ function cookies(): array; /** * Returns the cookie value with the given case-insensitive key or `null` if not found. * * @param string $key Cookie key. * @return string|null */ function cookie( $key ): ?string; /** * Returns a URL query or route parameter with the given name (key), * or `null` when the given name is not found. * * @param string $name Parameter name. * @return string */ function param( $name ): ?string; /** * Returns all the URL queries and route parameters as an array (map). * @return array */ function params(): array; /** * Returns extra, user-configurable data. * @return ExtraData */ function extra(): ExtraData; }
ExtraData
Extra, user-defined data.
class ExtraData { /** * Sets a value to the given key. Chainable method. * * @param string|int $key * @param mixed $value * @return ExtraData */ function set( $key, $value ): ExtraData; /** * Returns the value for the given key, or null otherwise. * @param string|int $key * @return mixed */ function get( $key ); /** * Returns the keys and values as an array. */ function toArray(): array; }
HttpResponse
Interface that represents an HTTP response.
Most of its methods are chainable, that is, you can call them in a sequence. Example:
$response->status( 201 )->send( 'Saved successfully.' );
API:
interface HttpResponse { /** * Sets the HTTP status code. * * @param int $code HTTP status code. * @return HttpResponse */ function status( int $code ): HttpResponse; /** * Indicates if the current HTTP status code is equal to the given one. * * @param int $code HTTP status code. * @return bool */ function isStatus( int $code ): bool; /** * Sets an HTTP header. * * @param string $header HTTP header. * @param string|int|float|bool|array $value Header value. * @return HttpResponse */ function header( string $header, $value ): HttpResponse; /** * Indicates if the response has the given HTTP header. * * @param string $header HTTP header. * @return boolean */ function hasHeader( string $header ): bool; /** * Returns the response header, if it exists. Returns `null` otherwise. * * @param string $header HTTP header. * @return string|null */ function getHeader( string $header ): ?string; /** * Removes a header. * * @param string $header Header to remove. */ function removeHeader( string $header ): void; /** * Sets a redirect response. * * @param int $statusCode HTTP status code. * @param string|null $path Path. * @return HttpResponse */ function redirect( int $statusCode, $path = null ): HttpResponse; /** * Sets a cookie. * * @param string $name Name (key) * @param string $value Value. * @param array $options Optional map with the following options: * - `domain`: string * - `path`: string * - `httpOnly`: true|1 * - `secure`: true|1 * - `maxAge`: int * - `expires`: string * - `sameSite`: true|1 * @return HttpResponse * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies for options' meanings. */ function cookie( string $name, string $value, array $options = [] ): HttpResponse; /** * Clears a cookie with the given name (key). * * @param string $name Name (key) * @param array $options Optional map with the same options as #cookie()'s. * @return HttpResponse */ function clearCookie( string $name, array $options = [] ): HttpResponse; /** * Sets the `Content-Type` header with the given MIME type. * * @param string $mime MIME type. * @return HttpResponse */ function type( string $mime ): HttpResponse; /** * Sends the given HTTP response body. * * @param mixed $body Response body. * @return HttpResponse */ function send( $body ): HttpResponse; /** * Sends a file based on its path. * * @param string $path File path * @param array $options Optional map with the options: * - `mime`: string - MIME type, such as `application/pdf`. * @return HttpResponse */ function sendFile( string $path, array $options = [] ): HttpResponse; /** * Send the given content as JSON, also setting the needed headers. * * @param mixed $body Content to send as JSON. * @return HttpResponse */ function json( $body ): HttpResponse; /** * Ends the HTTP response. * * @param bool $clear If it is desired to clear the headers and the body after sending them. It defaults to `true`. */ function end( bool $clear = true ): HttpResponse; }
Mocking an HTTP request
👉 Useful for API testing
require_once 'vendor/autoload.php'; use \phputil\router\FakeHttpRequest; use \phputil\router\Router; $app = new Router(); // Set a expectation $app->get( '/foo', function( $req, $res ) { $res->send( 'Called!' ); } ); // Mock the request $fakeReq = new FakeHttpRequest(); $fakeReq->withURL( '/foo' )->withMethod( 'GET' ); // Use the mock request $app->listen( [ 'req' => $fakeReq ] ); // It will use the fake request to call "/foo"