tnapf/router

Totally Not Another PHP Framework's router package

v6.0.1 2023-06-14 22:49 UTC

This package is auto-updated.

Last update: 2024-04-23 03:25:39 UTC


README

Totally Not Another PHP Framework's Route Component

Table of Contents

Installation

composer require tnapf/router

Routing

You can manually create a route and then store it with the addRoute method

use Tnapf\Router\Router;
use Tnapf\Router\Routing\Methods;
use Tnapf\Router\Routing\RouteRunner;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$router = new Router();
$handler = static function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    RouteRunner $route
) {
    $response->getBody()->write("Hello World");
    return $response;
};

$route = Route::new(uri: "uri", controller: $handler, methods: Methods::GET);
$router->addRoute($route);

If you want the same controller to be used for multiple methods you can do the following...

$route = Route::new(uri: "uri", controller: static fn() => new TextResponse(), methods: Methods::GET, Methods::POST); // can also spread Methods::ALL
$router->addRoute($route);

Routing Shorthands

Shorthands for single request methods are provided

$router->get('uri', $handler);
$router->post('uri', $handler);
$router->put('uri', $handler);
$router->delete('uri', $handler);
$router->options('uri', $handler);
$router->patch('uri', $handler);
$router->head('uri', $handler);

You can use this shorthand for a route that can be accessed using any method:

$router->all('uri', $handler);

Route Patterns

Route Patterns can be static or dynamic:

  • Static Route Patterns contain no dynamic parts and must match exactly against the path part of the current URL.
  • Dynamic Route Patterns contain dynamic parts that can vary per request. The varying parts are named subpatterns and are defined using either Perl-compatible regular expressions (PCRE) or by using placeholders

Static Route Patterns

A static route pattern is a regular string representing a URI. It will be compared directly against the path part of the current URL.

Examples:

  • /about
  • /contact

Usage Examples:

$router->get(
    '/about',
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $response->getBody()->write("About Us");
        return $response;
    }
);

Dynamic Placeholder-based Route Patterns

$router->get(
    "/profile/{username}",
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $response->getBody()->write("Hello, {$route->getParameter("username")}!");
        return $response;
    }
);

This type of Route Pattern is the same as Dynamic PCRE-based Route Patterns, but with one difference: they don't use regexes to do the pattern matching but they use the easy placeholders instead. Placeholders are strings surrounded by curly braces, e.g. {name}. Examples:

  • /movies/{id}
  • /profile/{username}

Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (.*).

$router->get(
    '/movies/{movieId}/photos/{photoId}',
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $response->getBody()
            ->write("Movie #{$route->getParameter('movieId')} | Photo #{$route->getParameter('photoId')}");
        return $response;
    }
);

Dynamic PCRE-based Route Patterns

This type of Route Pattern contains dynamic parts which can vary per request. The varying parts are named subpatterns and are defined using regular expressions.

Commonly used PCRE-based subpatterns within Dynamic Route Patterns are:

  • \d+ = One or more digits (0-9)
  • \w+ = One or more word characters (a-z 0-9 _)
  • [a-z0-9_-]+ = One or more word characters (a-z 0-9 _) and the dash (-)
  • .* = Any character (including /), zero or more
  • [^/]+ = Any character but /, one or more

Note: The PHP PCRE Cheat Sheet might come in handy.

The subpatterns defined in Dynamic PCRE-based Route Patterns are passed into the route's controller like dynamic placeholders.

$router->get(
    '/hello/{name}',
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $response->getBody()->write("Hello {$route->getParameter('name')}");
        return $response;
    }
)->setParameter('name', '[a-zA-Z]+');

]

Controllers

When defining a route you pass an instance of a class that implements Tnapf\Router\Interfaces\ControllerInterface or a closure which will get converted into an instance of Tnapf\Handlers\ClosureRequestHandler

class HelloWorld implements ControllerInterface
{
    public function handle(
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $response->getBody()->write("Hello World");
        return $response;
    }
};
$router->get("/home", new HelloWorld());

Responding to requests

All controllers MUST return an implementation of \Psr\Http\Message\ResponseInterface. You can use the premade response object passed into the controller or instantiate your own. I recommend taking a look at HttpSoft/Response for prebuilt response types.

$response = new HttpSoft\Response\HtmlResponse('<p>HTML</p>');
$response = new HttpSoft\Response\JsonResponse(['key' => 'value']);
$response = new HttpSoft\Response\JsonResponse("{key: 'value'}");
$response = new HttpSoft\Response\TextResponse('Text');
$response = new HttpSoft\Response\XmlResponse('<xmltag>XML</xmltag>');
$response = new HttpSoft\Response\RedirectResponse('https/example.com');
$response = new HttpSoft\Response\EmptyResponse();

Catchable Routes

Catchable routes are routes that are only invoked when exceptions are thrown while handling a request. To create a catchable route you can do the following...

Catching

$router->catch(
    Throwable::class,
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $exception = $route->exception;
        $exceptionString = $exception->getMessage() . "\n" . $exception->getTraceAsString();
        $logs = fopen("./error.log", "w+");
        fwrite($logs, $exceptionString);
        fclose($logs);

        $response->getBody()->write($exceptionString);
        return $response->withHeader("content-type", "text/plain");
    }
);

