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

一个简单的PHP路由实现

路由

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


  1. 对于静态的路由(没占位符的),正确调用callback
  2. 对于有占位符的路由,正确调用callback时传入占位符参数,譬如对于路由:**/user/{id}**,当请求为**/user/23**时,传入参数$args结构为  
  1. [
  1.     'id' => '23'
  1. ]

大致思路

  1. 我们需要把每个路由的信息管理起来:http方法($method),路由字符串($route),回调($callback),因此需要一个addRoute方法,另外提供短方法get,post(就是把$method写好)
  2. 对于**/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}),这里简单做了


### 分发流程

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

1. 一个个匹配静态路由

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


第一步很简单,主要说明第二步

对于三个独立的正则字符串(定界符是~):

~^/user/([^/]+)/(\d+)$~

~^/user/(\d+)$~

~^/user/([^/]+)$~

我们可以合起来,得到

~^(?:

    /user/([^/]+)/(\d+)

    | /user/(\d+)

    | /user/([^/]+)

)$~x

?:是非捕获型分组

这个转化很简单,我们怎么知道那个正则被匹配了呢??

举个例子:

preg_match($regex, '/user/nikic', $matches);

=> [

    "/user/nikic",   # 完全匹配

    "", "",          # 第一个(空)

    "",              # 第二个(空)

    "nikic",         # 第三个(被使用)

]

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

[

    1 => ['handler0', ['name', 'id']],

    3 => ['handler1', ['id']],

    4 => ['handler2', ['name']],

]

1是因为排除第一个匹配

3是因为第一个路由有两个占位符

4是因为第二个路由有一个占位符


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

$regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);

foreach ($regexes as $regex) {

    $routeLookup[$index] = [

        $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,

        $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,

    ];

    $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);

}


 最后

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

实现

Route.php类

<?php


namespace SalamanderRoute;


class Route {

    /** @var string */

    public $httpMethod;


    /** @var string */

    public $regex;


    /** @var array */

    public $variables;


    /** @var mixed */

    public $handler;


    /**

     * Constructs a route (value object).

     *

     * @param string $httpMethod

     * @param mixed  $handler

     * @param string $regex

     * @param array  $variables

     */

    public function __construct($httpMethod, $handler, $regex, $variables) {

        $this->httpMethod = $httpMethod;

        $this->handler = $handler;

        $this->regex = $regex;

        $this->variables = $variables;

    }


    /**

     * Tests whether this route matches the given string.

     *

     * @param string $str

     *

     * @return bool

     */

    public function matches($str) {

        $regex = '~^' . $this->regex . '$~';

        return (bool) preg_match($regex, $str);

    }


}


```


## Dispatcher.php

```

<?php

/**

 * User: salamander

 * Date: 2017/11/12

 * Time: 13:43

 */


namespace SalamanderRoute;


class Dispatcher {

    /** @var mixed[][] */

    protected $staticRoutes = [];


    /** @var Route[][] */

    private $methodToRegexToRoutesMap = [];


    const NOT_FOUND = 0;

    const FOUND = 1;

    const METHOD_NOT_ALLOWED = 2;


    /**

     * 提取占位符

     * @param $route

     * @return array

     */

    private function parse($route) {

        $regex = '~^(?:/[a-zA-Z0-9_]*|/\{([a-zA-Z0-9_]+?)\})+/?$~';

        if(preg_match($regex, $route, $matches)) {

            // 区分静态路由和动态路由

            if(count($matches) > 1) {

                preg_match_all('~\{([a-zA-Z0-9_]+?)\}~', $route, $matchesVariables);

                return [

                    preg_replace('~{[a-zA-Z0-9_]+?}~', '([a-zA-Z0-9_]+)', $route),

                    $matchesVariables[1],

                ];

            } else {

                return [

                    $route,

                    [],

                ];

            }

        }

        throw new \LogicException('register route failed, pattern is illegal');

    }


    /**

     * 注册路由

     * @param $httpMethod string | string[]

     * @param $route

     * @param $handler

     */

