十四、将 URL 路径与文件路径解耦

尽管我们有一个文档根,它将公共资源和非公共资源分开,但遗留应用程序的用户仍然可以直接浏览到我们的页面脚本。这意味着我们的 URL 直接耦合到 web 服务器上的文件系统路径。

我们的下一步是分离路径,这样我们就可以独立地将 URL 路由到我们想要的任何目标。这意味着放置一个前端控制器来处理遗留应用程序的所有传入请求。

耦合路径

正如我们在上一章中所指出的,我们的 web 服务器充当传统应用程序的前端控制器、路由器和调度器的组合。页面脚本的路由仍然直接映射到文件系统上,使用我们的docroot/目录作为所有 URL 路径的基础。

这给我们带来了一些结构性问题。例如,如果我们想要公开一个新的或不同的 URL,我们必须修改相关页面脚本在文件系统中的位置。类似地,我们无法更改页面脚本响应特定 URL 的内容。在传入请求被路由之前,无法拦截该请求。

这些和其他问题,包括完成未来重构的能力,意味着我们必须为所有传入请求创建一个入口点。该入口点称为前端控制器。

在我们的遗留应用程序的前端控制器的第一个实现中,我们将添加一个路由器,以将传入的 URL 路径转换为页面脚本路径。这将允许我们从文档根目录中删除页面脚本,从而将 URL 与文件系统分离。

解耦过程

正如将我们的公共资源与非公共资源分开一样,我们必须对我们的 web 服务器配置进行更改。具体来说,我们将启用 URL 重写,以便将所有传入请求指向前端控制器。我们需要与我们的操作人员协调此重构,以便他们能够尽可能轻松地部署更改。

一般而言,流程如下:

  1. 与运营部协调,传达我们的意图。
  2. 在文档根目录中创建前端控制器脚本。
  3. 为我们的页面脚本创建一个pages/目录,以及一个page not found页面脚本和控制器。
  4. 重新配置 web 服务器以启用 URL 重写。
  5. 抽查重新配置的 web 服务器,确保前端控制器和 URL 重写工作正常。
  6. 将所有页面脚本从docroot/移动到pages/,沿途抽查。
  7. 提交、推动和协调 QA 测试的操作。

配合运营

这是这个过程中最重要的一步。在未与服务器负责人(即我们的操作人员)讨论我们的意图之前,我们决不应做出影响服务器配置的更改。

在这种情况下,我们需要告诉我们的操作人员,我们必须启用 URL 重写。他们将建议或指导我们如何为特定的 web 服务器执行此操作。

或者,如果我们没有操作人员并且负责我们自己的服务器,我们将需要自行决定如何启用 URL 重写。在这种情况下,请小心操作。

增加一个前端控制器

一旦与我们的操作人员协调,我们将添加一个前端控制器脚本。我们还将添加一个page not found脚本、控制器和视图。

首先,我们在文档根目录中创建前端控制器脚本。它使用Router类将传入 URL 映射到页面脚本。我们称之为 front.php,或者其他一些名称,表明它是一个前端控制器:

docroot/front.php
1 <?php
2 // the router class file
3 require dirname(__DIR__) . '/classes/Mlaphp/Router.php';
4
5 // set up the router
6 $pages_dir = dirname(__DIR__) . '/pages';
7 $router = new \Mlaphp\Router($pages_dir);
8
9 // match against the url path
10 $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
11 $route = $router->match($path);
12
13 // require the page script
14 require $route;
15 ?>

由于自动加载器尚未注册,我们require创建了Router类文件。只有在执行页面脚本时才会发生这种情况,直到前端控制器逻辑结束时才会发生这种情况。我们将在下一章中纠正这种情况。

创建页面/目录

前控制器参考 a$pages_dir。我们的想法是将所有页面脚本从文档根目录移到这个新目录中。

首先,我们在遗留应用程序的顶层创建一个目录,紧挨着classes/docroot/views/等目录。

然后我们创建一个pages/not-found.php脚本,以及相应的控制器和视图文件。当Router无法匹配 URL 路径时,前端控制器将调用not-found.php脚本。not-found.php脚本应该像我们遗留应用程序中的任何其他页面脚本一样进行设置,然后调用其相应的视图文件进行响应:

pages/not-found.php
1 <?php
2 require '../includes/setup.php';
3
4 $request = new \Mlaphp\Request($GLOBALS);
5 $response = new \Mlaphp\Response('/path/to/app/views');
6 $controller = new \Controller\NotFound($request, $response);
7
8 $response = $controller->__invoke();
9
10 $response->send();
11 ?>
classes/Controller/NotFound.php
1 <?php
2 namespace Controller;
3
4 use Mlaphp\Request;
5 use Mlaphp\Response;
6
7 class NotFound
8 {
9 protected $request;
10
11 protected $response;
12
13 public function __construct(Request $request, Response $response)
14 {
15 $this->request = $request;
16 $this->response = $response;
17 }
18
19 public function __invoke()
20 {
21 $url_path = parse_url(
22 $this->request->server['REQUEST_URI'],
23 PHP_URL_PATH
24 );
25
26 $this->response->setView('not-found.html.php');
27 $this->response->setVars(array(
28 'url_path' => $url_path,
29 ));
30
31 return $this->response;
32 }
33 }
34 ?>
views/not-found.html.php
1 <?php $this->header('HTTP/1.1 404 Not Found'); ?>
2 <html>
3 <head>
4 <title>Not Found</title>
5 </head>
6 <body>
7 <h1>Not Found</h1>
8 <p><?php echo $this->esc($url_path); ?></p>
9 </body>
10 </html>

