Front控制器

到现在为止,我们的应用很简单,只有一个页面。为了增加点乐趣,让我们变得疯狂起来,添加另一个页面来say goodbye:

// 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的方式做重构,就是创建一个包含文件:

// 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();

让我们看下init.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();

在“GoodBye”页面中的使用:

// 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脚本是一种被称为front controller的设计模式。

这样一个脚本可能看上去会像下面这个样子:

// 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 = 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脚本的实例:

// framework/hello.php
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, 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并且去除front控制器脚本,这样你就可以输入http://127.0.0.1:4321/hello?name=Fabien来访问,这种URL就看起来好多了。

这里的诀窍是使用了Request::getPathInfo()方法,它返回了请求的路径,这些路径去除了front控制器脚本名称以及其子目录(仅在需要时 -- 请参阅上述提示)。

你甚至不需要专门设置一个web服务器来测试你的代码。事实上,将$request = Request::createFromGlobals();替换成类似于$request = Request::create('/hello?name=Fabien');的调用就可以了,它的参数就是你想要模拟的URL路径。

现在,web服务器对于所有的页面都要访问相同的脚本(front.php),我们可以通过将其他的PHP脚本移到web根目录之外来进一步的保护代码:

example.com
├── composer.json
├── composer.lock
├── src
│ └── pages
│ ├── hello.php
│ └── bye.php
├── vendor
│ └── autoload.php
└── web
   └── front.php

然后,设置你的web服务器的根目录指向web/,所有其他的文件就不能再被客户端访问到了。

运行PHP的内置服务器,在浏览器中测试你的修改:

$ php -S 127.0.0.1:4321 -t web/ web/front.php

为了使这个新的结构可以生效,你需要去调整各种PHP文件中的路径;这些修改就当做是留给读者的练习了。

最后,在所有的页面中还有一个重复的地方,setContent()的调用。我们可以将所有的页面转换为只打印内容的“模板”,同时直接在front控制器脚本中调用setContent():

// 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') ?>

我们有了第一个版本的框架:

// 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 = 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();

加入一个新的页面只要2个步骤:在映射中加入一个入口,在src/pages/中创建一个PHP模板。在一个模板中,通过$request变量获取请求的数据,并通过$response变量调整响应头。

如果你决定就此止步,你可以改善你的框架,将URL的映射的部分写到一个配置文件中。

results matching ""

    No results matching ""