资讯专栏INFORMATION COLUMN

Just for fun——PHP框架之简单的路由器(1)

smallStone / 2841人阅读

摘要:路由路由的功能就是分发请求到不同的控制器,基于的原理就是正则匹配。

路由

路由的功能就是分发请求到不同的控制器,基于的原理就是正则匹配。接下来呢,我们实现一个简单的路由器,实现的能力是

对于静态的路由(没占位符的),正确调用callback

对于有占位符的路由,正确调用callback时传入占位符参数,譬如对于路由:/user/{id},当请求为/user/23时,传入参数$args结构为

</>复制代码

  1. [
  2. "id" => "23"
  3. ]
大致思路

我们需要把每个路由的信息管理起来:http方法($method),路由字符串($route),回调($callback),因此需要一个addRoute方法,另外提供短方法get,post(就是把$method写好)

对于/user/{id}这样的有占位符的路由字符串,把占位符要提取出来,然后占位符部分变成正则字符串

代码讲解 路由分类

对于注册的路由,需要分成两类(下文提到的$uri是指$_SERVER["REQUEST_URI"]去掉查询字符串的值)

静态路由(就是没有占位符的路由,例如/articles)

带参数路由(有占位符的路由,例如/user/{uid})

其实这是很明显的,因为静态的路由的话,我们只需要和$uri直接比较相等与否就行了,而对于带参数路由,譬如/user/{uid},我们需要在注册的时候,提取占位符名,把{**}这一部分替换为([a-zA-Z0-9_]+)这样的正则字符串,使用()是因为要做分组捕获,把占位符对应的值要取出来。
Dispatcher.php中有两个数组

$staticRoutes

$methodToRegexToRoutesMap

分别对应静态路由和带参数路由,另外要注意,这两个数组是二维数组,第一维存储http method,第二维用来存储正则字符串(静态路由自身就是这个值,而带参数路由是把占位符替换后的值),最后的value是一个Route对象

Route类

这个类很好理解,用来存储注册路由的一些信息

$httpMethod:HTTP方法,有GET,POST,PUT,PATCH,HEAD

$regex:路由的正则表达式,带参数路由是占位符替换后的值,静态路由自身就是这个值

$variables:路由占位符参数集合,静态路由就是空数组

$handler:路由要回调的对象

当然,这个类还可以多存储一点信息,譬如带参数路由最原始的字符串(/user/{uid}),这里简单做了

分发流程

根据http method取数据,因为第一维都是http method

一个个匹配静态路由

对于带参数路由,把所有的正则表达式合起来,形成一个大的正则字符串,而不是一个个匹配(这样效率低)

第一步很简单,主要说明第二步
对于三个独立的正则字符串(定界符是~):

</>复制代码

  1. ~^/user/([^/]+)/(d+)$~
  2. ~^/user/(d+)$~
  3. ~^/user/([^/]+)$~

我们可以合起来,得到

</>复制代码

  1. ~^(?:
  2. /user/([^/]+)/(d+)
  3. | /user/(d+)
  4. | /user/([^/]+)
  5. )$~x

?:是非捕获型分组
这个转化很简单,我们怎么知道那个正则被匹配了呢??
举个例子:

</>复制代码

  1. preg_match($regex, "/user/nikic", $matches);
  2. => [
  3. "/user/nikic", # 完全匹配
  4. "", "", # 第一个(空)
  5. "", # 第二个(空)
  6. "nikic", # 第三个(被使用)
  7. ]

可以看到,第一个非空的位置就可以推断出哪个路由被匹配了(第一个完全匹配要剔除),我们需要一个数组要映射它

</>复制代码

  1. [
  2. 1 => ["handler0", ["name", "id"]],
  3. 3 => ["handler1", ["id"]],
  4. 4 => ["handler2", ["name"]],
  5. ]

1是因为排除第一个匹配
3是因为第一个路由有两个占位符
4是因为第二个路由有一个占位符

上面的数组,我们可以注册的methodToRegexToRoutesMap这个量形成的,我是这么写的

</>复制代码

  1. $regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);
  2. foreach ($regexes as $regex) {
  3. $routeLookup[$index] = [
  4. $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
  5. $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
  6. ];
  7. $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);
  8. }
最后

