alclab/subway-router

1.3.1 2025-01-21 03:15 UTC

This package is auto-updated.

Last update: 2025-05-28 08:38:43 UTC


README

SubwayPHP - php router

Usage

Basic example

use Subway\Map;

$map = new Map(); // create router
$map->route('GET', '/', function ($request, $response, $route) {
    // home page
    return $response->body('home page');
});
$map->group('/news', function ($group) {
    $group->route('GET', '/', function ($request, $response, $route) {
        // news page
        return $response->body('news');
    });
    $group->route('GET', '/{id:i}', function ($request, $response, $route) {
        $postId = (int)$request->segment(2);
        // article page
        if($postId < 1) {
            return $response->status(404)->body('Not found');
        } else return $response->body("Article: {$pageId}");
    });
});
$map->build(); // routes built and ready to use
$map->dispatch(); // route

Install

composer require alclab/subway-router

Routes

Create routes map

use Subway\Map;

$map = new Map();

Static path

$map->route('GET', '/foo/bar', function($request, $route) {});

Variables in url

$map->route('GET', '/foo/{bar}') // {bar} - any string
$map->route('GET', '/foo/{bar:i}') // {bar:i} - integers only
$map->route('GET', '/foo/{bar:a}') // {bar:a} - allowed alpha (a-zA-Z), underscores and dashes
$map->route('GET', '/foo/{bar:[a-z]{2}\d+}') - pattern matching (case insensitive)

Optional segments

$map->route('GET', '/foo?/{bar}') // first segment is optional

Groups

$map->group('/foo', function ($group) {
    $group->route('GET', '/', function ($req) {}); // GET /foo
    $group->route('GET', '/bar', function ($req) {}); // GET /foo/bar
});

Preparing and dispatching routes

Build

Your router has to be compiled before dispatching.

$map->build(); // now router is ready to use

You should re-build your router with any changes in your routes setup

Dispatch

// using current request data from $_SERVER, $_POST, $_COOKIES etc...
$map->dispatch();

// using custom Request instance
$req = new Request(
    $_SERVER['REQUEST_METHOD'],
    $_SERVER['REQUEST_URI'],
    $_POST,
    getallheaders(),
    $_COOKIE,
    @file_get_contents('php://input'),
    $_FILES,
);
$map->displatch($req);

Routes estimation and priorities

Static segments has the highest priority.
Pattern segments has the medium priority.
Variable (any) segments has the lowest priority.

$map->route('GET', '/foo/{bar}')->name('any')
$map->route('GET', '/foo/{bar:i}')->name('integer')
$map->route('GET', '/foo/bar')->name('static')

$map->dispatch(new Request('GET', '/foo/bar')) // route named 'static' will be loaded
$map->dispatch(new Request('GET', '/foo/foo')) // route named 'any' will be loaded

Optional segments has less priority than mandatory ones

$map->route('GET', '/foo?/bar')->name('first')
$map->route('GET', '/foo/bar')->name('second')

$map->dispatch(new Request('GET', '/foo/bar')) // route named 'second' will be loaded
// Technically both routes are fits such request,
// but 'first' route will get less estimation due to an optional segment.
// In this example 'first' route will never be loaded

Middleware hooks

use Subway\Map;
use Subway\Route;
use Subway\Request;
use Subway\Response;
use Subway\Middleware;

class CustomMiddleware extends Middleware {

    // execute after route estimated
    public function onEstimated(int $rate, Request $request, Route $route) : int {
        // this hook should return integer
        // return -1 to skip current route
        return $rate;
    }

    // execute before route loaded
    public function onResolving(callable $onLoad, Request $request, Response $response, Route $route) : callable {
        // this should return function. You can return $onLoad callback to keep default behavior
        return $onLoad;
    }

    // execute after route loaded
    public function onResolved(Request $request, Response $response, Route $route) : void {
        // this hook don't return anything
        // you can still manipulate with $response
        if($response->body !== 'ok') $response->status(404);
    }

}

$map = new Map();

$map->route('GET', '/foo/bar', function ($request, $response, $route) {
    return $response->body('not ok');
})->middleware(new CustomMiddleware());

$map->group('/', function ($group) {
    $group->route('GET', '/foo/bar', function ($request, $response, $route) {
        return $response->body('ok');
    });
})->middleware(new CustomMiddleware());

$map->build();
$map->dispatch();

Multiple middleware

class Logger extends Middleware {
    
    public function onResolved($request, $response, $route) {
        print_r([ $request->path, $request->query, $route->name ]);
    }
    
}

class Auth extends Middleware {
    
    public function onResolving($onLoad, $request, $response) {
        // check if user logged in
        if(isset($_SESSION['userId'])) {
            return $onLoad; // return default route loader
        } else {
            return function () {
                // return 401 code or redirect to login page
            };
        }
    }
    
}

$map->group('/', function ($group) {
    ...
})->middleware(new Logger(), new Auth());

Object reference

Request

new Subway\Request(string $method, string $url, array $post=[], array $headers=[], array $cookies=[], string | null $body=null, array $files=[])
    ->method : string // returns HTTP method
    ->url : string // returns full request URL
    ->origin : string // returns request origin (https://example.com)
    ->path : string // returns URI path (foo/bar)
    ->query : string // returns query string (foo=1&bar=2)
    ->params : array // returns POST params ([ 'foo' => 1, 'bar' => 2 ])
    ->param(string $paramName) : string // returns POST param by name
    ->segments : array // returns URI segments array ([ 'foo', 'bar' ])
    ->segment(int $segmentNumber) : string // returns segment by number (starts from 1)
    ->keys : array // return GET params ([ 'foo' => '1', 'bar' => '2' ])
    ->key(string $keyName) : string // returns GET property by name
    ->headers : array // returns HTTP headers array
    ->header(string $headerName) : string // returns HTTP headers by name
    ->cookies : array // returns cookies array
    ->cookie(string $cookieName) : string // returns cookie by cookie name
    ->body : string // returns request body string
    ->json(assoc=true) : array // returns array of decoded request body
    ->files : array // returns $_FILES array
    // static methods
    ::getCurrent() : Request // returns Request instance witch represents current request

Response

new Subway\Response()
    ->status : int // returns status code
    ->headers : array // returns headers array
    ->body : ?string // returns body or null
    ->json : ?array // returns content array or null
    ->status(int $statusCode) : $this // set status code
    ->header(string $name, string $value) : $this // set header
    ->body(string $data) : $this // set body content
    ->json(array $data) : $this // set body content as array
    ->redirect(string $url, int $statusCode=302) : void // redirects to given URL with given status code (default 302)

Route

Subway\Route
    ->method : string // http method
    ->name : string // name of route or empty string if not defined
    ->groups : array // array with names of groups which contain this route 
    ->inGroup(string $name) : bool // check if this route in group with specific name
    ->estimate(Subway\Request $request) : int // estimate route (onEstimated() middleware hooks will be executed if exists)
    ->getUrl(array $props) : string // get URL of this route. Keys from 'props' parameter will be used to replace variable segments. Example: $map->route('/foo/{bar}')->getUrl([ 'bar' => 'something' ]) will return '/foo/something'
    ->resolve(Subway\Request $request) // load route (onResolving() and onResolved() middleware hooks will be executed if exists)