控制器

3.3 version
维护中的版本

控制器是一个你创建的php函数,它能够获取http请求信息并构建和返回一个http响应(作为Symfony的Response对象),Response可能是一个html页面、xml文档、一个序列化的json数组、图像、重定向、404错误或者一些其他你能够想像的。控制器包含了你应用程序需要渲染页面的任何逻辑。

看一下symfony简单的控制器。下面控制器将输出 hello word

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\Response;
 
public function helloAction()
{
    return new Response('Hello world!');
}

控制器的目标都是相同的:创建并返回一个Response对象。在这个过程中,它可能会从请求中读取信息,加载数据库资源,发送邮件,在用户session中设置信息。但是所有情况下,控制器将最终返回 Response 对象给客户端。

没有什么神奇的不用担心还有别的要求!下面是一些常见的例子:

  • 控制器A准备了一个首页上的Response对象。-

  • 控制器B从请求中读取{slug}参数,从数据库中载入一条博文,并创建一个显示该博文的Response对象。如果{slug}不能被数据库中检索到,那么控制器将创建并返回一个带404状态码的Response对象。

  • 控制器C处理关于联系人的表单提交。它从请求中读取表单信息,将联系人信息存入数据库并发送包含联系人信息的电子邮件给网站管理员。最后,它创建一个Response对象将用户的浏览器重定向到联系人表单的“感谢”页面。

请求、控制器、响应的生命周期 

symfony处理的每一个请求都会有相同的生命周期。框架会负责把很多重复的任务用一个控制器最终执行,控制器执行你自定义的应用代码:

  1. 每个请求都被单个前端控制器(如app.php生产环境 或app_dev.php开发环境)文件处理,前端控制器负责引导框架;

  2. 前端控制器的唯一工作是去初始化Symfony引擎(调用Kernel)并传入一个Request对象来让它处理。

  3. Symfony核心要求路由器去检查这个请求;

  4. 路由查看并匹配请求信息,并将其指向一个特定的路由,该路由决定调用哪个控制器;

  5. 执行控制器,控制器中的代码将创建并返回一个Response对象;

  6. HTTP头和Response对象的内容将发回客户端。

创建控制器与创建页面一样方便,同时映射一个URI到该控制器。

http-xkcd-request

虽然名称相似,但前端控制器与我们在本章节所说的控制器是不同的,前端控制器是你web/目录中的一个PHP小文件,所有的请求都直接经过它。一个典型的应用程序将有一个用于生产的前端控制器(如app.php)和一个用于开发的前端控制器(如app_dev.php)。你可以永远不需要去对前端控制器编辑、查看或者有所担心。本章的“控制器类”用一种方便的方法组织各自的“controllers”,也被称为actions,它们都在一个类里(如,updateAction(), deleteAction(), 等)。所以,在控制器类里一个控制器就是一个方法。它们会持有你创建的代码,并返回Response响应对象。

一个简单的控制器 

虽然一个控制器可以是任何的可被调用的PHP(函数、对象的方法或Closure),在Symfony,控制器通常是在控制器类中的一个方法,控制器也常被称为action:

1
2
3
4
5
6
7
8
9
10
11
12
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
 
use Symfony\Component\HttpFoundation\Response;
 
class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

这里面的控制器是indexAction方法,它隶属于一个控制器类HelloController

这个控制器非常的简单:

  • 第2行:Symfony利用php命名空间函数去命名整个控制器类

  • 第4行:Symfony充分利用了PHP5.3的名称空间的功能:use关键字导入Response类,是我们控制器必须返回的;

  • 第6行:类名是一个串联的控制器类名称(例如hello)加上Controller关键字。这是一个约定,为控制器提供一致性,并允许它们引用控制器名称(例如hello)作为路由配置。

  • 第8行:在控制器类中的每个action都有着后缀Action,并且这个action名(index)被引用到路由配置文件中。在下一节中,我们将使用路由映射一个URI到该action,并展示如何将路由占位符({name})变成action的参数($name);

  • 第10行:控制器创建并返回一个Response对象。

将URI映射到控制器 

我们的新控制器返回一个简单的HTML页。为了能够在指定的URI中渲染该控制器,我们需要为它创建一个路由。 我们将在路由章节中讨论路由组件的细节,但现在我们为我们的控制器创建一个简单路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
 
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 
class HelloController
{
    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}
1
2
3
4
5
# app/config/routing.yml
hello:
    path:      /hello/{name}
    # uses a special syntax to point to the controller - see note below
    defaults:  { _controller: AppBundle:Hello:index }
1
2
3
4
5
6
7
8
9
10
11
12
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">
 
    <route id="hello" path="/hello/{name}">
        <!-- uses a special syntax to point to the controller - see note below -->
        <default key="_controller">AppBundle:Hello:index</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