调用回调函数,返回一个数组,第一个值用来判断最终有没有找到

实现 Route.php类

</>复制代码

  1. httpMethod = $httpMethod;
  2. $this->handler = $handler;
  3. $this->regex = $regex;
  4. $this->variables = $variables;
  5. }
  6. /**
  7. * Tests whether this route matches the given string.
  8. *
  9. * @param string $str
  10. *
  11. * @return bool
  12. */
  13. public function matches($str) {
  14. $regex = "~^" . $this->regex . "$~";
  15. return (bool) preg_match($regex, $str);
  16. }
  17. }
Dispatcher.php

</>复制代码

  1. 1) {
  2. preg_match_all("~{([a-zA-Z0-9_]+?)}~", $route, $matchesVariables);
  3. return [
  4. preg_replace("~{[a-zA-Z0-9_]+?}~", "([a-zA-Z0-9_]+)", $route),
  5. $matchesVariables[1],
  6. ];
  7. } else {
  8. return [
  9. $route,
  10. [],
  11. ];
  12. }
  13. }
  14. throw new LogicException("register route failed, pattern is illegal");
  15. }
  16. /**
  17. * 注册路由
  18. * @param $httpMethod string | string[]
  19. * @param $route
  20. * @param $handler
  21. */
  22. public function addRoute($httpMethod, $route, $handler) {
  23. $routeData = $this->parse($route);
  24. foreach ((array) $httpMethod as $method) {
  25. if ($this->isStaticRoute($routeData)) {
  26. $this->addStaticRoute($httpMethod, $routeData, $handler);
  27. } else {
  28. $this->addVariableRoute($httpMethod, $routeData, $handler);
  29. }
  30. }
  31. }
  32. private function isStaticRoute($routeData) {
  33. return count($routeData[1]) === 0;
  34. }
  35. private function addStaticRoute($httpMethod, $routeData, $handler) {
  36. $routeStr = $routeData[0];
  37. if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
  38. throw new LogicException(sprintf(
  39. "Cannot register two routes matching "%s" for method "%s"",
  40. $routeStr, $httpMethod
  41. ));
  42. }
  43. if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
  44. foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
  45. if ($route->matches($routeStr)) {
  46. throw new LogicException(sprintf(
  47. "Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"",
  48. $routeStr, $route->regex, $httpMethod
  49. ));
  50. }
  51. }
  52. }
  53. $this->staticRoutes[$httpMethod][$routeStr] = $handler;
  54. }
  55. private function addVariableRoute($httpMethod, $routeData, $handler) {
  56. list($regex, $variables) = $routeData;
  57. if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
  58. throw new LogicException(sprintf(
  59. "Cannot register two routes matching "%s" for method "%s"",
  60. $regex, $httpMethod
  61. ));
  62. }
  63. $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
  64. $httpMethod, $handler, $regex, $variables
  65. );
  66. }
  67. public function get($route, $handler) {
  68. $this->addRoute("GET", $route, $handler);
  69. }
  70. public function post($route, $handler) {
  71. $this->addRoute("POST", $route, $handler);
  72. }
  73. public function put($route, $handler) {
  74. $this->addRoute("PUT", $route, $handler);
  75. }
  76. public function delete($route, $handler) {
  77. $this->addRoute("DELETE", $route, $handler);
  78. }
  79. public function patch($route, $handler) {
  80. $this->addRoute("PATCH", $route, $handler);
  81. }
  82. public function head($route, $handler) {
  83. $this->addRoute("HEAD", $route, $handler);
  84. }
  85. /**
  86. * 分发
  87. * @param $httpMethod
  88. * @param $uri
  89. */
  90. public function dispatch($httpMethod, $uri) {
  91. $staticRoutes = array_keys($this->staticRoutes[$httpMethod]);
  92. foreach ($staticRoutes as $staticRoute) {
  93. if($staticRoute === $uri) {
  94. return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];
  95. }
  96. }
  97. $routeLookup = [];
  98. $index = 1;
  99. $regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);
  100. foreach ($regexes as $regex) {
  101. $routeLookup[$index] = [
  102. $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
  103. $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
  104. ];
  105. $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);
  106. }
  107. $regexCombined = "~^(?:" . implode("|", $regexes) . ")$~";
  108. if(!preg_match($regexCombined, $uri, $matches)) {
  109. return [self::NOT_FOUND];
  110. }
  111. for ($i = 1; "" === $matches[$i]; ++$i);
  112. list($handler, $varNames) = $routeLookup[$i];
  113. $vars = [];
  114. foreach ($varNames as $varName) {
  115. $vars[$varName] = $matches[$i++];
  116. }
  117. return [self::FOUND, $handler, $vars];
  118. }
  119. }
