PHP - Create your own PHP Router
F.R Michel
Posted on May 18, 2021
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;
}
}
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), '/'), '/');
}
}
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;
}
}
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']),
]);
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");
}
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']);
Generating URLs
echo $router->generateUri('home_page');
// /
echo $router->generateUri('api_articles', ['id' => 1]);
// /api/articles/1
Ideal for small project
Simple and easy!
https://github.com/devcoder-xyz/php-router
💖 💪 🙅 🚩
F.R Michel
Posted on May 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.