使用 Symfony 组件创建自己的 PHP 框架(第四部分:路由组件)

使用 Symfony 组件创建自己的 PHP 框架(第四部分:路由组件)

Chris Yue 5 comments
Posts

英文原文地址: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

十月 2, 2012 在 5:18 下午

请教博主,为什么在替换autoload这步骤的时候require_once __DIR__.’/../vendor/.composer/autoload.php’; 我没有.composer 我只有composer 没有前面的小数点 而且里面没有autoload.php这个文件啊 , 何解?

    Chris Yue

    十月 12, 2012 在 2:04 下午

    开发环境以及composer版本说一下呢?

     

    Chris Yue

    十一月 10, 2012 在 11:46 上午

    我今天看了一下,现在的autoloader.php的确是放在了vendor根目录下面

     

redfire.du

三月 18, 2012 在 9:33 下午

请教博主,为什么我这边老是提示Call to undefined method Symfony\Component\Routing\RequestContext::fromRequest()呢。

    Chris Yue

    三月 21, 2012 在 10:34 上午

    你能提供一下完整的代码吗?

     

duqingnian进行回复 取消回复

19 − = 16