通过 PHP 原生代码实现 HTTP 控制器

引言

上篇教程学院君给大家演示了如何基于 PHP 原生代码实现简单的 HTTP 路由器,并且留了个引子:在我们注册路由时,除了通过匿名函数作为处理器之外,还可以通过控制器方法。

说到控制器,不得不提 MVC 设计模式,目前主流的 Web 开发框架都是基于 MVC 模式的,在 MVC 模式中,M 代表模型(Model),V 代表视图(View),C 代表控制器(Controller),控制器负责对请求进行处理并返回响应,模型类负责底层数据存取与处理,而视图层负责数据渲染与页面交互。对于一些 CRUD 操作(数据库增删改查操作)来说,常见的业务逻辑也就是从模型类获取数据并将其渲染到视图页面,或者从视图页面获取用户提交数据并将其存储到模型类,控制器则负责局中调度:

编写控制器基类

在面向对象编程中,我们可以编写控制器类来表示控制器,然后通过控制器方法作为具体的请求处理器,以博客应用为例,在 blog/app/http 目录下新建 controller 子目录来存放所有控制器,在编写具体的业务逻辑控制器之前,先新建一个 Controller.php 脚本来编写控制器基类:

<?php
namespace App\Http\Controller;

use App\Core\Container;
use App\Http\Request;
use App\Store\StoreContract;

class Controller
{
    /**
     * @var StoreContract
     */
    protected $connection;

    /**
     * @var Container
     */
    protected $container;

    /**
     * @var Request
     */
    protected $request;

    public function __construct()
    {
        $this->container = Container::getInstance();
        $store = $this->container->resolve(StoreContract::class);
        $this->connection = $store->newConnection();
        $this->request = $this->container->resolve('request');
    }
}

在这个控制器基类中,我们定义了会被所有控制器共用的 $connection$container$request 属性,分别用于表示数据库连接、应用容器和全局请求实例。然后在该基类的构造函数中,对这几个属性进行初始化,也就是从应用容器中解析出对应的对象实例。

编写业务控制器类

接下来,我们要做的就是将 app/routes/web.php 中之前通过匿名函数注册的请求处理器代码重构到对应的控制器方法中。

app/http/controller 目录下创建对应的控制器文件:

-w522

然后编写对应的控制器类代码,首先是处理首页请求的 HomeController

<?php
namespace App\Http\Controller;

class HomeController extends Controller
{
    public function index()
    {
        $albums = $this->connection->table('albums')->selectAll();
        $pageTitle = $siteName = $this->container->resolve('app.name');
        $siteUrl = $this->container->resolve('app.url');
        $siteDesc = $this->container->resolve('app.desc');
        include __DIR__  . "/../../../views/home.php";
    }
}

然后是处理专辑页请求的 AlbumController

<?php
namespace App\Http\Controller;

class AlbumController extends Controller
{
    public function list()
    {
        $id = intval($this->request->get('id'));
        if (empty($id)) {
            echo '请指定要访问的专辑 ID';
            exit();
        }
        $album = $this->connection->table('albums')->select($id);
        $posts = $this->connection->table('posts')->selectByWhere(['album_id' => $id]);
        $pageTitle = $siteName = $this->container->resolve('app.name');
        $siteUrl = $this->container->resolve('app.url');
        include __DIR__  . '/../../../views/album.php';
    }
}

最后是处理文章页请求的 PostController

<?php
namespace App\Http\Controller;

class PostController extends Controller
{
    public function show()
    {
        $id = intval($this->request->get('id'));
        if (empty($id)) {
            echo '请指定要访问的文章 ID';
            exit();
        }
        $post = $this->connection->table('posts')->select($id);
        $printer = $this->container->resolve(\App\Printer\PrinterContract::class);
        if ($this->container->resolve('app.editor') == 'markdown') {
            $post['content'] = $printer->driver('markdown')->render($post['text']);
        } else {
            $post['content'] = $printer->render($post['html']);
        }
        $album = $this->connection->table('albums')->select($post['album_id']);
        $pageTitle = $post['title'] . ' - ' . $this->container->resolve('app.name');
        $siteUrl = $this->container->resolve('app.url');
        include __DIR__  . '/../../../views/post.php';
    }
}

重构路由注册和分发代码

这样一来,我们就将 app/routes/web.php 中之前以匿名函数形式注册的路由处理器代码都搬到控制器中了,因此,可以移除对应的代码,并将路由的处理器属性调整为对应的控制器方法:

<?php
$router = new \App\Http\Router();

$router->register('get', '/', 'HomeController@index');

$router->register('get', 'album', 'AlbumController@list');

$router->register('get', 'post', 'PostController@show');

这样一来,路由注册代码就精简了很多,结构也更加清晰,为了能够正常执行形如 HomeController@index 的路由处理器,需要在 Router 类的 dispatch 方法中对其进行解析和处理:

public function dispatch(Request $request)
{
    ...
    if (is_callable($callback)) {
        // 通过匿名函数注册的路由回调
        call_user_func($callback, $request);
    } elseif (is_string($callback) && strpos($callback, '@') !== FALSE) {
        // 通过控制器方法注册的路由回调
        list($controller, $method) = explode('@', $callback);
        $controller = 'App\\Http\\Controller\\' . $controller;
        $instance = new $controller;
        call_user_func([$instance, $method]);
    } else {
        throw new \Exception('无效的路由回调');
    }
}

重点关注通过控制器方法注册路由回调这段代码,首先通过 explode 函数解析出控制器名称和方法,然后加上默认命名空间前缀 App\Http\Controller\ 以便可以加载到具体的控制器类,最后,通过 call_user_func 函数执行控制器对象实例上的对应方法返回响应给客户端。

其他收尾工作

除此之外,由于我们调整了视图引入逻辑和位置,所以之前传入的 $container 变量不再可用,因此,需要将引用该变量的代码片段进行调整,具体可以参考 Github 上的项目源码

运行 composer dump-auto 让代码修改产生的命名空间与目录路径映射调整生效,访问 http://localhost:9000 访问博客首页,可以正常访问则表示代码重构成功:

-w977

到目前为止,我们已经在项目中引入了路由器和控制器,接下来,学院君会引入模板引擎机制优化视图模板的引入和变量传递,因为目前通过简单的 include 语句这种方式维护起来很不方便,实现也不够优雅。

上一篇: 通过 PHP 原生代码实现 HTTP 路由器

下一篇: 通过 PHP 原生代码实现视图模板引擎的解析和渲染