phputil/router

ExpressJS-like router for PHP

v0.3.3 2024-11-09 21:23 UTC

README

Version Build License

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 as php -S localhost:80, Apache, Nginx or http-server.
  • 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'] ) ] );

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

Known Middlewares

ℹ 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:

  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 */ } )
    ;

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.

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"

License

MIT © Thiago Delgado Pinto