    public function addRoute($httpMethod, $route, $handler) {

        $routeData = $this->parse($route);

        foreach ((array) $httpMethod as $method) {

            if ($this->isStaticRoute($routeData)) {

                $this->addStaticRoute($httpMethod, $routeData, $handler);

            } else {

                $this->addVariableRoute($httpMethod, $routeData, $handler);

            }

        }

    }



    private function isStaticRoute($routeData) {

        return count($routeData[1]) === 0;

    }


    private function addStaticRoute($httpMethod, $routeData, $handler) {

        $routeStr = $routeData[0];


        if (isset($this->staticRoutes[$httpMethod][$routeStr])) {

            throw new \LogicException(sprintf(

                'Cannot register two routes matching "%s" for method "%s"',

                $routeStr, $httpMethod

            ));

        }


        if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {

            foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {

                if ($route->matches($routeStr)) {

                    throw new \LogicException(sprintf(

                        'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',

                        $routeStr, $route->regex, $httpMethod

                    ));

                }

            }

        }


        $this->staticRoutes[$httpMethod][$routeStr] = $handler;

    }



    private function addVariableRoute($httpMethod, $routeData, $handler) {

        list($regex, $variables) = $routeData;


        if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {

            throw new \LogicException(sprintf(

                'Cannot register two routes matching "%s" for method "%s"',

                $regex, $httpMethod

            ));

        }


        $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(

            $httpMethod, $handler, $regex, $variables

        );

    }



    public function get($route, $handler) {

        $this->addRoute('GET', $route, $handler);

    }


    public function post($route, $handler) {

        $this->addRoute('POST', $route, $handler);

    }


    public function put($route, $handler) {

        $this->addRoute('PUT', $route, $handler);

    }


    public function delete($route, $handler) {

        $this->addRoute('DELETE', $route, $handler);

    }


    public function patch($route, $handler) {

        $this->addRoute('PATCH', $route, $handler);

    }


    public function head($route, $handler) {

        $this->addRoute('HEAD', $route, $handler);

    }


    /**

     * 分发

     * @param $httpMethod

     * @param $uri

     */

    public function dispatch($httpMethod, $uri) {

        $staticRoutes = array_keys($this->staticRoutes[$httpMethod]);

        foreach ($staticRoutes as $staticRoute) {

            if($staticRoute === $uri) {

                return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];

            }

        }


        $routeLookup = [];

        $index = 1;

        $regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);

        foreach ($regexes as $regex) {

            $routeLookup[$index] = [

                $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,

                $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,

            ];

            $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);

        }

        $regexCombined = '~^(?:' . implode('|', $regexes) . ')$~';

        if(!preg_match($regexCombined, $uri, $matches)) {

            return [self::NOT_FOUND];

        }

        for ($i = 1; '' === $matches[$i]; ++$i);

        list($handler, $varNames) = $routeLookup[$i];

        $vars = [];

        foreach ($varNames as $varName) {

            $vars[$varName] = $matches[$i++];

        }

        return [self::FOUND, $handler, $vars];

    }

}


```


# 配置

## nginx.conf重写到index.php

```

location / {

        try_files $uri $uri/ /index.php$is_args$args;


        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000

        #

        location ~ \.php$ {

            fastcgi_pass   127.0.0.1:9000;

            fastcgi_index  index.php;

            fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

            include        fastcgi_params;

        }


    }

```


## composer.json自动载入

```

{

    "name": "salmander/route",

    "require": {},

    "autoload": {

      "psr-4": {

        "SalamanderRoute\\": "SalamanderRoute/"

      }

  }

}


```


# 最终使用

## index.php

```

文章导读: 一个简单的PHP路由实现

  • 发表于 2018-01-06 16:40
  • 阅读 ( 816 )
  • 分类:PHP7

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
不写代码的码农
salamander

php,go,lisp语言爱好者

1 篇文章

作家榜 »

  1. Kemin 44 文章
  2. golanglover 5 文章
  3. D.Chen 4 文章
  4. salamander 1 文章
  5. 深圳-伟 1 文章
  6. 广训 1 文章
  7. PHP小菜 1 文章
  8. Undefined 0 文章