Just for fun——PHP框架之簡單的路由器(1)

路由

路由的功能就是分發請求到不一樣的控制器,基於的原理就是正則匹配。接下來呢,咱們實現一個簡單的路由器,實現的能力是php

  1. 對於靜態的路由(沒佔位符的),正確調用callback
  2. 對於有佔位符的路由,正確調用callback時傳入佔位符參數,譬如對於路由:/user/{id},當請求爲/user/23時,傳入參數$args結構爲
[
    'id' => '23'
]

大體思路

  1. 咱們須要把每一個路由的信息管理起來:http方法($method),路由字符串($route),回調($callback),所以須要一個addRoute方法,另外提供短方法get,post(就是把$method寫好)
  2. 對於/user/{id}這樣的有佔位符的路由字符串,把佔位符要提取出來,而後佔位符部分變成正則字符串

代碼講解

路由分類

對於註冊的路由,須要分紅兩類(下文提到的$uri是指$_SERVER['REQUEST_URI']去掉查詢字符串的值)nginx

  • 靜態路由(就是沒有佔位符的路由,例如/articles)
  • 帶參數路由(有佔位符的路由,例如/user/{uid})

其實這是很明顯的,由於靜態的路由的話,咱們只須要和$uri直接比較相等與否就好了,而對於帶參數路由,譬如/user/{uid},咱們須要在註冊的時候,提取佔位符名,把{**}這一部分替換爲([a-zA-Z0-9_]+)這樣的正則字符串,使用()是由於要作分組捕獲,把佔位符對應的值要取出來。
Dispatcher.php中有兩個數組正則表達式

  • $staticRoutes
  • $methodToRegexToRoutesMap

分別對應靜態路由和帶參數路由,另外要注意,這兩個數組是二維數組,第一維存儲http method,第二維用來存儲正則字符串(靜態路由自身就是這個值,而帶參數路由是把佔位符替換後的值),最後的value是一個Route對象json

Route類

這個類很好理解,用來存儲註冊路由的一些信息數組

  • $httpMethod:HTTP方法,有GET,POST,PUT,PATCH,HEAD
  • $regex:路由的正則表達式,帶參數路由是佔位符替換後的值,靜態路由自身就是這個值
  • $variables:路由佔位符參數集合,靜態路由就是空數組
  • $handler:路由要回調的對象

固然,這個類還能夠多存儲一點信息,譬如帶參數路由最原始的字符串(/user/{uid}),這裏簡單作了composer

分發流程

  1. 根據http method取數據,由於第一維都是http method
  2. 一個個匹配靜態路由
  3. 對於帶參數路由,把全部的正則表達式合起來,造成一個大的正則字符串,而不是一個個匹配(這樣效率低)

第一步很簡單,主要說明第二步
對於三個獨立的正則字符串(定界符是~):函數

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

咱們能夠合起來,獲得post

~^(?:
    /user/([^/]+)/(\d+)
    | /user/(\d+)
    | /user/([^/]+)
)$~x

?:是非捕獲型分組
這個轉化很簡單,咱們怎麼知道那個正則被匹配了呢??
舉個例子:ui

preg_match($regex, '/user/nikic', $matches);
=> [
    "/user/nikic",   # 徹底匹配
    "", "",          # 第一個(空)
    "",              # 第二個(空)
    "nikic",         # 第三個(被使用)
]

能夠看到,第一個非空的位置就能夠推斷出哪一個路由被匹配了(第一個徹底匹配要剔除),咱們須要一個數組要映射它this

[
    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

include_once 'vendor/autoload.php';

use SalamanderRoute\Dispatcher;

$dispatcher = new Dispatcher();

$dispatcher->get('/', function () {
    echo 'hello world';
});

$dispatcher->get('/user/{id}', function ($args) {
    echo "user {$args['id']} visit";
});

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// 去掉查詢字符串
if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case Dispatcher::NOT_FOUND:
        echo '404 not found';
        break;
    case Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        $handler($vars);
        break;
}

clipboard.png

clipboard.png

相關文章
相關標籤/搜索