├── .gitignore ├── src ├── Exception │ ├── ExceptionInterface.php │ ├── ResourceNotFoundException.php │ └── MethodNotAllowedException.php └── Router.php ├── README.md ├── composer.json └── tests └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor/ 3 | .idea 4 | composer.lock 5 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | allowedMethods = array_map('strtoupper', $allowedMethods); 15 | 16 | parent::__construct($message, $code, $previous); 17 | } 18 | 19 | public function getAllowedMethods() 20 | { 21 | return $this->allowedMethods; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Routing](http://pfinal.cn) 2 | 3 | ## 安装 4 | 5 | 环境要求:PHP >= 5.4、7+ 6 | 7 | * 使用 [composer](https://getcomposer.org/) 8 | 9 | ```shell 10 | composer require pfinal/routing 11 | ``` 12 | 13 | 使用示例 14 | 15 | ```php 16 | require __DIR__ . '/vendor/autoload.php'; 17 | 18 | use Symfony\Component\HttpFoundation\Request; 19 | use PFinal\Routing\Router; 20 | 21 | $router = new Router(); 22 | 23 | $router->get('/', function () { 24 | return 'index'; 25 | }); 26 | 27 | $router->any('/blog/:id', function ($id) { 28 | return $id; 29 | }); 30 | 31 | $router->post('/blog/:name/update', function ($name) { 32 | return $name; 33 | }); 34 | 35 | $request = Request::createFromGlobals(); 36 | 37 | $response = $router->dispatch($request); 38 | $response->send(); 39 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pfinal/routing", 3 | "description": "The PFinal Routing package.", 4 | "license": "MIT", 5 | "homepage": "http://www.pfinal.cn", 6 | "authors": [ 7 | { 8 | "name": "Zou Yiliang" 9 | } 10 | ], 11 | "require": { 12 | "php": "^5.4 || ^7.0", 13 | "pfinal/pipeline": "~1.1", 14 | "pfinal/container": "~1.1", 15 | "psr/http-message": "^1.0", 16 | "symfony/http-foundation": "~2.3 || ~3.0", 17 | "symfony/psr-http-message-bridge": "^1.0", 18 | "zendframework/zend-diactoros": "^1.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "PFinal\\Routing\\": "src/" 23 | } 24 | }, 25 | "require-dev": { 26 | "symfony/var-dumper": "^4.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/test.php: -------------------------------------------------------------------------------- 1 | any('/blog/test/:id/:w', $obj); 12 | //$router->any('/blog/test/:id/:w', function ($id = 1, $w = 22) { 13 | // 14 | // var_dump($id); 15 | // var_dump($w); 16 | //}); 17 | // 18 | //$router->any('/', function () { 19 | // echo 'index'; 20 | //}); 21 | // 22 | //$router->any('/blog/:id', function ($id) { 23 | // echo $id; 24 | //}); 25 | // 26 | //$router->get('/blog/:name/update', function ($name) { 27 | // echo 'name'; 28 | // echo $name; 29 | //}); 30 | // 31 | //$router->get('/blog', 'BlogController@index'); 32 | //$router->post('/blog', 'BlogController@create'); 33 | // 34 | // 35 | //class User 36 | //{ 37 | //} 38 | // 39 | //class BlogController 40 | //{ 41 | // public function __construct(User $user) 42 | // { 43 | // $this->user = $user; 44 | // } 45 | // 46 | // public function __invoke($id = 99, $w = 100) 47 | // { 48 | // var_dump($id); 49 | // var_dump($w); 50 | // } 51 | // 52 | // public function index() 53 | // { 54 | // var_dump($this); 55 | // return 'blog-index'; 56 | // } 57 | // 58 | // public function create() 59 | // { 60 | // return 'blog-create'; 61 | // } 62 | //} 63 | // 64 | // 65 | ////var_dump($router); 66 | ////var_dump(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); 67 | // 68 | ////$request = \Symfony\Component\HttpFoundation\Request::create('blog/11/update'); 69 | //$request = \Symfony\Component\HttpFoundation\Request::create('blog', 'post'); 70 | ////$request = \Symfony\Component\HttpFoundation\Request::create('blog', 'get'); 71 | ////$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals(); 72 | // 73 | //$response = $router->dispatch($request); 74 | //$response->send(); 75 | // 76 | //dump($router); -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | 'auth'), 45 | //array('middleware' => array('cors','csrf')), 46 | ); 47 | 48 | /** 49 | * @var string the GET variable name for route. For example, 'r' 50 | */ 51 | public $routeVar = null; 52 | 53 | /** 54 | * Router constructor. 55 | * @param \PFinal\Container\Container $container 56 | */ 57 | public function __construct($container = null) 58 | { 59 | $this->container = $container; 60 | } 61 | 62 | /** 63 | * 添加路由信息 64 | * @param string|array $method 请求方式 65 | * @param $path 66 | * @param $callback 67 | * @param array $middleware 68 | * @return $this 69 | */ 70 | public function add($method, $path, $callback, $middleware = array(), array $clearMiddleware = array()) 71 | { 72 | $groupStack = array(); 73 | foreach ($this->groupStack as $attribute) { 74 | if (array_key_exists('middleware', $attribute)) { 75 | $groupStack = array_merge($groupStack, (array)$attribute['middleware']); 76 | } 77 | } 78 | 79 | $middleware = array_merge($groupStack, (array)$middleware); // groupStack 优先 80 | $middleware = array_unique($middleware); 81 | 82 | if (count($clearMiddleware) > 0) { 83 | $middleware = array_diff($middleware, $clearMiddleware); 84 | } 85 | 86 | $tokens = explode(self::SEPARATOR, str_replace('.', self::SEPARATOR, trim($path, self::SEPARATOR))); 87 | $this->_add($this->tree, $tokens, $callback, $middleware, array_map('strtoupper', (array)$method), $path); 88 | return $this; 89 | } 90 | 91 | // 创建基于URL规则的树, `handler`保存到`#`节点 92 | protected function _add(&$node, $tokens, $callback, $middleware, $method, $uri) 93 | { 94 | if (!array_key_exists(self::PARAMETER, $node)) { 95 | $node[self::PARAMETER] = array(); 96 | } 97 | 98 | $token = array_shift($tokens); 99 | 100 | if (strncmp(self::PARAMETER, $token, 1) === 0) { 101 | $node = &$node[self::PARAMETER]; 102 | $token = substr($token, 1); 103 | } 104 | 105 | if ($token === null) { 106 | 107 | $handler = array( 108 | 'callback' => $callback, 109 | 'middleware' => (array)($middleware), 110 | 'method' => $method, 111 | 'uri' => $uri 112 | ); 113 | 114 | foreach ($method as $m) { 115 | $node[self::HANDLER][$m] = $handler; 116 | } 117 | 118 | return; 119 | } 120 | 121 | if (!array_key_exists($token, $node)) { 122 | $node[$token] = array(); 123 | } 124 | 125 | $this->_add($node[$token], $tokens, $callback, $middleware, $method, $uri); 126 | } 127 | 128 | // 根据path查找handler 129 | protected function _resolve($node, $tokens, $method, $params = array()) 130 | { 131 | $token = array_shift($tokens); 132 | 133 | if ($token === null && array_key_exists(self::HANDLER, $node)) { 134 | return $this->findHandler($method, $node[self::HANDLER], $params); 135 | } 136 | 137 | if (array_key_exists($token, $node)) { 138 | return $this->_resolve($node[$token], $tokens, $method, $params); 139 | } 140 | 141 | foreach ($node[self::PARAMETER] as $childToken => $childNode) { 142 | 143 | if ($token === null && array_key_exists(self::HANDLER, $childNode)) { 144 | return $this->findHandler($method, $childNode[self::HANDLER], $params); 145 | } 146 | 147 | $handler = $this->_resolve($childNode, $tokens, $method, array_merge($params, array($childToken => $token))); 148 | 149 | if ($handler !== false) { 150 | return $handler; 151 | } 152 | } 153 | return false; 154 | } 155 | 156 | private function findHandler($method, $handler, $params) 157 | { 158 | if (array_key_exists($method, $handler)) { 159 | return array_merge($handler[$method], array('arguments' => $params)); 160 | } 161 | 162 | if (array_key_exists('ANY', $handler)) { 163 | return array_merge($handler['ANY'], array('arguments' => $params)); 164 | } 165 | 166 | $allowedMethods = array_keys($handler); 167 | 168 | throw new MethodNotAllowedException($allowedMethods, sprintf('Method not allowed: %s', $method)); 169 | } 170 | 171 | protected function resolve($path, $method) 172 | { 173 | $tokens = explode(self::SEPARATOR, str_replace('.', self::SEPARATOR, trim($path, self::SEPARATOR))); 174 | return $this->_resolve($this->tree, $tokens, $method); 175 | } 176 | 177 | /** 178 | * 将请求转换为响应 179 | * 180 | * 如果PSR-7 Request,可以使用下面的方法,转化为Symfony Request 181 | * composer require symfony/psr-http-message-bridge 182 | * composer require zendframework/zend-diactoros 183 | * 184 | * $httpFoundationFactory = new \Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory(); 185 | * $request = $httpFoundationFactory->createRequest($psrRequest); 186 | * 187 | * 返回 symfony response对象,如果需要转化为 PSR-7 Response,可以使用下面的方法 188 | * $psr7Factory = new \Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory(); 189 | * $psrResponse = $psr7Factory->createResponse($symfonyResponse); 190 | * 191 | * http://symfony.com/blog/psr-7-support-in-symfony-is-here 192 | * 193 | * @param Request | \Psr\Http\Message\ServerRequestInterface $request 194 | * @return Response | \Psr\Http\Message\ResponseInterface 195 | */ 196 | public function dispatch($request) 197 | { 198 | $psr7 = $request instanceof \Psr\Http\Message\ServerRequestInterface; 199 | if ($psr7) { 200 | $httpFoundationFactory = new \Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory(); 201 | $request = $httpFoundationFactory->createRequest($request); 202 | } 203 | 204 | if ($this->routeVar === null) { 205 | $pathInfo = $request->getPathInfo(); 206 | } else { 207 | $pathInfo = (string)$request->get($this->routeVar, '/'); 208 | } 209 | 210 | $handler = $this->resolve($pathInfo, strtoupper($request->getMethod())); 211 | if ($handler === false) { 212 | throw new ResourceNotFoundException('Resource not found'); 213 | } 214 | 215 | /** @var $callback */ 216 | /** @var $middleware */ 217 | /** @var $method */ 218 | /** @var $arguments */ 219 | /** @var $uri */ 220 | extract($handler); 221 | 222 | if (method_exists($request, 'setRouteResolver')) { 223 | $request->setRouteResolver(function () use ($handler) { 224 | return $handler; 225 | }); 226 | } 227 | 228 | if (is_string($callback)) { 229 | list($class, $func) = explode(self::AT, $callback, 2); 230 | $callback = array($this->container->make($class), $func); 231 | } 232 | 233 | $pipeline = new Pipeline($this->container); 234 | 235 | $response = $pipeline->send($request)->through($middleware)->then(function (Request $request) use ($callback, $arguments) { 236 | 237 | $response = call_user_func_array($callback, $this->getArguments($callback, $arguments)); //php >= 5.4 238 | 239 | if ($response instanceof Response) { 240 | return $response; 241 | } 242 | 243 | //convert a psr response to symfony response 244 | if ($response instanceof \Psr\Http\Message\ResponseInterface) { 245 | 246 | //composer require symfony/psr-http-message-bridge 247 | //composer require zendframework/zend-diactoros 248 | 249 | $httpFoundationFactory = new \Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory(); 250 | return $httpFoundationFactory->createResponse($response); 251 | } 252 | 253 | if (is_array($response)) { 254 | return new JsonResponse($response); 255 | } 256 | 257 | return new Response($response); 258 | }); 259 | 260 | if (!($response instanceof Response)) { 261 | $response = new Response($response); 262 | } 263 | 264 | if ($psr7) { 265 | //composer require zendframework/zend-diactoros 266 | $psr7Factory = new \Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory(); 267 | $response = $psr7Factory->createResponse($response); 268 | } 269 | 270 | return $response; 271 | } 272 | 273 | /** 274 | * 获取调用参数和值 275 | * 276 | * @param $controller 277 | * @param $attributes 278 | * @return array 279 | * @throws \ReflectionException 280 | */ 281 | protected function getArguments($controller, $attributes) 282 | { 283 | if (is_array($controller)) { 284 | $ref = new \ReflectionMethod($controller[0], $controller[1]); 285 | } elseif (is_object($controller) && !$controller instanceof \Closure) { 286 | $ref = new \ReflectionObject($controller); 287 | $ref = $ref->getMethod('__invoke'); 288 | } else { 289 | $ref = new \ReflectionFunction($controller); 290 | } 291 | 292 | $parameters = $ref->getParameters(); 293 | $arguments = array(); 294 | 295 | foreach ($parameters as $param) { 296 | 297 | if (PHP_MAJOR_VERSION > 7) { 298 | $class = $param->getType(); 299 | } else { 300 | $class = $param->getClass(); 301 | } 302 | 303 | if (array_key_exists($param->name, $attributes)) { 304 | $arguments[] = $attributes[$param->name]; 305 | } elseif ($class && $this->container->offsetExists($class->getName())) { 306 | $arguments[] = $this->container[$class->getName()]; 307 | } elseif ($param->isDefaultValueAvailable()) { 308 | $arguments[] = $param->getDefaultValue(); 309 | } else { 310 | return $arguments; //参数不足 311 | } 312 | } 313 | return $arguments; 314 | } 315 | 316 | /** 317 | * 路由组 318 | * 319 | * $router->group(['middleware' => ['auth', 'cors']], function () use ($router) { 320 | * $router->get('/users', function () { 321 | * return 'users'; 322 | * }); 323 | * }); 324 | * 325 | * @param array $attributes 326 | * @param \Closure $callback 327 | */ 328 | public function group(array $attributes, \Closure $callback) 329 | { 330 | $this->groupStack[] = $attributes; 331 | $callback(); 332 | array_pop($this->groupStack); 333 | } 334 | 335 | public function getNodeData() 336 | { 337 | return $this->tree; 338 | } 339 | 340 | public function setNodeData(array $tree) 341 | { 342 | $this->tree = $tree; 343 | } 344 | 345 | /** 346 | * @param $name 347 | * @param $args 348 | * @return mixed 349 | * @throws \Exception 350 | */ 351 | public function __call($name, $args) 352 | { 353 | if (in_array($name, array('get', 'post', 'put', 'patch', 'delete', 'trace', 'connect', 'options', 'head', 'any'))) { 354 | array_unshift($args, $name); 355 | return call_user_func_array(array($this, 'add'), $args); 356 | } 357 | throw new \Exception('Call to undefined method ' . __CLASS__ . '::' . $name . '()'); 358 | } 359 | } 360 | --------------------------------------------------------------------------------