11
// app/config/routing.php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
 
$collection = new RouteCollection();
$collection->add('hello', new Route('/hello/{name}', array(
    // uses a special syntax to point to the controller - see note below
    '_controller' => 'AppBundle:Hello:index',
)));
 
return $collection;

现在,你来到 /hello/ryan(例如,如果你使用内置的web服务http://localhost:8000/hello/ryan),那么它就会执行HelloController::indexAction()控制器,并且将ryan赋给$name变量。创建这样一个页面就能够让路由跟控制器做简单的关联。

简单吧?

AppBundle:Hello:index控制器语法

如果你是使用YAML或者XML格式,你给你的控制器使用的一个特定快捷语法被称为逻辑控制器名称,例如AppBundle:Hello:index。更多关于控制器格式的信息,请阅读路由器章节的: 控制器命名模式

把路由参数传入控制器 

我们现在已经知道路由指向AppBundle中的HelloController::indexAction()方法。还有更有趣的就是控制器方法的参数传递:

1
2
3
4
5
6
7
8
9
10
11
// src/AppBundle/Controller/HelloController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 
/**
 * @Route("/hello/{name}", name="hello")
 */
public function indexAction($name)
{
    // ...
}

控制器有个参数$name,对应所匹配路由的{name}参数(如果你访问/hello/ryan, 在本例中是ryan)。实际上当执行你的控制器时,Symfony在所匹配路由中匹配带参数控制器中的每个参数。所以这个{name}值被传入到$name。只需要确保占位符的名称和参数名称一样就行。

以下是更有趣的例子,这里的控制器有两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Controller/HelloController.php
// ...
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 
class HelloController
{
    /**
     * @Route("/hello/{firstName}/{lastName}", name="hello")
     */
    public function indexAction($firstName, $lastName)
    {
        // ...
    }
}
1
2
3
4
# app/config/routing.yml
hello:
    path:      /hello/{firstName}/{lastName}
    defaults:  { _controller: AppBundle:Hello:index }
1
2
3
4
5
6
7
8
9
10
11
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">
 
    <route id="hello" path="/hello/{firstName}/{lastName}">
        <default key="_controller">AppBundle:Hello:index</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
// app/config/routing.php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
 
$collection = new RouteCollection();
$collection->add('hello', new Route('/hello/{firstName}/{lastName}', array(
    '_controller' => 'AppBundle:Hello:index',
)));
 
return $collection;

将路由参数映射到控制器参数是十分容易和灵活的。在你开发时请遵循以下思路:

1. 控制器参数的顺序无关紧要

Symfony可以根据路由参数名匹配控制器方法参数。换句话说,它可以实现last_name参数与$last_name参数的匹配。控制器可以在随意排列参数的情况下正常工作。

1
2
3
4
public function indexAction($lastName, $firstName)
{
    // ...
}

2.控制器所需参数必须匹配路由参数

下面会抛出一个运行时异常(RuntimeException),因为在路由定义中没有foo参数:

1
2
3
4
public function indexAction($firstName, $lastName, $foo)
{
    // ...
}

如果参数是可选的,那该多好。下面的例子不会抛出异常:

1
2
3
4
public function indexAction($firstName, $lastName, $foo = 'bar')
{
    // ...
}

3.不是所有的路由参数都需要在控制器上有响应参数的

如果,举个例子,last_name对你控制器不是很重要的话,你可以完全忽略掉它:

1
2
3
4
public function indexAction($firstName)
{
    // ...
}

你也可以从你的路由器传入其他的参数到你的控制器参数。请看 如何从路由向控制器传递额外的信息

Controller基类 

出于方便的考虑,Symfony提供了一个可选的Controller基类。如果你继承它,它不会改变你控制器的任何工作原理,而且你还能够很容易的继承一些帮助方法和服务容器(可看,下面的访问其他容器):允许你在系统中访问每一个有用的对象,类似一个数组对象一样。这些有用的对象被称为服务,并且symfony附带这些服务对象,可以渲染模板,还可以记录日志信息等。

在顶部使用use语句添加Controller类,然后修改HelloController去继承它。如下所示:

1
2
3
4
5
6
7
8
9
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class HelloController extends Controller
{
    // ...
}

而无论你是否使用Controller基类,这些帮助方法只是让你可以方便地使用Symfony的核心功能。其实查看核心功能的最好方式就是看Controller类本身。

如果你想了解没有继承controller基类控制器是如何运作的,可以查看 如何把控制器定义为服务。这是可选的,但它可以让你精确控制的更多的“对象/依赖项",注入到你的控制器。

生成URL 

generateUrl()能够生成一个URL给路由器的辅助方法。

重定向 

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

1
2
3
4
5
6
7
public function indexAction()
{
    return $this->redirectToRoute('homepage');
 
    // redirectToRoute is equivalent to using redirect() and generateUrl() together:
    // return $this->redirect($this->generateUrl('homepage'));
}

默认情况下,redirectToRoute()方法执行302(临时)重定向。如果要执行301(永久)重定向,请修改第2个参数:

1
2
3
4
public function indexAction()
{
    return $this->redirectToRoute('homepage', array(), 301);
}

从定向到外部网站,使用redirect()并传入外部URL:

1
2
3
4
public function indexAction()
{
    return $this->redirect('http://symfony.com/doc');
}

更多细节,请参考框架起步之路由

比创建一个专门从事重定向用户的Response对象来说 redirectToRoute()方法是个简单的捷径,它相当于:

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\RedirectResponse;
 
public function indexAction()
{
    return new RedirectResponse($this->generateUrl('homepage'));
}

渲染模板 

如果你使用HTML,你应该想要去渲染一个模板。render()方法可以用来渲染模板并可以把输出的内容放到你的Response 对象:

1
2
// renders app/Resources/views/hello/index.html.twig
return $this->render('hello/index.html.twig', array('name' => $name));

模板也可以防止在更深层次的子目录。但应该避免创建不必要的深层结构:

1
2
3
4
// renders app/Resources/views/hello/greetings/index.html.twig
return $this->render('hello/greetings/index.html.twig', array(
    'name' => $name
));

模板可以在任何格式的文件中以一种通用的方式去渲染内容。虽然在大多数情况下,你使用模板来渲染HTML内容,模板也可以很容易地生成JavaScript,CSS,XML或者你能想到的任何其他格式。要了解如何使不同的模板格式,参考创建并使用模板中的“模板格式”。

模板的命名模式

你也可以把模板放在一个bundle的Resources/views目录下并引用它们的特殊快捷语法,例如@App/Hello/index.html.twig 或者 @App/layout.html.twig。这些将分别存放在bundle的Resources/views/Hello/index.html.twigResources/views/layout.html.twig

访问其他服务 

Symfony塞了很多有用对象,成为服务。这些服务用于呈现模板,发送电子邮件,查询数据库,以及你能够想到的任何其他的”工作“。当你安装一个新的bundle,它也可能会带来更多的服务。

当继承controller基类后,你可以通过get()方法访问任何Symfony的服务。下面列举了一些常见服务:

1
2
3
4
5
$templating = $this->get('templating');
 
$router = $this->get('router');
 
$mailer = $this->get('mailer');

到底存在哪些服务?我想要看所有的服务,请使用debug:container命令行查看:

$ php bin/console debug:container

更多信息请看 服务容器

管理错误和404页面 

如果有些动作没找到,将返回一个404响应。为此,你需要抛出一个异常。如果你继承了基础的Controller类,你可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
public function indexAction()
{
    // retrieve the object from database
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('The product does not exist');
    }
 
    return $this->render(...);
}

