英文原文地址:https://symfony.com/doc/current/create_framework/routing.html
在开始我们今天的话题前,我们先重构一下我们的框架,让我们的模板文件更加易读:
<?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();
$map = array(
'/hello' => 'hello',
'/bye' => 'bye',
);
$path = $request->getPathInfo();
if (isset($map[$path])) {
ob_start();
extract($request->query->all(), EXTR_SKIP);
include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]);
$response = new Response(ob_get_clean());
} else {
$response = new Response('Not Found', 404);
}
$response->send();
由于我们将请求里面的 GET 参数解压(extract)出来了,我们就可以简化模板代码:
<!-- example.com/src/pages/hello.php -->
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
现在我们的代码将可以以更好的状态来添加新的功能了。
任何一个网站都有一个重要的要素,那就是他们的 URL 的形式。多亏了有 $map
变量这个映射表,我们将 URL 以及跟他关联的响应这两部分代码从我们的代码里面解耦出来了,但目前还是不够灵活。举个例子,如果你想动态生成 URL,并将 URL 中的一个部分来代替 GET 参数:
# 之前
/hello?name=Fabien
# 之后
/hello/Fabien
想添加这个功能?请先添加 sf 路由组件
$ composer require symfony/routing
用来代替之前$map这个映射关系数组的,是路由组件,它依赖于 RouteCollection 实例来描述映射关系:
use Symfony\Component\Routing\RouteCollection;
$routes = new RouteCollection();
让我添加两条路由规则,一条是 /hello/SOMETHING
,另一条是简单的 /bye
:
use Symfony\Component\Routing\Route;
$routes->add('hello', new Route('/hello/{name}', array('name' => 'World')));
$routes->add('bye', new Route('/bye'));
每一条路由规则都由一个名字(hello)以及一个 Route 实例来定义,而一条路由实例又由一条路由规则(/hello/{name}
)以及默认值数组(array('name' => 'World')
)来定义。
请阅读官方文档学会路由组件其他更多功能,比如 URL 生成器,属性限制,HTTP 方法限制,YAML,XML 配置载入器,规则转储为 PHP 文件甚至 Apache 的 URL 重写规则来获取性能上的提升,以及其他更多功能。
基于 RouteCollection
实例里存储的信息,UrlMatch
对象可实现对 URL 的匹配:
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Matcher\UrlMatcher;
$context = new RequestContext();
$context->fromRequest($request);
$matcher = new UrlMatcher($routes, $context);
$attributes = $matcher->match($request->getPathInfo());
match 方法接受请求路径为参数,然后返回相关的路由属性数组(注意路由的名字已经自动赋值给 _route 属性了):
print_r($matcher->match('/bye'));
/*array (
'_route' => 'bye',
);*/
print_r($matcher->match('/hello/Fabien'));
/*array (
'name' => 'Fabien',
'_route' => 'hello',
);*/
print_r($matcher->match('/hello'));
/*array (
'name' => 'World',
'_route' => 'hello',
);*/
我们并不严格要求要使用 $context
参数,但在真正的项目中最好还是加上,因为如果你的路由规则除了匹配 URL 还需要匹配 HTTP 的某个方法(译者注:比如 GET、POST 方法),是必须要这个参数的
如果匹配器找不到任何一个匹配规则,他会抛出一个意外:
$matcher->match('/not-found');
// throws a Symfony\Component\Routing\Exception\ResourceNotFoundException
利用上面的知识点,我们将框架代码重写一下:
<?php
// example.com/web/front.php
require_once __DIR__.'/../vendor/.composer/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
try {
extract($matcher->match($request->getPathInfo()), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
$response = new Response(ob_get_clean());
} catch (Routing\Exception\ResourceNotFoundException $e) {
$response = new Response('Not Found', 404);
} catch (Exception $e) {
$response = new Response('An error occurred', 500);
}
$response->send();
此段代码改进以下一些事情:
- 使用 route 名字作为模板的文件名
- 500 错误也可以进行控制和管理了
- 解压后的请求变量让我们的模板文件代码简单许多
<!-- example.com/src/pages/hello.php --> Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
- 路由管理被单独分配到一个文件里面
<?php // example.com/src/app.php use Symfony\Component\Routing; $routes = new Routing\RouteCollection(); $routes->add('hello', new Routing\Route('/hello/{name}', array('name' => 'World'))); $routes->add('bye', new Routing\Route('/bye')); return $routes;
现在我们的框架(
front.php
里的代码)和配置文件(所有都在app.php
文件里配置)有了很明确的分工。
我们用不到 30 行的代码便写好了我们新的框架,比之前那个更灵活更强大了。
使用路由组件还有一个很大的好处:利用路由规则生成 URL。如果你使用路由组件来匹配你的 URL,又使用路由组件来生成你的URL(译者注:也就是传说中的『双向路由』),那么你想更换某个路由的规则,可毫无顾忌对系统的影响。想知道如何利用这个功能生成链接?小菜一碟:
use Symfony\Component\Routing;
$generator = new Routing\Generator\UrlGenerator($routes, $context);
echo $generator->generate('hello', array('name' => 'Fabien'));
// outputs /hello/Fabien
代码非常明了,根本不用再重新说明过程了;然后,多亏了有 context 你甚至可以生成全路径:
echo $generator->generate('hello', array('name' => 'Fabien'), true);
// outputs something like http://example.com/somewhere/hello/Fabien
译者注:任何前段控制器框架,或者说单点入口框架,都会面对路由器性能问题,这个问题甚至被 PHP 之父作为“反对使用框架”的论点之一。事实上,如果一个项目有几十个甚至上百个路由规则,路由器性能的确是一个头痛的问题。sf 的路由转存组件将路由转存为 Apache 的 URL 改写规则,将本来 PHP 就不擅长的路由工作交给特别擅长此工作的 web 服务器,的确是个很靠谱的创新。对性能要求较高的同学可以考虑尝试一下。另外我想既然 Apache 的改写能做,Nginx 的改写规则也应该不远了 2017-01-18 补充:在 Symfony3 里已经不推荐将路由规则转储为 Apache,官方的描述是『为了一点点的性能提升,而需要每次在路由规则有变化的时候去更新 Apache 的改写规则是不划算的』。可能 Symfony3 在路由性能这方面已经优化得足够好了,再加上 PHP7 的性能提升,已经没有必要这么做了。
使用 Symfony 组件创建自己的 PHP 框架(第四部分:路由组件) by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人
5 Comments
duqingnian
10月 2, 2012 在 5:18 下午请教博主,为什么在替换autoload这步骤的时候require_once __DIR__.’/../vendor/.composer/autoload.php’; 我没有.composer 我只有composer 没有前面的小数点 而且里面没有autoload.php这个文件啊 , 何解?
Chris Yue
10月 12, 2012 在 2:04 下午开发环境以及composer版本说一下呢?
Chris Yue
11月 10, 2012 在 11:46 上午我今天看了一下,现在的autoloader.php的确是放在了vendor根目录下面
redfire.du
3月 18, 2012 在 9:33 下午请教博主,为什么我这边老是提示Call to undefined method Symfony\Component\Routing\RequestContext::fromRequest()呢。
Chris Yue
3月 21, 2012 在 10:34 上午你能提供一下完整的代码吗?