跳到内容

前端控制器

编辑此页

到目前为止,我们的应用程序非常简单,只有一个页面。为了增加一些趣味性,让我们疯狂一下,添加另一个页面来说再见

1
2
3
4
5
6
7
8
9
10
// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';

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

$request = Request::createFromGlobals();

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

正如您自己所见,大部分代码与我们为第一个页面编写的代码完全相同。 让我们提取可以在所有页面之间共享的通用代码。 代码共享听起来像是一个创建我们的第一个“真正的”框架的好计划!

PHP 的重构方式可能是创建包含文件

1
2
3
4
5
6
7
8
// framework/init.php
require_once __DIR__.'/vendor/autoload.php';

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

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

让我们看看它的实际效果

1
2
3
4
5
6
7
// framework/index.php
require_once __DIR__.'/init.php';

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

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

以及“再见”页面

1
2
3
4
5
// framework/bye.php
require_once __DIR__.'/init.php';

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

我们确实将大部分共享代码移到了中心位置,但这感觉不像是一个好的抽象,不是吗? 我们仍然为所有页面都有 send() 方法,我们的页面看起来不像模板,我们仍然无法正确测试此代码。

此外,添加新页面意味着我们需要创建一个新的 PHP 脚本,其名称通过 URL 暴露给最终用户 (http://127.0.0.1:4321/bye.php)。 PHP 脚本名称和客户端 URL 之间存在直接映射。 这是因为请求的调度直接由 Web 服务器完成。 将此调度移动到我们的代码中以获得更好的灵活性可能是一个好主意。 这可以通过将所有客户端请求路由到单个 PHP 脚本来实现。

提示

向最终用户公开单个 PHP 脚本是一种名为“前端控制器”的设计模式。

这样的脚本可能如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

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

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

$map = [
    '/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 脚本

1
2
3
// framework/hello.php
$name = $request->query->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));

front.php 脚本中,$map 将 URL 路径与其对应的 PHP 脚本路径关联起来。

作为奖励,如果客户端请求的路径未在 URL 映射中定义,我们将返回自定义 404 页面。 您现在可以控制您的网站了。

要访问页面,您现在必须使用 front.php 脚本

  • http://127.0.0.1:4321/front.php/hello?name=Fabien
  • http://127.0.0.1:4321/front.php/bye

/hello/bye 是页面路径

提示

大多数 Web 服务器(如 Apache 或 nginx)都能够重写传入的 URL 并删除前端控制器脚本,以便您的用户能够键入 http://127.0.0.1:4321/hello?name=Fabien,这看起来好得多。

诀窍是使用 Request::getPathInfo() 方法,该方法通过删除前端控制器脚本名称(包括其子目录)来返回 Request 的路径(仅在需要时 - 请参阅上面的提示)。

提示

您甚至不需要设置 Web 服务器来测试代码。 相反,将 $request = Request::createFromGlobals(); 调用替换为类似 $request = Request::create('/hello?name=Fabien'); 的内容,其中参数是您要模拟的 URL 路径。

现在,Web 服务器始终为所有页面访问相同的脚本 (front.php),我们可以通过将所有其他 PHP 文件移到 Web 根目录之外来进一步保护代码

1
2
3
4
5
6
7
8
9
10
11
example.com
├── composer.json
├── composer.lock
├── src
│   └── pages
│       ├── hello.php
│       └── bye.php
├── vendor
│   └── autoload.php
└── web
    └── front.php

现在,将您的 Web 服务器根目录配置为指向 web/,所有其他文件将不再可从客户端访问。

要在浏览器中测试您的更改 (http://127.0.0.1:4321/hello?name=Fabien),请运行 Symfony 本地 Web 服务器

1
$ symfony server:start --port=4321 --passthru=front.php

注意

为了使这种新结构能够工作,您将必须调整各种 PHP 文件中的某些路径; 更改留给读者作为练习。

每个页面中重复的最后一件事是对 setContent() 的调用。 我们可以通过回显内容并直接从前端控制器脚本调用 setContent() 来将所有页面转换为“模板”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 脚本现在可以转换为模板

1
2
3
4
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->query->get('name', 'World') ?>

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

我们有第一个版本的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

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

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

$map = [
    '/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();

添加新页面是一个两步过程:在映射中添加条目并在 src/pages/ 中创建 PHP 模板。 从模板中,通过 $request 变量获取 Request 数据,并通过 $response 变量调整 Response 标头。

注意

如果您决定在此处停止,您可能会通过将 URL 映射提取到配置文件来增强您的框架。

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本