使用Symfony2的组件创建自己的PHP框架(第四部分)

英文原文地址:http://fabien.potencier.org/article/53/create-your-own-framework-on-top-of-the-symfony2-components-part-4

在开始我们今天的话题前,我们先重构一下我们的框架,让我们的模板文件更加易读:

<?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

想添加这个功能?请使用sf2的路由组件吧。如同以往,现在composer.json文件里面添加它,然后执行Composer的update命令安装它。

{
    "require": {
        "symfony/class-loader": "2.1.*",
        "symfony/http-foundation": "2.1.*",
        "symfony/routing": "2.1.*"
    }
}

现在开始,我们开始使用composer的autoloader,来代替我们之前所用的sf2自己的autoloader。删除之前写得autoload.php文件,然后把front.php引用它的代码改成引用composer的autoloader:

<?php

// example.com/web/front.php

require_once __DIR__.'/../vendor/.composer/autoload.php';

// ...

用来代替之前$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参数,但在真正的项目中最好还是加上,因为需要他来匹配http方法以及其他属性(译者注:你可以定义一个url只能用http的GET方法访问,不使用context而只传pathinfo参数,是做不到这一点的,所以作者说最好还是使用context。但实际上我认为context的存在让api看上去不舒服,因为第一时间很难判断context的作用是什么,为什么需要这个参数,以及思考:既然已经使用了context,为什么还需要单独传入pathinfo)。

如果匹配器找不到任何一个匹配规则,他会抛出一个意外:

$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

是否在关注路由的性能问题?基于你指定的路由规则,你可以创建一个被强力优化过的匹配类,来代替之前的UrlMatcher():

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

echo $dumper->dump();

还不满足?你还可以将路由规则转储为apache的重写规则:

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

echo $dumper->dump();

译者注:任何前段控制器框架,或者说单点入口框架,都会面对路由器性能问题,这个问题甚至被php之父作为“反对使用框架”的论点之一。但事实上,如果一个项目有几十个甚至上百个路由规则,路由器性能的确是一个头痛的问题。sf2的路由转存组件将路由转存为apache的url改写规则,将本来php就不擅长的路由工作交给特别擅长此工作的web服务器,的确是个很靠谱的创新。对性能要求较高的同学可以考虑尝试一下。另外我想既然apache的改写能做,nginx的改写规则也应该不远了。

返回阅读第三部分 | 继续阅读第五部分

This content is published under the Attribution-Noncommercial-Share Alike 3.0 Unported license.

分享到:
This entry was posted in Posts and tagged , , . Bookmark the permalink.

6 Responses to 使用Symfony2的组件创建自己的PHP框架(第四部分)

  1. Pingback: 使用Symfony2的组件创建自己的PHP框架 | Chrisyue's Blog

  2. Pingback: 使用Symfony2的组件创建自己的PHP框架(第五部分) | Chrisyue's Blog

  3. Pingback: 使用Symfony2的组件创建自己的PHP框架(第五部分) | Chrisyue's Blog

  4. Pingback: A week of symfony #263 (9->15 January 2012) « We are php

  5. redfire.du says:

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

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>