[翻译]为MVC框架构建路由
Posted Fogwind
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[翻译]为MVC框架构建路由相关的知识,希望对你有一定的参考价值。
[翻译]为MVC框架构建路由
如果你还不清楚什么是MVC,建议你在读这篇文章之前先了解一下什么是MVC。
这篇文章里的代码要求的php版本最低是7.1。
假设你的域名是example.com
,你想让你的应用通过example.com/gallery/cats
请求给用户展示一个凯蒂猫的图片库,并且通过example.com/gallery/dogs
请求展示狗的图片库。
通过分析请求URL,我们注意到单词gallery
没有变化,只有cats
,dogs
变了。因此,我们将创建一个类来处理这些逻辑。
// GalleryController.php
class GalleryController
{
/** @var string */
private $animal_type;
public function __construct(string $animal_type)
{
$this->animal_type = $animal_type;
}
public function display()
{
// do whatever you need here, fetch from database, etc.
echo $this->animal_type;
}
}
所以当用户用浏览器访问example.com/gallery/cats
时,应用需要用cats
参数实例化类GalleryController
,然后调用display
方法。
我们将使用正则表达式实现这个路由。
首先我们创建一个类把请求和对应的控制器和方法关联起来。
// Route.php
class Route
{
/** @var string */
public $path;
/** @var string */
public $controller;
/** @var string */
public $method;
public function __construct(string $path, string $controller, string $method)
{
$this->path = $path;
$this->controller = $controller;
$this->method = $method;
}
}
我们创建一个简单的路由类,并通过它看看路由是如何工作的。
// Router.php
class Router
{
/** @var Route[] */
private $routes;
public function register(Route $route)
{
$this->routes[] = $route;
}
public function handleRequest(string $request)
{
$matches = [];
foreach ($this->routes as $route) {
//
if (preg_match($route->path, $request, $matches)) {
// $matches[0] will always be equal to $request, so we just shift it off
array_shift($matches);
// here comes the magic
// $route->controller是控制器的名字
// 根据控制器名字创建一个控制器类的反射类(类的映射)
$class = new ReflectionClass($route->controller);
// 通过反射类获取控制器类中对应的方法(的定义),例子中是display方法
$method = $class->getMethod($route->method);
// we instantiate a new class using the elements of the $matches array
// 通过反射类实例化控制器类,例子中参数是cats
$instance = $class->newInstance(...$matches);
// equivalent:
// $instance = $class->newInstanceArgs($matches);
// then we call the method on the newly instantiated object
// 通过控制器类的实例对象调用前面获取到的方法,例子中是调用display方法
$method->invoke($instance);
// finally, we return from the function, because we do not want the request to be handled more than once
return;
}
}
throw new RuntimeException("The request \'$request\' did not match any route.");
}
}
现在,运行应用并测试路由类。将下面代码保存为index.php
文件,配置你的web服务器将所有请求都指向这个文件。
// index.php
spl_autoload_extensions(\'.php\');
spl_autoload_register();
$router = new Router();
$router->register(new Route(\'/^\\/gallery\\/(\\w+)$/\', \'GalleryController\', \'display\'));
$router->handleRequest($_SERVER[\'REQUEST_URI\']);
通过浏览器访问example.com/gallery/cats
,将会在屏幕上显示cats
。
工作原理很简单:
- 使用router注册route,并告诉router处理用户的请求;
- router检查所有注册过的route,并与请求进行匹配;
- 如果找到匹配的route,那么使用指定的参数实例化对应的控制器类,并在实例对象上调用指定的方法。
上面的例子很简单,下面我们走的更远一点。如果控制器的构造函数接受一个对象为参数而不是简单的字符串呢?
我们将定义一个User
类并暴露用户的名字和年龄:
// User.php
class User
{
/** @var string */
public $name;
/** @var int */
public $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
}
UserController
类以User
为参数:
// UserController.php
class UserController
{
/** @var User */
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function show()
{
echo "{$this->user->name} is {$this->user->age} years old.";
}
}
在index.php
中注册一个新的route,在handleRequest
函数调用之前添加:
$router->register(new Route(\'/^\\/users\\/(\\w+)\\/(\\d+)$/\', \'UserController\', \'show\'));
现在,如果你访问example.com/users/mike/26
,将会得到一个异常(Exception),因为Router
试图给UserController
的构造函数传递一个字符串参数,解决方法如下(充分利用反射类):
// Router.php
class Router
{
/** @var Route[] */
private $routes;
public function register(Route $route)
{
$this->routes[] = $route;
}
public function handleRequest(string $request)
{
$matches = [];
foreach ($this->routes as $route) {
if (preg_match($route->path, $request, $matches)) {
// $matches[0] will always be equal to $request, so we just shift it off
array_shift($matches);
// here comes the magic
$class = new ReflectionClass($route->controller);
$method = $class->getMethod($route->method);
// we construct the controller using the newly defined method
$instance = $this->constructClassFromArray($class, $matches);
// then we call the method on the newly instantiated object
$method->invoke($instance);
// finally, we return from the function because we do not want the request to be handled more than once
return;
}
}
throw new RuntimeException("The request \'$request\' did not match any route.");
}
private function constructClassFromArray(ReflectionClass $class, array &$array)
{
// getConstructor -- 获取已反射的类的构造函数。类不存在构造函数时返回 null。
// getParameters -- 返回参数列表
$parameters = $class->getConstructor()->getParameters();
// construct the arguments needed for its constructor
$args = [];
foreach ($parameters as $parameter)
$args[] = $this->constructArgumentFromArray($parameter, $array);
// then return the new instance
// 和newInstance方法一样,区别是参数传递方式不一样,这里传的是数组
return $class->newInstanceArgs($args);
}
private function constructArgumentFromArray(ReflectionParameter $parameter, array &$array)
{
// 获取控制器类构造函数的参数类型
$type = $parameter->getType();
// if the parameter was not declared with any type, just return the next element from the array
// array_shift 将 array 的第一个单元移出并作为结果返回,如果 array 为 空或不是一个数组则返回 null。
if ($type === null)
return array_shift($array);
// if the parameter is a primitive type, just cast it
switch ($type->getName()) {
case \'string\':
return (string) array_shift($array);
case \'int\':
return (int) array_shift($array);
case \'bool\':
return (bool) array_shift($array);
}
$class = $parameter->getClass();
// if the parameter is a class type
if ($class !== null) {
// make another call that will actually call this method
return $this->constructClassFromArray($class, $array);
}
throw new RuntimeException("Cannot construct the \'{$parameter->getName()}\' parameter in {$parameter->getDeclaringClass()->getName()}::{$parameter->getDeclaringFunction()->getName()} because it is of an invalid type{$type->getName()}.");
}
}
现在UserController
类正确实例化了,因为Router
知道如何构造控制器所需要的User
参数了。
上面的代码中User
的构造函数接受两个参数。但是,如果User
的构造函数还可以接受一个可为null
的bool类型的参数呢?比如,这个用户是否通过了某项测试?
当然,php的布尔转换规则依然有效。下面我们修改User
类,使其能包含这个参数:
// User.php
class User
{
/** @var string */
public $name;
/** @var int */
public $age;
/** @var bool|null */
public $passed_test;
public function __construct(string $name, int $age, ?bool $passed_test)
{
$this->name = $name;
$this->age = $age;
$this->passed_test = $passed_test;
}
}
修改UserController
的show
方法:
public function show()
{
$message = \'invalid\';
if ($this->user->passed_test === true)
$message = \'They passed the test!\';
elseif ($this->user->passed_test === false)
$message = \'They didn\\\'t pass the test!\';
elseif ($this->user->passed_test === null)
$message = \'They didn\\\'t attempt the test yet.\';
echo "{$this->user->name} is {$this->user->age} years old.\\n";
echo $message;
}
最后修改注册的用户route,使其能够反映上述修改:
$router->register(new Route(\'/^\\/users\\/(\\w+)\\/(\\d+)\\/?(\\w+)?$/\', \'UserController\', \'show\'));
通过浏览器访问example.com/users/mike/26
,此时passed_test
的值会被设置为false
而不是null
,为什么会这样?
原因是,当我们构造User
类时,它的构造函数接受3各参数,但是URL中只包含了两个。因此,最终在constructArgumentFromArray
中调array_shift
结果返回的是null
,而null
被布尔转换规则转换成了false
。
下面做一个简单的修改,constructArgumentFromArray
方法变成:
private function constructArgumentFromArray(ReflectionParameter $parameter, array &$array)
{
$type = $parameter->getType();
// if the parameter was not declared with any type, just return the next element from the array
if ($type === null)
return array_shift($array);
$class = $parameter->getClass();
// if the parameter is a class type
if ($class !== null) {
// make another call that will actually call this method
return $this->constructClassFromArray($class, $array);
}
// we ran out of $array elements
if (count($array) === 0)
// but we can pass null if the parameter allows it
if ($parameter->allowsNull())
return null;
else
throw new RuntimeException("Cannot construct the \'{$parameter->getName()}\' in {$parameter->getDeclaringClass()->getName()}::{$parameter->getDeclaringFunction()->getName()} because the array ran out of elements.");
// if the parameter is a primitive type, just cast it
switch ($type->getName()) {
case \'string\':
return (string) array_shift($array);
case \'int\':
return (int) array_shift($array);
case \'bool\':
return (bool) array_shift($array);
}
throw new RuntimeException("Cannot construct the \'{$parameter->getName()}\' parameter in {$parameter->getDeclaringClass()->getName()}::{$parameter->getDeclaringFunction()->getName()} because it is of an invalid type{$type->getName()}.");
}
现在,当我们通过浏览器访问example.com/users/mike/26
和example.com/users/mike/26/any_truthy_or_falsy_value
时,User
类可以正确实例化了。
以上是关于[翻译]为MVC框架构建路由的主要内容,如果未能解决你的问题,请参考以下文章
ASP.NET Core 6框架揭秘实例演示[02]:基于路由MVC和gRPC的应用开发