phputil/router

ExpressJS-like router for PHP

v0.3.6 2025-05-09 19:23 UTC

README

Packagist Version GitHub License Packagist Downloads Build

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 as php -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 calling listen(). 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:

ℹ 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();

See all the examples

ℹ 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) and PATCH.
  • [✔] Route parameters
    • e.g. $app->get('/customers/:id', function( $req, $res ) { $res->send( $req->param('id') ); } );
  • [✔] URL groups
    • e.g. $app->route('/customers/:id')->get('/emails', $cbGetEmails );
  • [✔] Global middlewares
    • e.g. $app->use( function( $req, $res, &$stop ) { /*...*/ } );
  • [✔] Middlewares per URL group
    • e.g. $app->route( '/admin' )->use( $middlewareIsAdmin )->get( '/', function( $req, $res ) { /*...*/ } );
  • [✔] Middlewares per route
    • e.g. $app->get( '/', $middleware1, $middleware2, function( $req, $res ) { /*...*/ } );
  • [✔] Request cookies
    • e.g. $app->get('/', function( $req, $res ) { $res->send( $req->cookie('sid') ); } );
  • [✔] Extra: Can mock HTTP requests for testing, without the need to running an HTTP server.
  • [🕑] (soon) Deal with multipart/form-data on PUT and PATCH

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:

  1. Perform some action (e.g., set response headers, verify permissions) before a route is evaluated.
  2. 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 to true.

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 interface HttpRequest, 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 class RealHttpRequest.
  • res is an object that implements the interface HttpResponse. You probably won't need to change its value. By default, it will receive an object from the class RealHttpResponse.

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

Options for the Router's listen() method.

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"

License

MIT © Thiago Delgado Pinto