前言
由于API原先使用的bramus/router在性能等方面上不如symfony/routing。在对照了大部分的PHP路由器后,虽然fastRoute也能带来足够的性能,但是不是很喜欢它的部分内容,综合考虑下来便放弃了。于是,抱着尝鲜的心态,选用了symfony的routing作为API的路由框架。
但由于本人觉得Symfony自带的路由配置组件不太符合我的部分需求,于是对它根据自己的需求进行了适配,便有了这篇文章。
使用技术栈:
- PHP 8.x
- nginx
- 聪明的脑子
- 良好的网络环境
- 能干的手
一、初识
The Routing component maps an HTTP request to a set of configuration variables.
When your application receives a request, it calls a controller action to generate the response. The routing configuration defines which action to run for each incoming URL. It also provides other useful features, like generating SEO-friendly URLs (e.g.
摘自 Symfony官方文档/read/intro-to-symfony
instead ofindex.php?article_id=57
).
优点:
- URI 识别支持 Regex(正则) 检测
- 方便,快速,能够快速上手
- 支持前缀,import,便于模块化
- 灵活性强
- 支持闭包,Controller等方式的callback
- SEO友好
二、上手
2.1 基础配置
首先,通过 composer 进行安装:
composer require symfony/routing
其次,配置好伪静态(以 Nginx 为例):
location / {
try_files $uri $uri/ /index.php?$query_string;
}
再建立一个基本的 MVC模式 的文件架构,架构内的关系正确处理好,并于伪静态规则里的内容相映射(这里不再赘述)
2.2 绑定路由
建立一个Router/SymfonyRouting.php (目录,名称可随意),并添加几个方法:
<?php
namespace App\Router;
class SymfonyRouting
{
public function __construct()
public function bind(string $method, string $pattern, array|string|\Closure $callback): self
private function map(string $method, string $pattern, \Closure|array $callback, $name = null): self
public function run(): void
}
由于使用Routing框架需要一些组件(如使用UrlMatcher去匹配路径),让我们在构造器函数里写下这些内容:
public function __construct
(
public static RouteCollection $routes ??= new RouteCollection();
public static RequestContext $context = new RequestContext();
public static Request $Request = Request::createFromGlobals();
public static UrlMatcher $matcher ??= new UrlMatcher(self::$routes, self::$context);
) {
self::$context->fromRequest(self::$Request);
}
同时,别忘了引用以下类:
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\HttpFoundation\Request;
接着,我们在bind方法写下如下内容(为了方便链式调用,此处的返回为 $this 对象):
public function bind(string $method, string $pattern, array|string|\Closure $callback, array $paramters): self
{
if (is_string($callback) && str_contains($callback, '@')) {
// 用@符号分割字符串,得到类名和方法名
list($class, $action) = explode('@', $callback);
// 用双冒号分割类名和方法名,得到标准的控制器格式
$callback = $class.'::'.$action;
}
$this->map($method,$pattern,$paramters,$callback);
return $this;
}
参数详解:
- $method: 路由支持的请求方法,支持以下类型
- GET|POST|PUT 式
- [‘GET’, ‘POST’, ‘PUT’] 式
- $pattern: 路由的路径(如需正则动态匹配,请使用 ‘/foo/{name}’ 并传入 $paramters 数组)
- $callback: 回调函数
- $paramters: 动态匹配关系的映射表
- 如: ‘foo/{id}/{name}/{sth}’ 的映射表为:array(‘{id}’ => ‘\d+’, ‘{name}’ => ‘\w+’, ‘{sth}’ => ‘.*’),具体情况详见Routing文档,不多强调
这样一来,我们便支持Callback的如下引用方式:
- Controller@someMethod 式
- [new Controller(), ‘someMethod’] 式
- Controller::someMethod 式
最后,让我们在map方法里面添加以下内容,这是定义路由的核心:
private function map(string $method, string $pattern, array $paramters, \Closure|array $callback, $name = null): self
{
if (is_string($method)) {
// 分割字符串并转换成大写数组以支持 'GET|POST|PUT' 这样的定义方式
$method = array_map('strtoupper', explode('|', $method));
}
$route = new Route(
path: $pattern,
defaults: ['_controller' => $callback],
requirements: $paramters,
host: '',
methods: $method
);
self::$routes
->add(
$name ?? $pattern,
$route
);
return $this;
}
参数详解:
- $method, $pattern, $paramters, $callback 同上
2.3 派发路由
下面,让我们来到路由运行的另一重要部分:派发
symfony/routing的match方法可以快速帮我们判断路径下是否匹配路由,并返回一个路由相关数组;且当不匹配(如请求方法错误,路径不匹配)时,就会扔出几个Exception。所以,我们只需要针对这些对路由进行处理,就能顺利派发路由,下面是实现代码:
public function run(): void
{
global $content;
$path = self::$Request->getPathInfo();
try {
// 尝试匹配请求路径,如果成功,返回一个包含路由属性的数组
$attributes = self::$matcher->match($path);
$args = [];
$params = self::$routes->get($attributes['_route'])->getRequirements();
// 遍历$params数组,根据参数名从匹配结果数组中获取对应的值,并存入$args数组中
foreach ($params as $key => $value) {
$args[] = $attributes[$key];
}
// 调用控制器并传递参数,获取响应内容
if ($attributes['_controller'] instanceof \Closure)
{
// 针对闭包的处理
$content = call_user_func_array($attributes,['_controller'], $args);
} else {
// 针对Controller的处理
list($controller, $method) = $attributes['_controller'];
$content = call_user_func_array([$controller, $method], array_merge([self::$Request], array_values($attributes)));
}
$response = new Response($content, 200);
} catch (Exception $e) {
if ($e instanceof ResourceNotFoundException)
{
$response = new Response('404 Not Found', 404);
}
elseif ($e instanceof MethodNotAllowedException)
{
$response = new Response('405 Method Not Allowed', 405);
}
}
$response->send();
}
针对catch部分的不同报错内容的显示,你可以使用一个ErrorHandler对不同种类的错误做出不同相应,这里仅为最小实现
在这里,我们需要引入几个类:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
2.4 测试
在你的index.php(或者其他地方),use这个类,并调用bind()方法:
<?php
use App\Router\SymfonyRouting;
$route = new SymfonyRouting();
$route->bind('get', '/hello/{name}', function ($name) {
echo 'Hello ' . $name . '!';
}, array('{name}' => '\w+'));
$route->run();
不出意外,当你访问 /hello/world 时,将会显示:’Hello World!’
三、总结
原理: 通过RouteCollection和Route增加路由,通过UrlMatcher匹配路由,使用Request和Response接收和发送请求内容
关于数组读取那一块,其实可以用Laravel的Arr::get方法来更优雅的实现,但是,毕竟是最小实现,那么这种优雅操作肯定是不需要的,毕竟能跑就行
其实吧,路由这一块说简单了就是匹配请求路径是否和路由数组里面的路径一致,如果一致,就去执行对应的callback。说难不难,但要是真的想做好来也确实不是一件容易的事。Symfony/routing在这方面的成功也是有目共睹的,不然Laravel这些大型框架也不会基于它来写框架的路由器了。
Respect to Symfony developers and all open source developers.
致敬每一位开发者。