DEV Community

F.R Michel
F.R Michel

Posted on

PHP - Create your own PHP Router

A simple Router for PHP App using PSR-7 message implementation

PHP version required 7.3

Now we create a Router.php file contain the router

<?php

declare(strict_types=1);

namespace DevCoder;

use Psr\Http\Message\ServerRequestInterface;

final class Router
{
    private const NO_ROUTE = 404;

    /**
     * @var \ArrayIterator<Route>
     */
    private $routes;

    /**
     * @var UrlGenerator
     */
    private $urlGenerator;

    /**
     * Router constructor.
     * @param $routes array<Route>
     */
    public function __construct(array $routes = [])
    {
        $this->routes = new \ArrayIterator();
        $this->urlGenerator = new UrlGenerator($this->routes);
        foreach ($routes as $route) {
            $this->add($route);
        }
    }

    public function add(Route $route): self
    {
        $this->routes->offsetSet($route->getName(), $route);
        return $this;
    }

    public function match(ServerRequestInterface $serverRequest): Route
    {
        return $this->matchFromPath($serverRequest->getUri()->getPath(), $serverRequest->getMethod());
    }

    public function matchFromPath(string $path, string $method): Route
    {
        foreach ($this->routes as $route) {
            if ($route->match($path, $method) === false) {
                continue;
            }
            return $route;
        }

        throw new \Exception(
            'No route found for ' . $method,
            self::NO_ROUTE
        );
    }

    public function generateUri(string $name, array $parameters = []): string
    {
        return $this->urlGenerator->generate($name, $parameters);
    }

    public function getUrlgenerator(): UrlGenerator
    {
        return $this->urlGenerator;
    }
}

Enter fullscreen mode Exit fullscreen mode

We create a Route.php file defining a route

<?php

declare(strict_types=1);

namespace DevCoder;

/**
 * Class Route
 * @package DevCoder
 */
