phputil / router
ExpressJS-like router for PHP
Requires
- php: >=7.4
- ext-json: *
- ext-mbstring: *
- ext-pcre: *
Requires (Dev)
- php: >=8.1
- friendsofphp/php-cs-fixer: ^2
- kahlan/kahlan: ^5.2
- phpstan/phpstan: ^1.9
- phputil/restage: ^0.4.1
- rector/rector: ^1.2
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
Was it useful for you? Consider giving it a Star ⭐
Installation 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'] ) ] );
Middlewares
You may also want to install the following middlewares:
- phputil/cors - CORS Middleware
- phputil/csrf - Anti CSRF Middleware
ℹ Did you create a useful middleware? Open an Issue for including it here.
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();
ℹ Interested in helping us? Submit a Pull Request with a new example or open an Issue with your 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
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 */ } ) ;
⚠️ Don't forget to finish a route/group with the method end()
.
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.
Syntax:
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; /** * Returns all the response headers. If a header key is given, it returns all the headers with the given key. * The headers are returned as an array of [ key, value ] pairs. * * Example: `[['Set-Cookie', 'foo=1;'], ['Set-Cookie', 'bar=hello;'], ['Content-Type', 'application/json']]` * * Note that the inner arrays do not have keys. * * @param string $header HTTP header. Optional, it default to `''`. * @return array<int, array<int, string>> */ function getHeaders( string $header = '' ): array; /** * Removes the first header with the given key. Optionally removes all the headers with the given key. * * @param string $header Header to remove. * @param bool $removeAll Option (default `false`) to remove all the headers with the given key. * @return int The number of removed headers. */ function removeHeader( string $header, bool $removeAll = false ): int; /** * 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"