简单 前缀树路由实现

最简易版

最开始只是使用composer 搭建了一个基础的项目, 用来进行一些简单的接口处理, 开始的路由定义如下:

在项目的根目录下定义了一个routes.php 文件

return [
 // import template
 'import' => [
 'method' => 'get',
 'dispatch' => [\App\Controllers\ImportController::class, 'index']
 ],// import data web api
 'import_data' => [
 'method' => 'post',
 'dispatch' => [\App\Controllers\ImportController::class, 'importData']
 ],'check_inc_pk' => [
 'method' => 'post',
 'dispatch' => [\App\Controllers\ImportController::class, 'checkIncPk']
 ],// testing
 'test' => [\App\Controllers\TestController::class, 'test'],
];

这返回一个path与方法的映射关联数组map,然后在入口文件进行判断

function send()
{
    $method = $_SERVER['REQUEST_METHOD'];$routes = require_once BASE_PATH . '/routes.php';$action = trim(input('action', ''));if (! array_key_exists($action, $routes)) {
    header("HTTP/1.0 404 Not Found");
    echo '404 Not Found';
    exit();
    }$route = $routes[$action];if (isset($route['method'], $route['dispatch'])) {
    if ($method !== strtoupper($route['method'])) {
    header("HTTP/1.0 404 Not Found");
    echo '404 Not Found';
    exit();
 }$service = new $route['dispatch'][0];
 $callback = [$service, $route['dispatch'][1]];} else {
 $service = new $routes[$action][0];
 $callback = [$service, $routes[$action][1]];
 }$args = [];
 $result = call_user_func_array($callback, $args);echo $result;
 exit();
}

这种实现方式简单是简单了, 但是只能完成静态的路由绑定, 无法完成动态路由解析的功能, 什么是动态路由呢?

例如: /user/{id} 这种 或者 /user/:id , 也就是后面的path参数是可变的,并能完成路由的解析, 我的目标是可以完成类似如下的功能:

# 可以解析动态路由 /user/:id# 可以在路由定义简单的 闭包函数 完成具体的实现# 可以在 闭包或者类方法中接受到 动态路由的参数
动态路由实现

通过了解, 有一种叫做前缀树的实现方式,如下:

首先我们需要设计一个树的节点

class TrieNode
{
 public $children; // 子节点
 public $isEndOfRoute; 
 public $handler; // 节点对应绑定的实现方法
 public $paramName; // 参数名称public function __construct() {
 $this->children = [];
 $this->isEndOfRoute = false;
 $this->handler = [];
 $this->paramName = null;
 }
}

然后来定义一个树 , 应该包含添加与查找的功能

class Trie
{
 private $root;public function __construct() {
     $this->root = new TrieNode();
 }/**
 * 这个方法用来单独添加绑定一个路由
 * @param $method
 * @param $path
 * @param $handler
 * @return void
 */
 public function insert($method, $path, $handler) {
     $node = $this->root;

     // 拆分 path
     $parts = explode('/', trim($path, '/'));foreach ($parts as $part) {// $part[0] Get the first character of a string
     // 这里获取字符串第一个字符 判断是否为 :
     // 是否为动态绑定
     if ($part !== '' && $part[0] === ':') {
     // Path parameter name   :id  param = id
     // 使用 substr 去除字符串前面的 : 得到需要的path参数的名称
     $paramName = substr($part, 1);

     // 使用:param 来进行判断是否存在动态的绑定 不存在则进行设置
     if (!isset($node->children[':param'])) {
         $node->children[':param'] = new TrieNode();
         $node->children[':param']->paramName = $paramName;
     }
     $node = $node->children[':param'];} else {
     // 普通的路由绑定
     if (!isset($node->children[$part])) {
        $node->children[$part] = new TrieNode();
     }
         $node = $node->children[$part];
     }
 }

     // 设置路由的方法
     $node->isEndOfRoute = true;
     // 根据不同的请求类型进行绑定
     $node->handler[$method] = $handler;
 }/**
 * 在树中检索 返回node的handle 与 param
 * @param $method
 * @param $path
 * @return array
 */
 public function search($method, $path) {
     $node = $this->root;
     $parts = explode('/', trim($path, '/'));
     $params = [];foreach ($parts as $part) {
     if (isset($node->children[$part])) {
        $node = $node->children[$part];
     } elseif (isset($node->children[':param'])) {
         $node = $node->children[':param'];
         $params[$node->paramName] = $part;
     } else {
        return [null, array()];
     }
 }if ($node->isEndOfRoute && isset($node->handler[$method])) {
        return [$node->handler[$method], $params];
     }return [null, array()];
 }
}

最后定一个Router类来调用 这个树的方法

class Router
{
     private $trie;public function __construct() {
     $this->trie = new Trie();
 }public function addRoute($method, $path, $handler) {
     $this->trie->insert(strtoupper($method), $path, $handler);
 }public function getRouteHandler($method, $path) {
     list($handler, $params) = $this->trie->search(strtoupper($method), $path);
     return array($handler, $params);
 }public function getTrie()
 {
     return $this->trie;
 }/**
 * @param $method
 * @param $path
 * @return mixed|string
 * @throws RouteNotFoundException
 */
 public function dispatch($method, $path, $args = []) {list($handler, $params) = $this->getRouteHandler($method, $path);if ($handler) {
     // 这里主要想实现 通过数组定义的方式
     // 有点类似laravel 例如
     // [\App\Controllers\TestController::class, 'test'] 这样绑定handle
     if (is_array($handler) && is_callable($handler)) {
     $call = new $handler[0];
     $action = $handler[1];
     $callback = [$call, $action];return call_user_func_array($callback, array_merge($params, $args));
     }if (is_callable($handler)) {
     // 因为我还需要为每个方法 默认绑定一个 Symfony的Request对象

     // call_user_func 依次传递对应的参数
     // function ($param) => $param is array// call_user_func_array 可以按数组传递对应的参数
     // function ($param, $args1, $args2) => $param is valuereturn call_user_func_array($handler, array_merge($params, $args));
     }
     }throw new RouteNotFoundException('404 Not Found', 404);
 }
}

到此就可以进行路由的定义了, 修改根目录的routes.php 文件

use App\Helpers\Router\Router;$router = new Router();$router->addRoute('GET','/test', [\App\Controllers\TestController::class, 'test']);$router->addRoute('GET', '/user/:id', function($id, $request) {return "User ID: " . $id;
});$router->addRoute('GET', '/user', function() {
 return "Get User";
});return $router;

如何使用, 在入口文件index.php 中 测试一下

const BASE_PATH = __DIR__;require BASE_PATH . '/vendor/autoload.php';$router = require_once BASE_PATH . '/routes.php';$request = \request(); // 返回 Symfony的Request对象
$path = $request->getPathInfo();$args['request'] = $request;
$response = $router->dispatch(strtoupper($method), $path, $args);if ($response instanceof \Symfony\Component\HttpFoundation\Response) {
 $response->send();
}echo $response;
exit();

自己简单测试了一下,可以基本满足之前需要实现的功能。

完整代码

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!