HTTP
路由是一个负责将HTTP
请求路由到对应控制器的组件,它可以将应用的逻辑解耦到不同的控制器中,让代码易于维护。
路由有很多实现的方式,例如通过注解如#[Route('/foo/bar', methods: ['GET', 'POST'])]
、路由配置文件、编程语言本身等等。下面以PHP
的路由作为示例,路由需要两个基本的功能:1、注册;2、分发。
注册功能并不复杂,只需要将请求方法、请求路径以及回调方法一一映射即可,将要实现的路由简单支持的全路径、命名正则以及命名路径匹配,全路径如/foo/bar
,命名正则如{id:\d+}
将匹配\d+
正则的路径命名为id
,{name}
将普通路径命名为name
。代码:
class Router
{
private array $routes = [];
public function addRoute(string $method, string $uri, callable $controller): void
{
$uri_arr = explode('/', $uri);
if (count($uri_arr) < 2) {
return;
}
foreach ($uri_arr as $key => $item) {
// {id:\d+}
if (preg_match('~^\{([\s\S]+?):([\s\S]+?)}$~', $item)) {
$uri_arr[$key] = preg_replace('~^\{([\s\S]+?):([\s\S]+?)}$~', '(?<\1>\2)', $item);
// {name}
} else if (preg_match('~^\{([\s\S]+?)}$~', $item)) {
$uri_arr[$key] = preg_replace('~^\{([\s\S]+?)}$~', '(?<\1>[\s\S]+?)', $item);
}
}
$uri = implode('/', $uri_arr);
$this->routes[$method][$uri] = $controller;
}
}
注册方法用法如下:
$router = new Router();
$router->addRoute('GET', '/', [FooController::class, 'index']); // 路径 /
$router->addRoute('GET', '/{bar}/a', [FooController::class, 'bar']); // 路径 /foo/a 或 /bar/a 等等
$router->addRoute('GET', '/regex/{name}/{id}', [FooController::class, 'regex']); // 路径 /regex/foo/1 等等
$router->addRoute('GET', '/regex/{value:\d+}', [FooController::class, 'regex']); // 路径 /regex/1 等等
我们在这模拟了框架的控制器作为路由的回调方法:
class CoreController { }
class FooController extends CoreController
{
public function index(RequestInterface $request): void
{
var_dump(__METHOD__, $request);
}
public function bar(RequestInterface $request): void
{
var_dump(__METHOD__, $request);
}
public function regex(RequestInterface $request): void
{
var_dump(__METHOD__, $request, $request->getPathParams());
}
}
控制器的方法都有一个RequestInterface $request
参数,由于PHP
存在多种SAPI
,可以根据RequestInterface
接口自行实现各自的请求类。请求接口:
interface RequestInterface
{
public function getMethod(): string;
public function getPathInfo(): string;
public function setPathParams(string $name, string $value): void;
public function getPathParams(string $name): array;
}
路由以及模拟框架都装备好了,接下来就可以进行路由分发:
class Router
{
// ...
public function match(RequestInterface $request)
{
$method = $request->getMethod();
$uri = $request->getPathInfo();
if (isset($this->routes[$method][$uri])) {
return $this->routes[$method][$uri];
} else if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $pattern => $callable) {
if (preg_match(sprintf('~^%s$~', $pattern), $uri, $match)) {
foreach ($match as $k => $v) {
if (is_string($k)) {
$request->setPathParams($k, $v);
}
}
return $this->routes[$method][$pattern];
}
}
}
return null;
}
public function dispatch(RequestInterface $request): void
{
$controller = $this->match($request);
if ($controller !== null) {
call_user_func($controller, $request);
} else {
throw new Exception('Not Found');
}
}
}
路由分发的关键点在于请求路径跟注册的路由进行匹配,匹配正确即调用回调方法,否则提示404
。完整代码如下:
<?php
interface RequestInterface
{
public function getMethod(): string;
public function getPathInfo(): string;
public function setPathParams(string $name, string $value): void;
public function getPathParams(string $name): array;
}
class HTTPException extends Exception {}
class Router
{
private array $routes = [];
public function getRoutes(): array
{
return $this->routes;
}
public function addRoute(string $method, string $uri, callable $controller): void
{
$uri_arr = explode('/', $uri);
if (count($uri_arr) < 2) {
return;
}
foreach ($uri_arr as $key => $item) {
// {id:\d+}
if (preg_match('~^\{([\s\S]+?):([\s\S]+?)}$~', $item)) {
$uri_arr[$key] = preg_replace('~^\{([\s\S]+?):([\s\S]+?)}$~', '(?<\1>\2)', $item);
// {name}
} else if (preg_match('~^\{([\s\S]+?)}$~', $item)) {
$uri_arr[$key] = preg_replace('~^\{([\s\S]+?)}$~', '(?<\1>[\s\S]+?)', $item);
}
}
$uri = implode('/', $uri_arr);
$this->routes[$method][$uri] = $controller;
}
public function match(RequestInterface $request)
{
$method = $request->getMethod();
$uri = $request->getPathInfo();
if (isset($this->routes[$method][$uri])) {
return $this->routes[$method][$uri];
} else if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $pattern => $callable) {
if (preg_match(sprintf('~^%s$~', $pattern), $uri, $match)) {
foreach ($match as $k => $v) {
if (is_string($k)) {
$request->setPathParams($k, $v);
}
}
return $this->routes[$method][$pattern];
}
}
}
return null;
}
public function dispatch(RequestInterface $request): void
{
$controller = $this->match($request);
if ($controller !== null) {
call_user_func($controller, $request);
} else {
throw new HTTPException('Not Found', 404);
}
}
}
class Request implements RequestInterface
{
private array $_path_params = [];
public function getMethod(): string
{
return $_SERVER['REQUEST_METHOD'];
}
public function getPathInfo(): string
{
return $_SERVER['PATH_INFO'] === '' ? '/' : $_SERVER['PATH_INFO'];
}
public function setPathParams(string $name, string $value): void
{
$this->_path_params[$name] = $value;
}
public function getPathParams(string $name = ''): array
{
if ($name === '') {
return $this->_path_params;
}
return $this->_path_params[$name];
}
}
class CoreController { }
class FooController extends CoreController
{
public function index(RequestInterface $request): void
{
var_dump(__METHOD__, $request);
}
public function bar(RequestInterface $request): void
{
var_dump(__METHOD__, $request);
}
public function regex(RequestInterface $request): void
{
var_dump(__METHOD__, $request, $request->getPathParams());
}
}
try {
$router = new Router();
$router->addRoute('GET', '/', [FooController::class, 'index']); // 路径 /
$router->addRoute('GET', '/{bar}/a', [FooController::class, 'bar']); // 路径 /foo/a 或 /bar/a 等等
$router->addRoute('GET', '/regex/{name}/{id}', [FooController::class, 'regex']); // 路径 /regex/foo/1 等等
$router->addRoute('GET', '/regex/{value:\d+}', [FooController::class, 'regex']); // 路径 /regex/1 等等
$router->dispatch(new Request());
} catch (HTTPException $e) {
header(sprintf('HTTP/1.1 %d %s', $e->getCode(), $e->getMessage()));
echo $e->getMessage();
} catch (Throwable $t) {
echo $t->getMessage();
}
更复杂的路由可以在以上代码的基础上修改,如添加路由分组等特性。