createNotFoundException() 方法创建了一个特殊的NotFoundHttpException对象,来触发symfony内部的http的404响应。

当然,你也可以自由地抛出你控制器中的任何Exception类,Symfony将自动返回HTTP响应代码500。

1
throw new \Exception('Something went wrong!');

在每个示例中,一个带格式的错误页被显示给最终用户,而一个全是错误的调试页会被显示给开发者(当在调试模式app_dev.php查看该页时 - 可查看 配置Symfony(和环境))。

这些错误页都是可以自定义的。要想知道更多请阅读“如何自定义错误页”。

Request对象作为一个控制器参数 

如果你需要获取查询参数,抓取请求头或者获得一个上传文件?这些信息都存储在Symfony的Request对象。在你的控制器里获取它们,只需要添加Request对象作为一个参数并强制类型为Request类:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Request;
 
public function indexAction($firstName, $lastName, Request $request)
{
    $page = $request->query->get('page', 1);
 
    // ...
}

管理Session 

Symfony提供了一个好用的Session对象,它能够存储有关用户的信息(它可以是使用浏览器的人、bot或Web服务)之间的请求。默认情况下,Symfony通过使用PHP的原生Session来保存cookie中的属性。

去获取这个session,需要调用Request 对象的getSession()方法。这个方法会返回一个SessionInterface,它用最简单的方法来存储和抓取session的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\HttpFoundation\Request;
 
public function indexAction(Request $request)
{
    $session = $request->getSession();
 
    // store an attribute for reuse during a later user request
    $session->set('foo', 'bar');
 
    // get the attribute set by another controller in another request
    $foobar = $session->get('foobar');
 
    // use a default value if the attribute doesn't exist
    $filters = $session->get('filters', array());
}

这些属性将会在该用户session的有效期内保留。

Flash Message 

