跳到内容

路由组件

编辑此页

在我们开始深入研究路由组件之前,让我们稍微重构一下当前的框架,使模板更具可读性

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

既然我们现在提取了请求查询参数,请按如下方式简化 hello.php 模板

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

现在,我们已经准备好添加新功能了。

任何网站一个非常重要的方面是其 URL 的形式。 感谢 URL 映射,我们已经将 URL 与生成关联响应的代码解耦,但它还不够灵活。 例如,我们可能希望支持动态路径,以允许将数据直接嵌入到 URL 中(例如 /hello/Fabien),而不是依赖查询字符串(例如 /hello?name=Fabien)。

为了支持此功能,添加 Symfony 路由组件作为依赖项

1
$ composer require symfony/routing

路由组件依赖于 RouteCollection 实例,而不是使用数组作为 URL 映射

1
2
3
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();

让我们添加一个描述 /hello/SOMETHING URL 的路由,并为简单的 /bye URL 添加另一个路由

1
2
3
4
use Symfony\Component\Routing\Route;

$routes->add('hello', new Route('/hello/{name}', ['name' => 'World']));
$routes->add('bye', new Route('/bye'));

集合中的每个条目都由一个名称 (hello) 和一个 Route 实例定义,Route 实例由路由模式 (/hello/{name}) 和路由属性的默认值数组 (['name' => 'World']) 定义。

注意

阅读 路由文档 以了解更多关于其众多功能的信息,例如 URL 生成、属性要求、HTTP 方法强制、YAML 或 XML 文件的加载器、PHP 或 Apache 重写规则的转储器以提高性能等等。

基于存储在 RouteCollection 实例中的信息,UrlMatcher 实例可以匹配 URL 路径

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$context = new RequestContext();
$context->fromRequest($request);
$matcher = new UrlMatcher($routes, $context);

$attributes = $matcher->match($request->getPathInfo());

match() 方法接受请求路径并返回属性数组(请注意,匹配的路由会自动存储在特殊的 _route 属性下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$matcher->match('/bye');
/* Result:
[
    '_route' => 'bye',
];
*/

$matcher->match('/hello/Fabien');
/* Result:
[
    'name' => 'Fabien',
    '_route' => 'hello',
];
*/

$matcher->match('/hello');
/* Result:
[
    'name' => 'World',
    '_route' => 'hello',
];
*/

注意

即使在我们的示例中我们并不严格需要请求上下文,但在实际应用中会使用它来强制执行方法要求等。

当没有路由匹配时,URL 匹配器会抛出异常

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 $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->send();

代码中有一些新内容

  • 路由名称用于模板名称;
  • 现在可以正确管理 500 错误;
  • 提取请求属性以保持模板简洁
1
2
// example.com/src/pages/hello.php
Hello <?= 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}', ['name' => 'World']));
    $routes->add('bye', new Routing\Route('/bye'));
    
    return $routes;

现在,我们在配置(app.php 中特定于我们应用程序的所有内容)和框架(front.php 中为我们应用程序提供支持的通用代码)之间有了明确的分离。

使用少于 30 行代码,我们就拥有了一个新的框架,它比以前的框架更强大、更灵活。 尽情享用吧!

使用路由组件有一个很大的额外好处:能够基于路由定义生成 URL。 当在代码中同时使用 URL 匹配和 URL 生成时,更改 URL 模式应该没有其他影响。 您可以这样使用生成器

1
2
3
4
5
6
use Symfony\Component\Routing;

$generator = new Routing\Generator\UrlGenerator($routes, $context);

echo $generator->generate('hello', ['name' => 'Fabien']);
// outputs /hello/Fabien

代码应该是自解释的;并且由于上下文,您甚至可以生成绝对 URL

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

echo $generator->generate(
    'hello',
    ['name' => 'Fabien'],
    UrlGeneratorInterface::ABSOLUTE_URL
);
// outputs something like http://example.com/somewhere/hello/Fabien

提示

担心性能? 基于您的路由定义,创建一个高度优化的 URL 匹配器类,它可以替换默认的 UrlMatcher

1
2
3
4
5
6
7
8
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;

// $compiledRoutes is a plain PHP array that describes all routes in a performant data format
// you can (and should) cache it, typically by exporting it to a PHP file
$compiledRoutes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes();

$matcher = new CompiledUrlMatcher($compiledRoutes, $context);
本作品(包括代码示例)根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本