路由组件

3.3 版本
维护中的版本

在深入路由组件之前,先轻度重构一下我们的构架,以令模板更具可读性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
 
$request = Request::createFromGlobals();
 
$map = [
    '/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();

由于extract了请求中的query parameters,hello.php模板简化如下:

1
2
<!-- example.com/src/pages/hello.php -->
Hello <?php echo htmlspecialchars(isset($name) ? $name : 'World', ENT_QUOTES, 'UTF-8') ?>

现在,我们处在一个良好的(框架)形态中,可以添加新功能了。

任何网站都有一个非常重要的方面,也即它的URLs的组织形式。得益于URL映射(map),我们已经把URL从“负责生成响应的”代码解耦,但这仍然不够灵活。例如,我们可能想要支持动态路径(dynamite paths)来把data直接嵌入到URL中(如/hello/Fabien),而不是依赖于一个query string(如/hello?name=Fabien)。

为支持此功能,添加Symofny Routing组件作为依赖:

1
$  composer require symfony/routing

不同于使用一个URL映射数组,Routing组件依赖一个RoutingCollection实例:

1
2
3
use Symfony\Component\Routing\RouteCollection;
 
$routes = new RouteCollection();

现在添加路由,描述/hello/SOMETHING这种URL,再添加另外一个简单的/bye

1
2
3
4
use Symfony\Component\Routing\Route;
 
$routes->add('hello', new Route('/hello/{name}', array('name' => 'World')));
$routes->add('bye', new Route('/bye'));

路由集合中的每一个入口(entry),都是通过一个名字(name。如hello)和一个Route实例来定义,该实例则通过一个route pattern(路由匹配)和一个路由属性(route attributes)的“默认值”数组来定义(array('name' => 'World')。

参考中文版路由组件文档以了解更多功能,比如URL生成、属性条件(attribute requirements)、HTTP方法的强制指定、YAML或XML文件的loader(加载器)、通过PHP/Apache rewrite rules的dumpers(剥离器)来提升性能等等。

基于存放在RouteCollection实例中的信息,一个UrlMatcher实例能够匹配URL路径:

1
2
3
4
5
6
7
8
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()方法接收一个request路径,返回的是一个属性数组(attributes。注意被匹配的路由(之name)将自动地存在一个特殊的_route()属性中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
print_r($matcher->match('/bye'));
/* Gives:
array (
  '_route' => 'bye',
);
*/
 
print_r($matcher->match('/hello/Fabien'));
/* Gives:
array (
  'name' => 'Fabien',
  '_route' => 'hello',
);
*/
 
print_r($matcher->match('/hello'));
/* Gives:
array (
  'name' => 'World',
  '_route' => 'hello',
);
*/

就算我们在例程中不十分需要request context,它在真实世界的程序中,可以用作强制method requirements(HTTP方法的匹配)等功能。

URL matcher在匹配不到路由时会抛出一个异常:

1
2
3
$matcher->match('/not-found');
 
// throws a Symfony\Component\Routing\Exception\ResourceNotFoundException

对这些知识胸有成竹的话,我们写一个新版本的框架吧:

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
26
27
// example.com/web/front.php
require_once __DIR__.'/../vendor/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 names)被使用在模板名称中;

  • 500错误现已被正确管理;

  • 请求的属性被提取出来以令模板更加简单:

1
2
<!-- example.com/src/pages/hello.php -->
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
  • 路由配置已被移入属于它的一个文件:
1
2
3
4
5
6
7
8
// 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;

我们现在得到了配置(程序的每一个细节都在app.php中)与框架(驱动我们程序的通用代码全在front.php中)之间的清晰分离。

少于30行代码,我们却得到了一个全新框架,比之前的更加强大,更加灵活。Enjoy!

使用路由组件有一个很大的利益:基于路由定义来生成URLs。当你在代码中同时使用URL匹配和URL生成时,修改一个URL匹配条件时将不会带来其他冲击。想知道如何使用generator(生成器)?超级简单:

1
2
3
4
5
6
use Symfony\Component\Routing;
 
$generator = new Routing\Generator\UrlGenerator($routes, $context);
 
echo $generator->generate('hello', array('name' => 'Fabien'));
// outputs /hello/Fabien

以上代码能够自我解释。另外得益于context,你甚至可以生成绝对URLs:

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 
echo $generator->generate(
    'hello',
    array('name' => 'Fabien'),
    UrlGeneratorInterface::ABSOLUTE_URL
);
// outputs something like http://example.com/somewhere/hello/Fabien

关心性能?基于你的路由定义,创建一个优化良好的URL matcher类,即可取代默认的UrlMatcher

1
2
3
$dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes);
 
echo $dumper->dump();

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)