一个简单的 HTTP 路由

2023/11/21 计算机网络

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();
}

更复杂的路由可以在以上代码的基础上修改,如添加路由分组等特性。

Search

    Table of Contents