final class Route
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var string
     */
    private $path;

    /**
     * @var array<string>
     */
    private $parameters = [];

    /**
     * @var array<string>
     */
    private $methods = [];

    /**
     * @var array<string>
     */
    private $vars = [];

    /**
     * Route constructor.
     * @param string $name
     * @param string $path
     * @param array $parameters
     *    $parameters = [
     *      0 => (string) Controller name : HomeController::class.
     *      1 => (string|null) Method name or null if invoke method
     *    ]
     * @param array $methods
     */
    public function __construct(string $name, string $path, array $parameters, array $methods = ['GET'])
    {
        if ($methods === []) {
            throw new \InvalidArgumentException('HTTP methods argument was empty; must contain at least one method');
        }
        $this->name = $name;
        $this->path = $path;
        $this->parameters = $parameters;
        $this->methods = $methods;
    }

    public function match(string $path, string $method): bool
    {
        $regex = $this->getPath();
        foreach ($this->getVarsNames() as $variable) {
            $varName = trim($variable, '{\}');
            $regex = str_replace($variable, '(?P<' . $varName . '>[^/]++)', $regex);
        }

        if (in_array($method, $this->getMethods()) && preg_match('#^' . $regex . '$#sD', self::trimPath($path), $matches)) {
            $values = array_filter($matches, static function ($key) {
                return is_string($key);
            }, ARRAY_FILTER_USE_KEY);
            foreach ($values as $key => $value) {
                $this->vars[$key] = $value;
            }
            return true;
        }
        return false;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPath(): string
    {
        return $this->path;
    }

    public function getParameters(): array
    {
        return $this->parameters;
    }

    public function getMethods(): array
    {
        return $this->methods;
    }

    public function getVarsNames(): array
    {
        preg_match_all('/{[^}]*}/', $this->path, $matches);
        return reset($matches) ?? [];
    }

    public function hasVars(): bool
    {
        return $this->getVarsNames() !== [];
    }

    public function getVars(): array
    {
        return $this->vars;
    }

    public static function trimPath(string $path): string
    {
        return '/' . rtrim(ltrim(trim($path), '/'), '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

We finish with the Urlgenerator class to generate the urls

<?php

declare(strict_types=1);

namespace DevCoder;

final class UrlGenerator
{
    /**
     * @var \ArrayAccess<Route>
     */
    private $routes;

    public function __construct(\ArrayAccess $routes)
    {
        $this->routes = $routes;
    }

    public function generate(string $name, array $parameters = []): string
    {
        if ($this->routes->offsetExists($name) === false) {
            throw new \InvalidArgumentException(
                sprintf('Unknown %s name route', $name)
            );
        }
        $route = $this->routes[$name];
        if ($route->hasVars() && $parameters === []) {
            throw new \InvalidArgumentException(
                sprintf('%s route need parameters: %s', $name, implode(',', $route->getVarsNames()))
            );
        }
        return self::resolveUri($route, $parameters);
    }

    private static function resolveUri(Route $route, array $parameters): string
    {
        $uri = $route->getPath();
        foreach ($route->getVarsNames() as $variable) {
            $varName = trim($variable, '{\}');
            if (array_key_exists($varName, $parameters) === false) {
                throw new \InvalidArgumentException(
                    sprintf('%s not found in parameters to generate url', $varName)
                );
            }
            $uri = str_replace($variable, $parameters[$varName], $uri);
        }
        return $uri;
    }
}
Enter fullscreen mode Exit fullscreen mode

How to use ?

<?php
class IndexController {

    public function __invoke()
    {
        return 'Hello world!!';
    }
}

class ArticleController {

    public function getAll()
    {
        // db get all post
        return json_encode([
            ['id' => 1],
            ['id' => 2],
            ['id' => 3]
        ]);
    }

    public function get(int $id)
    {
        // db get post by id
        return json_encode(['id' => $id]);
    }

    public function put(int $id)
    {
        // db edited post by id
        return json_encode(['id' => $id]);
    }

    public function post()
    {
        // db create post
        return json_encode(['id' => 4]);
    }
}

$router = new \DevCoder\Router([
    new \DevCoder\Route('home_page', '/', [IndexController::class]),
    new \DevCoder\Route('api_articles_collection', '/api/articles', [ArticleController::class, 'getAll']),
    new \DevCoder\Route('api_articles', '/api/articles/{id}', [ArticleController::class, 'get']),
]);
Enter fullscreen mode Exit fullscreen mode

Example

$_SERVER['REQUEST_URI'] = '/api/articles/2'
$_SERVER['REQUEST_METHOD'] = 'GET'

try {
    // Example
    // \Psr\Http\Message\ServerRequestInterface
    //$route = $router->match(ServerRequestFactory::fromGlobals());
    // OR

    // $_SERVER['REQUEST_URI'] = '/api/articles/2'
    // $_SERVER['REQUEST_METHOD'] = 'GET'
    $route = $router->matchFromPath($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);

    $parameters = $route->getParameters();
    // $arguments = ['id' => 2]
    $arguments = $route->getVars();

    $controllerName = $parameters[0];
    $methodName = $parameters[1] ?? null;

    $controller = new $controllerName();
    if (!is_callable($controller)) {
        $controller =  [$controller, $methodName];
    }

    echo $controller(...array_values($arguments));

} catch (\Exception $exception) {
    header("HTTP/1.0 404 Not Found");
}
Enter fullscreen mode Exit fullscreen mode

How to Define Route methods

new \DevCoder\Route('api_articles_post', '/api/articles', [ArticleController::class, 'post'], ['POST']);
new \DevCoder\Route('api_articles_put', '/api/articles/{id}', [ArticleController::class, 'put'], ['PUT']);
Enter fullscreen mode Exit fullscreen mode

Generating URLs

echo $router->generateUri('home_page');
// /
echo $router->generateUri('api_articles', ['id' => 1]);
// /api/articles/1
Enter fullscreen mode Exit fullscreen mode

Ideal for small project
Simple and easy!
https://github.com/devcoder-xyz/php-router

Top comments (1)

Collapse
 
tiagofrancafernandes profile image
Tiago França

It is amazing! Thank you
FENOMENAL!