控制器

3.4 版本
维护中的版本

看完前两部分仍然留在这里?那你已经是Symfony饭了。 抛开前方的纷扰,来看看控制器(controller)部分。

返回原始响应 

Symfony把自己定义成一个“请求-响应”(Request-Response)的框架。当用户发送请求给程序时,Symfony创建了一个Request对象来封装所有与请求有关的信息。类似的,任何一个控制器中的任何action在执行完毕后的输出结果,也将被制成一个Response对象,供Symfony框架使用,来生成HTML内容返回给用户。

到目前为止,本教程中所有的action都是使用$this->render()这个“控制器快捷方式”(译注:即controller封装的简便方法)来输出已经渲染完毕的响应结果。若你需要,还可以打出一个原始的Response对象,用以返回文本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
 
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return new Response('Welcome to Symfony!');
    }
}

路由参数 

多数情况下,程序页面的URL中要包含变量。例如你建立一个博客,URL可用来显示特定文章,链接中可能会有标题(译注:英文标题的slug)或是其他的唯一识别符(比如文章在数据表中的id),这样程序就可以知道到底要显示哪篇文章。

在Symfony中,路由中的变量部分,要被大括号包起来(比如/blog/read/{article_title}/)。每个变量都要被赋予一个唯一的名字,以便后续在controller中被用到时,能够取出相应变量的值。

我们来创建一个全新action并带有路由变量,用来演示这个功能。打开src/AppBundle/Controller/DefaultController.php文件并添加一个helloAction()新方法,包含以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class DefaultController extends Controller
{
    // ...
 
    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function helloAction($name)
    {
        return $this->render('default/hello.html.twig', array(
            'name' => $name
        ));
    }
}

打开浏览器访问http://localhost:8000/hello/fabien链接,查看这个新action的执行结果。与预期不同,你看到的是报错信息。也许你已猜到了,错误原因是因为我们试图渲染模板default/hello.html.twig,可它并不存在。

建立一个新的app/Resources/views/default/hello.html.twig模板,包含以下代码:

1
2
3
4
5
6
{# app/Resources/views/default/hello.html.twig #}
{% extends 'base.html.twig' %}
 
{% block body %}
    <h1>Hi {{ name }}! Welcome to Symfony!</h1>
{% endblock %}

重新访问http://localhost:8000/hello/fabien链接,你将看到模板输出的内容,里面有传递到controller中的相关信息。如果改变链接中的最后部分,例如http://localhost:8000/hello/thomas,然后刷新浏览器,你会看到页面显示了不同信息。如果去掉这最后一部分,例如http://localhost:8000/hello, Symfony将会报错,因为路由预期一个name变量,但你并没有提供给它。

使用格式 

如今,现代web程序能够传送HTML页面之外的更多内容,从RSS订阅中XML/WebService的应用,再到Ajax请求中的Json返回值,大量不同的输出格式备选。在Symfony中支持这些格式是简单直接的,多亏了特殊变量_format专门用于存储用户请求的格式信息。

调整hello路由,加上一个新的_format变量给它,并设置html为其默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Controller/DefaultController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
// ...
 
/**
 * @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, name="hello")
 */
public function helloAction($name, $_format)
{
    return $this->render('default/hello.'.$_format.'.twig', array(
        'name' => $name
    ));
}

很明显,当你支持多种请求格式时,你可以为每一种支持的格式提供一个模板。本例中,你应该建立一个新的hello.xml.twig模板:

1
2
3
4
<!-- app/Resources/views/default/hello.xml.twig -->
<hello>
    <name>{{ name }}</name>
</hello>

现在,当你浏览http://localhost:8000/hello/fabien时,你看到的是通常的HTML页面,因为html是默认格式。若你访问http://localhost:8000/hello/fabien.html的话,你仍将看到HTML页面,这次你显式地指定了html格式。最后,如果你访问http://localhost:8000/hello/fabien.xml,则将在浏览器中看到新的XML模板所生成的内容。

这就是格式的全部。对于标准格式来说,Symfony自动为响应(response)挑选最合适的Content-Type头信息。如果想对指定的action限制其输出格式,使用@Route()注释的requirements选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/AppBundle/Controller/DefaultController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
// ...
 
/**
 * @Route("/hello/{name}.{_format}",
 *     defaults = {"_format"="html"},
 *     requirements = { "_format" = "html|xml|json" },
 *     name = "hello"
 * )
 */
public function helloAction($name, $_format)
{
    return $this->render('default/hello.'.$_format.'.twig', array(
        'name' => $name
    ));
}

hello action将匹配像是/hello/fabien.xml/hello/fabien.json这样的URL,但对于/hello/fabien.js这种就会报错,因为此时_format变量的值与它所需要的不一样。

重定向 

如果你想把用户重定向到另外一个页面,使用redirectToRoute()方法:

1
2
3
4
5
6
7
8
9
10
11
// src/AppBundle/Controller/DefaultController.php
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->redirectToRoute('hello', array('name' => 'Fabien'));
    }
}