Note that $route->exception will only be instantiated when catching. Also note that catching \Throwable will catch EVERY exception but catching \Exception will only catch \Exception

Specific URI's

$router->catch(
    Throwable::class,
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ) {
        $response->getBody()->write("{$request->getUri()->getPath()} is not valid");
        return $response;
    },
    "/users/{id}"
)->setParameter("id", "[0-9]{4}");

Note: Catchers are treated just like routes meaning they can have custom parameters

Middleware

Middleware is part of the request handling process that comes before the route controller is invoked.

A good example of middleware is making sure the user is an administrator before they go to a restricted page. You could do this in your routes controller for every admin page sure but, that would be redundant.

You can add middleware to a route by invoking the addMiddleware method and supply controller(s).

NOTE: The controllers will be invoked in the order they're appended!

$router->get(
    "/users/{username}",
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $response->getBody()->write("Viewing {$route->getParameter("username")}'s profile");
        return $response;
    }
)->setParameter("id", "[0-9]{4}")->addMiddleware(
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $users = ["command_string", "realdiegopoptart"];

        if (!in_array(strtolower($route->getParameter("username")), $users)) {
            return $response->withStatus(404);
        }

        return $route->next($request, $response);
    }
);

*Another Note: If you don't want to proceed to the next part of request just return a ResponseInterface instead of invoking RouteRunner::Next

Postware

Postware is a type of middleware that operates on the response generated by the Controller and can modify the response data before it is sent to the client. While it doesn't sit between the Controller and the View, it does operate on the response after the View has been generated.

Adding postware is just like middleware, just with a different method.

use HttpSoft\Response\EmptyResponse;

$router->get( // runs second
    "/users/{username}",
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $response->getBody()->write("Viewing {$route->getParameter("username")}'s profile");
        return $route->next($request, $response);
    }
)->setParameter("username", "[A-z\_]+")->addMiddleware( // runs first
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $users = ["command_string", "realdiegopoptart"];

        if (!in_array(strtolower($route->getParameter("username")), $users)) {
            return new EmptyResponse(404)
        }

        return $route->next($request, $response);
    }
)->addPostware( // runs third
    static function (
        ServerRequestInterface $request,
        ResponseInterface $response,
        RouteRunner $route
    ): ResponseInterface {
        $stream = fopen(__DIR__ . "/access-logs.txt", "a");
        fwrite($stream, "{$route->getParameter("username")}'s profile was accessed at " . date("Y-m-d H:i:s") . "\n");
        fclose($stream);

        return $response;
    }
);

Group routes

If you have multiple routes to inherit the same base uri and the same before/after middleware then you can define them inside the group method...

$router->group("/users", static function (Router $router): void {
    $router->get("/", static fn(): TextResponse => new TextResponse("List Users"));

    $router->group(
        "/{id}",
        static function (Router $router): void {
            $router->get(
                "/",
                static function (
                    ServerRequestInterface $request,
                    ResponseInterface $response,
                    RouteRunner $route
                ): ResponseInterface {
                    $response->getBody()->write(" profile");
                    return $route->next($request, $response);
                }
            );

            $router->get(
                "/json",
                static function (
                    ServerRequestInterface $request,
                    ResponseInterface $response,
                    RouteRunner $route
                ): ResponseInterface {
                    $response->getBody()->write(" json object");
                    return $route->next($request, $response);
                }
            );
        },
        middlewares: [
            static function (
                ServerRequestInterface $request,
                ResponseInterface $response,
                RouteRunner $route
            ): ResponseInterface {
                $response = new TextResponse("User {$route->args->id}");
                $response->getBody()->seek(0, SEEK_END);
                return $route->next($request, $response);
            }
        ],
        postwares: [
            static function (
                ServerRequestInterface $request,
                ResponseInterface $response,
                RouteRunner $route
            ): ResponseInterface {
                $response->getBody()->write($route->args->eof);
                return $response;
            }
        ],
        parameters: [
            "id" => "[0-9]{8}"
        ],
        staticArguments: [
            "eof" => "!"
        ]
    );
});

Static Arguments

If you want to pass static arguments to your controller you can do so by using the addArgument method on the Route object.

$router->get("/staticPage", static fn($request, $response, $route) => new TextResponse($route->getParameter("path")))
    ->addArgument("path", __DIR__ . "/index.html")
;

You would then be able to access the argument in your controller like any other argument. Note that all static arguments will override any arguments that are passed in the URI.

React/Http Integration

<?php

use HttpSoft\Response\TextResponse;
use Psr\Http\Message\ResponseInterface;
use Tnapf\Router\Router;

require_once __DIR__ . "/../vendor/autoload.php";

$router = new Router();

$router->get("/", static fn(): ResponseInterface => new TextResponse("Hello World!")); // register routes outside the HttpServer closure!!

$http = new React\Http\HttpServer(static function (Psr\Http\Message\ServerRequestInterface $request) use ($router) {
    return $router->run($request); // pass the request to the router
});

$socket = new React\Socket\SocketServer('0.0.0.0:8000');

$http->listen($socket);