使用 Symfony 组件创建自己的 PHP 框架(第三部分:前端控制器)

使用 Symfony 组件创建自己的 PHP 框架(第三部分:前端控制器)

Chris Yue No Comment
Posts

英文原文地址:https://symfony.com/doc/current/create_framework/front_controller.html

直到现在,我们的应用程序还只有一个页面,非常简单。为了再增加一点点乐趣,我们再添加一个“再会”页面。

<?php

// framework/bye.php

require_once __DIR__.'/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$response = new Response('Goodbye!');
$response->send();

如你所见,这段代码跟我们第一个页面的代码写法都差不多。让我们把相同的代码提取出来,这样我们就可以让这段代码在所有要创建的页面里重用了。对于创建我们“真正的”框架来说,代码重用听起来是个不错的想法!

以 PHP 的方式来完成上面目标的重构,就是创建一个包含文件:

<?php

// framework/init.php

require_once __DIR__.'/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

首页改成:

<?php

// framework/index.php

require_once __DIR__.'/init.php';

$input = $request->get('name', 'World');

$response->setContent(sprintf(
    'Hello %s', 
    htmlspecialchars($input, ENT_QUOTES, 'UTF-8')
));
$response->send();

以及我们的“再会”页:

<?php

// framework/bye.php

require_once __DIR__.'/init.php';

$response->setContent('Goodbye!');
$response->send();

这样我们已将公共代码挪到了单独的文件,但这样做感觉上还是不够好,是吧?首先,我们每个页面代码依然还存在共同的send方法,其次我们的页面代码任然看起来不像模版代码,而且我们依然不能很好的测试这段代码。

再其次,添加一个新的页面,意味着我们要添加一个新的 PHP 脚本文件,并且此文件是通过 URL 完全暴露给终端用户的(http://example.com/bye.php),PHP 脚本文件名和客户端 URL 是完全直接相互映射的,这是因为请求的调度工作完全是由web服务器直接完成的。但为了灵活性,把调度工作直接交给我们自己的代码来做似乎是一个更好的想法。将所有的请求交给一个 PHP 脚本来做路由处理的话,这个想法是很容易实现的。

只暴露一个脚本给终端用户这种方式,是一种叫“前段控制器(front controller)”的设计模式

这个文件如下所示:

<?php

// framework/front.php

require_once __DIR__.'/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

$map = array(
    '/hello' => __DIR__.'/hello.php',
    '/bye'   => __DIR__.'/bye.php',
);

$path = $request->getPathInfo();
if (isset($map[$path])) {
    require $map[$path];
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

经过改造后的新页面代码,举个例子,hello.php,应该是这个样子:

<?php

// framework/hello.php

$input = $request->get('name', 'World');
$response->setContent(sprintf(
    'Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')
));

在 front.php 文件中,$map 定义了 URL 路径和相关 PHP 页面脚本路径的对应关系。

这么做还有一个好处是,如果用户输入了一个 $map 里未定义的 URL,我们可以返回一个自定义的 404 页面。现在你可以用代码完全控制你的网站了

你现在必须使用front.php来访问一些页面:

  • http://example.com/front.php/hello?name=Fabien
  • http://example.com/front.php/bye

/hello/bye是页面的路径

大部分 web 服务器,比如 Nginx 和 Apache,都有改写 URL 的功能,可以将前段控制器脚本从 URL 里面去掉,比如可以让用户直接输入 http://example.com/hello?name=Fabien 来进入上面的连接,这样看起来 URL 干净了许多。

能实现上面功能的诀窍便是使用 Request::getPathInfo() 方法,此方法将返回不包括前段控制器文件名,但包含它的子目录路径的路径信息。

你甚至都不用建立一个 web 服务器来做测试,你只用将 Request::createFromGlobals() 方法替换成比如 Request::create('/hello?name=Fabien'),你就可以模拟任何请求了。

目前所有的请求都是通过 front.php 来访问的,所以我们可以将其他 PHP 文件挪到 web 目录的外面去,以达到保护其他代码的作用。

example.com/
    composer.json
    src/
        autoload.php
        pages/
            hello.php
            bye.php
    vendor/
    web/
        front.php

现在,更改一下 web 服务器的配置,把访问的根目录设置到 web 目录下,这样 web 目录外面的代码,客户端就不能直接访问到了。

更改目录结构以后,有些包含路径是需要手工去调整的,这些就留给读者自己去修改了

最后一个被重复写过多次的代码是 Response::setContent() 方法。我们可以把所有的页面代码变成直接 echo 出来的模版代码,这样我们就可以在前段控制器脚本中直接调用 setContent 方法了:

<?php

// example.com/web/front.php

// ...

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

// ...

hello.php也需要修改成模版页:

<!-- example.com/src/pages/hello.php -->

<?php $name = $request->get('name', 'World') ?>

Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>

最终我们得到了我们今天的框架代码:

<?php

// example.com/web/front.php

require_once __DIR__.'/../src/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

$map = array(
    '/hello' => __DIR__.'/../src/pages/hello.php',
    '/bye'   => __DIR__.'/../src/pages/bye.php',
);

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

要在添加一个新页面,目前只需要两步完成:在 $map 变量里面添加一个映射,然后在 src/pages/ 目录添加一个模版页面。在模版文件里面,通过 $request 对象获得请求参数,然后利用 $response 变量,修改响应内容。

如果你决定在此停止阅读,你最好是把 $map 的映射关系表提取出来放在单独的文件里面

返回阅读第二部分 | 继续阅读第四部分

使用 Symfony 组件创建自己的 PHP 框架(第三部分:前端控制器) by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

写作累,服务器也越来越贵
求分担,祝愿好人一生平安
天使打赏人

发表评论

7 + 1 =

2021年7月
 1234
567891011
12131415161718
19202122232425
262728293031  
Composer CSS Firefox industrial metal JavaScript LeetCode Linux MySQL NGINX nu metal OAuth PHP PHP7 Shell Symfony Ubuntu Vim 全栈 到底系列 命令行 安全 教程 框架 算法 翻译