上面的redirectToRoute()接收的参数包括路由名称和可选的参数数组,它可以把用户重定向到由这些参数构成的路由页面。

显示报错页面 

任何网络程序在执行过程中不可避免地会报错。以404页面为例,Symfony在controller中自带了一个好用的快捷方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Controller/DefaultController.php
// ...
 
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        // ...
        throw $this->createNotFoundException();
    }
}

对于500错误,可以在controller中直接抛出PHP异常,Symfony会自动把它转换成为适当的500错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Controller/DefaultController.php
// ...
 
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        // ...
        throw new \Exception('Something went horribly wrong!');
    }
}

从Request对象中取得信息 

有时你的控制器需要用到与user请求有关的信息,例如他们使用的语言,ip地址,或是url中的参数。为了得到这些信息,添加一个Request参数到action中(译注:在controller中将Request对象注入到action中,是Symfony自动完成,毋须设置服务容器和依赖关系)。这个新参数的名字无关紧要(译注:根据约定,一般就用$request),但是它必须是Request类型(type hint)才能工作(不要忘记添加use声明,才能引用这Request类)。

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
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
 
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        // is it an Ajax request? 是个Ajax请求吗?
        $isAjax = $request->isXmlHttpRequest();
 
        // what's the preferred language of the user?
        // 用户的首选语言是什么?
        $language = $request->getPreferredLanguage(array('en', 'fr'));
 
        // get the value of a $_GET parameter 取得$_GET参数值
        $pageName = $request->query->get('page');
 
        // get the value of a $_POST parameter 取得$_POST参数值
        $pageName = $request->request->get('page');
    }
}

在模板中,你可以使用Request对象,通过特殊的app.request(这个全局变量由Symfony内部提供):

1
2
3
{{ app.request.query.get('page') }}
 
{{ app.request.request.get('page') }}

用Session存储数据 

即便HTTP协议是无状态的,Symfony仍提供了一个完美的session对象,用于描述客户端(比如,一个真人使用浏览器,或是引擎爬虫,又或是WebService)。在两次请求之间,Symfony通过PHP原生session存储了一些属性到cookie中。

从session中存取信息,在控制器中可以轻松实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\HttpFoundation\Request;
 
public function indexAction(Request $request)
{
    $session = $request->getSession();
 
    // store an attribute for reuse during a later user request
    // 存储一个session属性,以便在后面的用户请求中使用
    $session->set('foo', 'bar');
 
    // get the value of a session attribute
    // 取得session的属性值
    $foo = $session->get('foo');
 
    // use a default value if the attribute doesn't exist
    // 如果属性不存在,使用该属性的默认值
    $foo = $session->get('foo', 'default_value');
}

你可以存储 "flash messages" ,这是在下次请求成功后,可以自动在页面上消失的信息。当你要对即将跳转到另外一个页面的用户显示成功信息时这非常有用(用户被重定向之后将新页面看到信息)。

1
2
3
4
5
6
7
8
public function indexAction(Request $request)
{
    // ...
 
    // store a message for the very next request
    // 存储一个在即将到来的请求中需要使用的flash信息
    $this->addFlash('notice', 'Congratulations, your action succeeded!');
}

在模板中显示flash信息:

1
2
3
4
5
{% for flashMessage in app.session.flashBag.get('notice') %}
    <div class="flash-notice">
        {{ flashMessage }}
    </div>
{% endfor %}

控制器总结 

这就是全部了,我不确定你是否会花满10分钟。你被暂时引入到bundle中,目前你学到的所有功能都是Symfony的核心:framework bundle所提供的。多亏了bundle概念,Symfony中的一切都可以扩展或被替换。本教程的下一节,将就此话题深入。

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

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