模板

3.4 版本
维护中的版本

眼尖的读者已经注意到,我们的框架写死了“特定代码”(即模板)运行的方式。对于简单的页面,像我们目前创建的这些,这样没什么问题,但如果你要添加更多逻辑,你只能被迫把逻辑部分放到模板本身,这可能不是好主意,特别是当你坚持“seperation of concerns”(关联分离)原则时。

通过添加一个新层(a new layer),我们把模板代码从逻辑中分离出来:控制器——控制器的任务,是基于客户端请求所传递的信息,生成一个响应

把框架的模板渲染部分,改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
// example.com/web/front.php
 
// ...
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func('render_template', $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}

由于渲染的完成依靠的是一个外部函数(这里是render_template()),我们需要对它传入提取自URL的属性(attributes)。我们已经把它们作为render_template()的一个附加参数(argument),但不同的是,我们使用了Request类的另一个功能,被称之为attrubutes(属性):Request属性是添加“和请求有关、但不直接同HTTP请求数据相关”的附加信息的一种方式。

现在你可以创建render_template()函数了,也就是一个通用的控制器,在没有特殊逻辑时渲染模板。保持模板同先前一样,request attributes在模板被渲染之前已经被提取出来:

1
2
3
4
5
6
7
8
function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    return new Response(ob_get_clean());
}

由于render_template被用于PHP的call_user_func()函数,我们可以用任何有效的PHP callbacks来替换它。这令我们可以把一个函数,一个匿名函数或类中的一个方法作为一个控制器...你想选哪个都行。

作为一个约定,对于每个路由来说,其关联的控制器被配置在_controller这个路由属性中(route attribute):

1
2
3
4
5
6
7
8
9
10
11
12
13
$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => 'render_template',
)));
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}

现在,一个路由可以与任何控制器进行关联,当然在控制器里,你还是可以使用render_template()来渲染模板:

1
2
3
4
5
6
$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => function ($request) {
        return render_template($request);
    }
)));

这样一来灵活度更高,因为你可以在后面改变Response对象,甚至可以传递附加参数到模板中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => function ($request) {
        // $foo will be available in the template
        // $foo 可用于模板中
        $request->attributes->set('foo', 'bar');
 
        $response = render_template($request);
 
        // change some header 修改头信息
        $response->headers->set('Content-Type', 'text/plain');
 
        return $response;
    }
)));

以下是我们更新过的改进版本框架:

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
28
29
30
31
32
33
// 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;
 
function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    return new Response(ob_get_clean());
}
 
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 
$response->send();

为了庆祝新框架的诞生,我们再创建一个全新的程序,它需要一些逻辑。我们的程序有一个页面,可以告之一个给定的年份是否为闰年。当请求/is_leap_year时,你可以得到当前年份的答案,但你也可以指定一个年份如/is_leap_year/2009。基本上,框架不需要改变什么,直接创建一个新的app.php文件即可:

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
// example.com/src/app.php
use Symfony\Component\Routing;
use Symfony\Component\HttpFoundation\Response;
 
function is_leap_year($year = null) {
    if (null === $year) {
        $year = date('Y');
    }
 
    return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100);
}
 
$routes = new Routing\RouteCollection();
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => function ($request) {
        if (is_leap_year($request->attributes->get('year'))) {
            return new Response('Yep, this is a leap year!');
        }
 
        return new Response('Nope, this is not a leap year.');
    }
)));
 
return $routes;

当给定年份是闰年时,is_leap_year()函数返回true,否则是false。如果年份是null,当前年份被使用。控制器是很简单的:它从request attributes中拿到年份,然后传给is_leap_year()函数,再根据返回值来创建一个Response对象。

一如往常,你可以决定止步于此,来使用当前的框架:它差不多已经够你创建简单网站用了,像是那些精美的one-page单页website,乃至其他一些。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)