你也可以在用户session中存储一些指定的消息,这个消息被称为“flash message”(消息条子)。证据规定,flash消息只能够使用一次:当你取回它们的时候它们会自动的消失。这种特性使得“flash”消息特别适合存储用户通知。

让我们看看我们处理表单提交的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\HttpFoundation\Request;
 
public function updateAction(Request $request)
{
    $form = $this->createForm(...);
 
    $form->handleRequest($request);
 
    if ($form->isValid()) {
        // do some sort of processing
 
        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
 
        // $this->addFlash is equivalent to $this->get('session')->getFlashBag()->add
 
        return $this->redirectToRoute(...);
    }
 
    return $this->render(...);
}

在处理请求之后,控制器设置了一个名为notice的flash消息,然后重定向。名称(notice)并不重要 – 它就是一个确定消息的识别符。

接下来是模板(或者是更好的,在你的基础布局模板),从session中读取每一条信息:

1
2
3
4
5
{% for flash_message in app.session.flashBag.get('notice') %}
    <div class="flash-notice">
        {{ flash_message }}
    </div>
{% endfor %}
1
2
3
4
5
<?php foreach ($view['session']->getFlash('notice') as $message): ?>
    <div class="flash-notice">
        <?php echo "<div class='flash-error'>$message</div>" ?>
    </div>
<?php endforeach ?>

通常使用的noticewarningerror作为不同类型提示信息的键,但是你可以使用任何你需要的键。

你可以使用peek()方法来获取消息,它可以让消息保存住.

请求和响应对象 

正如前面所提到的,框架的Request 对象会作为控制器的参数传入并强制指定数据类型为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
use Symfony\Component\HttpFoundation\Request;
 
public function indexAction(Request $request)
{
    $request->isXmlHttpRequest(); // is it an Ajax request?
 
    $request->getPreferredLanguage(array('en', 'fr'));
 
    // retrieve GET and POST variables respectively
    $request->query->get('page');
    $request->request->get('page');
 
    // retrieve SERVER variables
    $request->server->get('HTTP_HOST');
 
    // retrieves an instance of UploadedFile identified by foo
    $request->files->get('foo');
 
    // retrieve a COOKIE value
    $request->cookies->get('PHPSESSID');
 
    // retrieve an HTTP request header, with normalized, lowercase keys
    $request->headers->get('host');
    $request->headers->get('content_type');
}

这个Request类有一些公共的属性和方法,它们能够返回任何你需要的请求信息。

Request一样,Response对象也有一个公共的headers属性。它是一个ResponseHeaderBag它有一些不错的方法来getting和setting响应头。头名称的规范化使得 Content-Type等于content-type甚至等于content_type,它们都是相同的。

对于控制器,唯一的要求就是返回一个Response对象。Response类是一个PHP对于HTTP响应的一个抽象,一个基于文本的消息填充HTTP头,其内容发返客户端:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Response;
 
// create a simple Response with a 200 status code (the default)
$response = new Response('Hello '.$name, Response::HTTP_OK);
 
// create a CSS-response with a 200 status code
$response = new Response('<style> ... </style>');
$response->headers->set('Content-Type', 'text/css');

也有一些特殊的类能够简化某种响应:

JSON Helper 

3.1 json() helper从symfony3.1开始被引入。

返回JSON类型在基于API的应用程序中是日益流行的。出于这个原因,控制器基类定义了json()方法,来创建一个JsonResponse 并自动编码给定的内容:

1
2
3
4
5
6
7
8
9
// ...
public function indexAction()
{
    // returns '{"username":"jane.doe"}' and sets the proper Content-Type header
    return $this->json(array('username' => 'jane.doe'));
 
    // the shortcut defines three optional arguments
    // return $this->json($data, $status = 200, $headers = array(), $context = array());
}

如果Serializer服务在你的应用程序中启用,那么内容传递到json()就会自动编码。否则,你将要使用json_encode函数。

你现在已经了解了Symfony RequestResponse 对象的基础,你还可以参考HttpFoundation组件以了解更多。

创建静态页面 

你可以在没有控制器的情况下创建一个静态页面(只需要一个路由和模板)。参考 不使用自定义控制器时如何渲染模板

总结 

当你创建了一个页面,你需要在页面中写一些业务逻辑的代码。在symfony中,这些就是控制器,它是一个能够做任何事情的php函数,目的是把最终的Response对象返回给用户。

而且你能够继承Controller基类,使工作变得轻松。例如,你不用把html代码写到控制器,您可以使用render()来渲染模板,并返回模板的内容。

在其他章节中,你会看到控制器是如何使用从数据库中读取对象和进行持久化的,处理表单提交,操作缓存以及更多。

Keep Going 

接下来,集中学习使用Twig渲染模板

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

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