路由组件

在我们开始学习路由组件前,让我们给当前框架做一点重构,提高模板的可读性:

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

在我们提取出了请求的查询参数后,就可以像下面一样简化hello.php:

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

现在,我们做好的充足的准备去添加新的特性。

任何网站中,URL的格式都是一个非常重要的方面。感谢URL映射,我们将URL从生成关联相应的代码中解耦,但是这仍然不够灵活。例如,我们可能想要去支持可以直接嵌入数据的动态路径(比如/hello/Fabien),来取代依赖查询字符串的模式(比如/hello?name=Fabien)。

为了实现这个特性,添加一个新的依赖,Symfony 路由组件:

$ composer require symfony/routing

取代了URL映射的数组,路由组件依靠的是一个RouteCollection实例:

use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();

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

use Symfony\Component\Routing\Route;

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

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

阅读 Routing component documentation 来了解路由组件的其他特性,例如URL迭代,属性需求,HTTP模式执行,YAML 或者 XML文件加载器,转储到PHP或Apache重写规则以提高性能等等。

根据储存在RouteCollection实例中的信息,一个UrlMatcher实例可以匹配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'));
/* 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',
);
*/

尽管在我们的例子中没有严格意义上需要请求的上下文,但是在现实中的应用可以被用来实现方法需求等。

如果没有可以匹配的路由,URL匹配器抛出一个异常:

$matcher->match('/not-found');

// throws a Symfony\Component\Routing\Exception\ResourceNotFoundException

知道了这一点,我们可以写一个新版本的框架了:

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

在上述代码中有几个要点:

  • 路由的名称也被用做模板的名称;

  • 500的错误被正确的管理;

  • 请求的属性被提取出来,保证了模板的简易性:

<!-- example.com/src/pages/hello.php -->
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-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行的代码,我们拥有了一个新的,更加强力,更加灵活的框架。尽情玩耍吧!

使用路由组件有一个的额外的好处:基于路由定义生成URL的能力。当在代码中同时使用URL匹配和URL迭代,改变URL模式不会有其他的影响。想要知道怎么使用迭代器?很简单:

use Symfony\Component\Routing;

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

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

这段代码是很容易理解的;感谢上下文,你甚至能够生成抽象的URL:

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匹配器类可以取代默认的UrlMatcher:

$dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes);

echo $dumper->dump();

results matching ""

    No results matching ""