重新配置服务器

现在我们有了前端控制器和页面脚本的目标位置,我们重新配置了本地开发 web 服务器,以支持 URL 重写。我们的操作人员应该给我们一些如何操作的指示。

不幸的是,关于 web 服务器管理的完整说明超出了本书的范围。有关详细信息,请查看特定服务器的文档。

在 Apache 中,我们首先启用mod_rewrite模块。在一些 Linux 发行版中,这就像发布sudo a2enmod重写一样简单。在其他情况下,我们需要编辑httpd.conf文件以启用它。

启用 URL 重写后,我们需要指示 web 服务器将所有传入请求指向前端控制器。在 Apache 中,我们可能会向遗留应用程序添加一个docroot/.htaccess文件。或者,我们可以修改本地开发服务器的一个 Apache 配置文件。重写逻辑如下所示:

docroot/.htaccess
1 # enable rewriting
2 RewriteEngine On
3
4 # turn empty requests into requests for the "front.php"
5 # bootstrap script, keeping the query string intact
6 RewriteRule ^$ front.php [QSA]
7
8 # for all files and dirs not in the document root,
9 # reroute to the "front.php" bootstrap script,
10 # keeping the query string intact, and making this
11 # the last rewrite rule
12 RewriteCond %{REQUEST_FILENAME} !-f
13 RewriteCond %{REQUEST_FILENAME} !-d
14 RewriteRule ^(.*)$ front.php [QSA,L]

例如,如果传入的请求是针对/foo/bar/baz.php,web 服务器将调用front.php脚本。每一个请求都是如此。各种超全局值将保持不变,因此$_SERVER['REQUEST_URI']仍将指示/foo/bar/baz.php

最后,在启用 URL 重写后,我们重新启动或重新加载 web 服务器以使更改生效。

抽查

既然我们已经启用了 URL 重写,将所有请求指向新的前端控制器,那么我们应该使用我们知道不存在的 URL 路径浏览到遗留应用程序。前端控制器应该向我们显示not-found.php页面脚本的输出。这表明我们的更改工作正常。如果没有,我们需要审查和修改我们的更改,直到目前为止,并尝试修复任何出错的地方。

移动页面脚本

一旦我们确定 URL 重写和前端控制器工作正常,我们就可以开始将所有页面脚本移出docroot/并移到新的pages/目录中。请注意,我们只移动页面脚本。我们应该将所有其他资源留在docroot/中,包括front.php前端控制器。

例如,如果我们从这个结构开始:

/path/to/app/
docroot/
css/
foo/
bar/
baz.php
front.php
images/
index.php
js/
pages/
not-found.php

我们最终应该采用这种结构:

/path/to/app/
docroot/
css/
front.php
images/
js/
pages/
foo/
bar/
baz.php
index.php
not-found.php

我们只移动了页面脚本。图像、CSS 文件、Javascript 文件和前端控制器都保留在docroot/中。

因为我们正在移动文件,我们可能需要更改 include path 值以指向新的相对目录位置。

当我们将每个文件或目录从docroot/移动到pages/时,我们应该抽查所做的更改,以确保遗留应用程序继续正常工作。

由于前面描述的重写规则,我们的页面脚本应该继续工作,无论它们在docroot/还是pages/中。我们希望确保在继续之前将所有页面脚本移动到pages/

提交、推送、协调

当我们将所有页面脚本移动到新的pages/目录,并且我们的遗留应用程序在这个新结构中正常工作时,我们提交所有更改并将它们推送到公共存储库。

在这一点上,我们通常会通知 QA 我们的更改,以便他们进行测试。但是,由于我们已经更改了服务器配置,我们需要与操作人员协调 QA 测试。运营部门可能需要将新配置部署到 QA 服务器。只有这样,QA 才能有效地检查我们的工作。

常见问题

我们真的把路径解耦了吗?

精明的观察者会注意到我们的路由器仍然使用传入的 URL 路径来查找页面脚本。此设置与原始设置的唯一区别是路径映射到pages/目录,而不是docroot/目录。我们是否真的将 URL 与文件系统解耦了?

是的,我们已经实现了脱钩目标。这是因为我们现在在 URL 路径和执行的页面脚本之间有一个拦截点。使用路由器,我们可以创建一个路由数组,其中 URL 路径是密钥,文件路径是值。该映射数组允许我们将传入的 URL 路径路由到我们喜欢的任何页面脚本。

例如,如果我们想要将/foo/bar.php这样的 URL 路径映射到/baz/dib.php这样的页面脚本,我们可以通过路由器上的setRoutes()方法进行映射:

1 $router->setRoutes(array(
2 '/foo/bar.php' => '/baz/dib.php',
3 ));

然后当我们match()针对路由器输入/foo/bar.php的 URL 路径时,我们返回的路由将是/baz/dib.php。然后,我们可以将该路由作为传入 URL 的页面脚本执行。我们将在下一章中使用此技术的变体。

回顾和下一步

随着 URL 与页面脚本的分离,我们的现代化工作即将完成。只剩下两次重构。首先,我们将页面脚本中的重复逻辑移到前端控制器。然后,我们将完全删除页面脚本,并用依赖项注入容器替换它们。