在开始我们今天的话题前,我们先重构一下我们的框架,让我们的模板文件更加易读:
<?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.
Pingback: 使用Symfony2的组件创建自己的PHP框架 | Chrisyue's Blog
Pingback: 使用Symfony2的组件创建自己的PHP框架(第五部分) | Chrisyue's Blog
Pingback: 使用Symfony2的组件创建自己的PHP框架(第五部分) | Chrisyue's Blog
Pingback: A week of symfony #263 (9->15 January 2012) « We are php
请教博主,为什么我这边老是提示Call to undefined method Symfony\Component\Routing\RequestContext::fromRequest()呢。
你能提供一下完整的代码吗?