配置 nginx.conf重写到index.php

</>复制代码

  1. location / {
  2. try_files $uri $uri/ /index.php$is_args$args;
  3. # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  4. #
  5. location ~ .php$ {
  6. fastcgi_pass 127.0.0.1:9000;
  7. fastcgi_index index.php;
  8. fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  9. include fastcgi_params;
  10. }
  11. }
composer.json自动载入

</>复制代码

  1. {
  2. "name": "salmander/route",
  3. "require": {},
  4. "autoload": {
  5. "psr-4": {
  6. "SalamanderRoute": "SalamanderRoute/"
  7. }
  8. }
  9. }
最终使用 index.php

</>复制代码

  1. get("/", function () {
  2. echo "hello world";
  3. });
  4. $dispatcher->get("/user/{id}", function ($args) {
  5. echo "user {$args["id"]} visit";
  6. });
  7. // Fetch method and URI from somewhere
  8. $httpMethod = $_SERVER["REQUEST_METHOD"];
  9. $uri = $_SERVER["REQUEST_URI"];
  10. // 去掉查询字符串
  11. if (false !== $pos = strpos($uri, "?")) {
  12. $uri = substr($uri, 0, $pos);
  13. }
  14. $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
  15. switch ($routeInfo[0]) {
  16. case Dispatcher::NOT_FOUND:
  17. echo "404 not found";
  18. break;
  19. case Dispatcher::FOUND:
  20. $handler = $routeInfo[1];
  21. $vars = $routeInfo[2];
  22. $handler($vars);
  23. break;
  24. }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/26079.html

相关文章

  • Just for fun——PHP框架简单由器(2)

    改进 紧接上一篇文章Just for fun——PHP框架之简单的路由器(1)。代码下载 效率不高原因 对于以下合并的正则 ~^(?: /user/([^/]+)/(d+) | /user/(d+) | /user/([^/]+) )$~x 最终匹配的是分组中的某一个,我们需要的子匹配也是那个分组中的,然而从结果看 preg_match($regex, /user/niki...

    tomato 评论0 收藏0
  • Just for fun——基于Swoole做个小框架

    摘要:使开发人员可以编写高性能的异步并发,服务。使用作为网络通信框架,可以使企业研发团队的效率大大提升,更加专注于开发创新产品。总之,这个库让可以常驻内存,并提供了,等功能。 swoole 使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(...

    CoreDump 评论0 收藏0
  • Just for fun——基于Swoole做个小框架

    摘要:使开发人员可以编写高性能的异步并发,服务。使用作为网络通信框架,可以使企业研发团队的效率大大提升,更加专注于开发创新产品。总之,这个库让可以常驻内存,并提供了,等功能。 swoole 使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(...

    fevin 评论0 收藏0
  • Just for fun——PHP框架简单模板引擎

    摘要:原理使用模板引擎的好处是数据和视图分离。对于循环语句怎么办呢这个的话,请看流程控制的替代语法 原理 使用模板引擎的好处是数据和视图分离。一个简单的PHP模板引擎原理是 extract数组($data),使key对应的变量可以在此作用域起效 打开输出控制缓冲(ob_start) include模板文件,include遇到html的内容会输出,但是因为打开了缓冲,内容输出到了缓冲中 ob...

    X1nFLY 评论0 收藏0
  • Just for fun——Slim借力Swoole

    摘要:的话,是一个遵循规范微型的框架,作者这样说大致意思的核心工作分发了请求,然后调用回调函数,返回一个对象。执行的方法时,我们从中取出的依赖,这时候,注册的回调函数被调用,返回实例。 Slim Slim的话,是一个遵循PSR (PSR-7)规范微型的框架,作者这样说: Slim is a PHP micro framework that helps you quickly write si...

    leejan97 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<