[翻译]为MVC框架构建路由

[翻译]为MVC框架构建路由

原文地址

如果你还不清楚什么是MVC,建议你在读这篇文章之前先了解一下什么是MVC。

这篇文章里的代码要求的PHP版本最低是7.1。

假设你的域名是example.com,你想让你的应用通过example.com/gallery/cats请求给用户展示一个凯蒂猫的图片库,并且通过example.com/gallery/dogs请求展示狗的图片库。

通过分析请求URL,我们注意到单词gallery没有变化,只有catsdogs变了。因此,我们将创建一个类来处理这些逻辑。

// 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
工作原理很简单:

  1. 使用router注册route,并告诉router处理用户的请求;
  2. router检查所有注册过的route,并与请求进行匹配;
  3. 如果找到匹配的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;
    }
}

修改UserControllershow方法:

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/26example.com/users/mike/26/any_truthy_or_falsy_value时,User类可以正确实例化了。

posted @ 2021-09-01 15:30  Fogwind  阅读(81)  评论(0编辑  收藏  举报