控制器是一个你创建的php函数,它能够获取http请求信息并构建和返回一个http响应(作为Symfony2的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对象将用户的浏览器重定向到联系人表单的“感谢”页面。
请求、控制器、响应的生命周期
symfony2处理的每一个请求都会有相同的生命周期。这个框架会负责把很多重复的任务用一个控制器最终执行,这个控制器执行你自定义的应用代码:
1、每个请求都被单个前端控制器(如app.php或index.php)文件处理,前端控制器负责引导框架;
2、路由查看并匹配请求信息,并将其指向一个特定的路由,该路由决定调用哪个控制器;
3、执行控制器,控制器中的代码将创建并返回一个Response对象;
4、HTTP头和Response对象的内容将发回客户端。
创建控制器与创建页面一样方便,同时映射一个URI到该控制器。
虽然名称相似,但前端控制器与我们在本章节所说的控制器是不同的,前端控制器是你web目录中的一个PHP小文件,所有的请求都直接经过它。一个典型的应用程序将有一个用于生产的前端控制器(如app.php)和一个用于开发的前端控制器(如app_dev.php)。你可以永远不需要对前端控制器进行编辑、查看和担心。
一个简单的控制器
虽然一个控制器可以是任何的可被调用的PHP(函数、对象的方法或Closure),在Symfony2,控制器通常是在控制器对象中的一个方法,控制器也常被称为action。
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Component\HttpFoundation\Response; class HelloController { public function indexAction($name) { return new Response('<html><body>Hello '.$name.'!</body></html>'); } } |
注意:控制器是indexAction方法,它隶属于一个控制器类(HelloController)。不要对名称感到困惑:控制器类只是简单将几个控制器集中在一起的。通常情况下,控制器类将放置多个控制器(如updateAction、deleteAction等),一个控制器有时也会被当作一个action被提及。
这个控制器相当的明确:
- 第4行:Symfony2充分利用了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 |
# app/config/routing.yml hello: path: /hello/{name} defaults: { _controller: AcmeHelloBundle:Hello:index } |
如果我们在浏览器中输入 /hello/ryan 那么他就会执行HelloController::indexAction()控制器,并且将ryan赋给$name变量。创建这样一个页面就能够让路由跟控制器做简单的关联。
注意,这个是用于指向控制器的语法:AcmeHelloBundle:Hello:index。Symfony2使用一个灵活的字符串注释来指向不同的控制器。这是Symfony2最通用的语法,告诉AcmeHelloBundle在内部去查找一个名为HelloController的控制器类,然后执行方法indexAction()。
关于指向不同控制器的字符串格式用法的更多细节,请参见控制器命名模式。
这个例子的路由配置目录在app/config/ 下。有更好的方式就是你可以将每个bundle的路由放到各自的资源下,更多信息可查看 引入外部路由资源。
你也能了解和学习更多的路由系统在 Routing chapter.
把路由参数传入控制器
我们现在已经知道_controller的参数 AcmeHelloBundle:hello:index 指向AcmeHelloBundle 中的HelloController::indexAction()方法,这里更有趣的是发送给该方法的参数:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class HelloController extends Controller { public function indexAction($name) { // ... } } |
控制器有个参数$name,对应所匹配路由的{name}参数(在本例中是ryan)。实际上当执行你的控制器时,Symfony2在所匹配路由中匹配带参数控制器中的每个参数。以下所示:
1 2 3 4 |
# app/config/routing.yml hello: path: /hello/{firstName}/{lastName} defaults: { _controller: AcmeHelloBundle:Hello:index, color: green } |
因此控制器也需要多个参数:
1 2 3 4 |
public function indexAction($firstName, $lastName, $color) { // ... } |
注意两个占位符({{first_name}}、{{last_name}})和默认color变量做为控制器的参数。当路由匹配时,占位符变量与 defaults 合并成一个数组,并且用于你的控制器。
将路由参数映射到控制器参数是十分容易和灵活的。在你开发时请遵循以下思路:
1. 控制器参数的顺序无关紧要
Symfony2可以根据路由参数名匹配控制器方法参数的特征。换句话说,它可以实现last_name参数与$last_name参数的匹配。控制器可以在随意排列参数的情况下正常工作。
1 2 3 4 |
public function indexAction($lastName, $color, $firstName) { // ... } |
2.控制器所需参数必须匹配路由参数
下面会抛出一个运行时异常(RuntimeException),因为在路由定义中没有foo参数
1 2 3 4 |
public function indexAction($firstName, $lastName, $color, $foo) { // ... } |
3.然而如果设置该参数可选是完全可行的。下面的例子不会抛出异常:
1 2 3 4 |
public function indexAction($firstName, $lastName, $color, $foo = 'bar') { // ... } |
4.不是所有的路由参数都需要在控制器上有相应参数的
如果,举个例子,last_name对你控制器不是很重要的话,你可以完全忽略掉它:
1 2 3 4 |
public function indexAction($firstName, $color) { // ... } |
每条路由也都有一个特殊的_route参数,该参数匹配路由的名字(如hello)。虽然不常用,但它一样也可以作为控制器的参数。
Request作为一个控制器的参数
为了方便起见,你可以在symfony中把request对象当作一个参数传到你的Controller里。当你处理表单的时候这是非常方便的,例如:
1 2 3 4 5 6 7 8 9 |
use Symfony\Component\HttpFoundation\Request; public function updateAction(Request $request) { $form = $this->createForm(...); $form->handleRequest($request); // ... } |
创建静态页面
有时你也可只创建一个静态页面不用创建控制器(仅仅一个路由和模板即可)。
如何使用,请查看 没有自定义控制器如何渲染一个模板。
基本控制器类
出于方便的考虑,Symfony2提供了一个Controller基类,以帮助实现常用的一些控制器任务,并站你的控制器类能够访问所需的资源。通过继承该类,你可以利用其中的一些帮手方法。
在顶部使用use语句添加Controller类,然后修改HelloController去继承它。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/Acme/HelloBundle/Controller/HelloController.php namespace Acme\HelloBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; class HelloController extends Controller { public function indexAction($name) { return new Response('<html><body>Hello '.$name.'!</body></html>'); } } |
到目前为止,继承Controller类并没有改变任何东西。在下一节中,我们将使用几个该基类中的助手方法。这些方法只是让你可以方便地使用Symfony2的核心功能,而无论你是否使用Controller基类。其实查看核心功能的最好方式就是看Controller类本身。
在Symfony2中是否继承基类是可以选择的;它包含了非常有用的便捷方式,但并不强制使用。你也可以继承ContainerAwaer或使用Symfony\Component\DependencyInjection\ContainerAwareTrait(如果你有php5.4)。这个服务容器对象将通过container属性访问。
2.4 ContainerAwareTrait 实在symfony2.4中引入的
你还可以定义你的 Controller为一个服务。这个是可选的,但可以把你更多的控件依赖注入到你的控制器。
常用控制器任务
虽然控制器实际上可以做任何事情,但大多数控制器都是一遍又一遍地执行同一个基本任务。这些任务包括重定向、转发、渲染模板和访问核心服务,这一切在Symfony2中非常便于管理。
重定向
如果你想将用户重定向到另一个页面,请使用 redirect() 方法:
1 2 3 4 |
public function indexAction() { return $this->redirect($this->generateUrl('homepage')); } |
这个generateUrl() 方法仅仅是帮助函数生成指定的URL路径,更多信息请查看路由章节。
默认情况下,redirect方法执行302(临时)重定向。如果要执行301(永久)重定向,请修改第2个参数:
1 2 3 4 |
public function indexAction() { return $this->redirect($this->generateUrl('homepage'), 301); } |
比创建一个专门从事重定向用户的
Response对象来说
这个redirect()方法
是个简单的捷径,它相当于:
123 use Symfony\Component\HttpFoundation\RedirectResponse;return new RedirectResponse($this->generateUrl('homepage'));
转发
你也可以很容易地通过forward()方法内部转发到另一个action。与重定向用户浏览器不同,它产生一个内部的子请求,并调用一个特殊的控制器。forward()方法返回Response对象,这个Response对象是控制器返回的。
1 2 3 4 5 6 7 8 9 10 11 |
public function indexAction($name) { $response = $this->forward('AcmeHelloBundle:Hello:fancy', array( 'name' => $name, 'color' => 'green', )); // ... further modify the response or return it directly return $response; } |
注意forward()方法使用与路由配置中相同的控制器字符串表示。在本例中目标控制器类将是在AcmeHelloBundle中的Hellocontroller类,发送给控制器类方法的数组做为其参数。当内嵌的控制器进入模板时使用相同的接口(参见内嵌控制器)。目标控制器访问如下所示:
1 2 3 4 |
public function fancyAction($name, $color) { // ... create and return a Response object } |
就这样,当为一个路由创建一个控制器时,fancyAction的参数顺序不是问题。Symfony2检索关键词名(如name),并将其匹配到方法参数名(如$name)。如果你改变参数的顺序,Symfony2也会将正确的值赋给每个变量。
正如其它Controller基类的方法一样,forward方法也只是Symfony2核心功能是快捷方式。转发可以通过http_kernel服务直接完成。转发返回一个Response对象。
123456789101112131415 use Symfony\Component\HttpKernel\HttpKernelInterface;$path = array('_controller' => 'AcmeHelloBundle:Hello:fancy','name' => $name,'color' => 'green',);$request = $this->container->get('request');$subRequest = $request->duplicate(array(), null, $path);$httpKernel = $this->container->get('http_kernel');$response = $httpKernel->handle($subRequest,HttpKernelInterface::SUB_REQUEST);
渲染模板
虽然不是必需的,多数控制器都会渲染一个模板,这些控制器会负责生产html(或者其他格式)。renderView()方法渲染模板并返回它的内容。来自模板的内容可以用来创建一个Response对象:
1 2 3 4 5 6 7 8 |
use Symfony\Component\HttpFoundation\Response; $content = $this->renderView( 'AcmeHelloBundle:Hello:index.html.twig', array('name' => $name) ); return new Response($content); |
这个方法甚至可以在render()方法中一步完成,返回一个Response
对象,其中包含模板的内容:
1 2 3 4 |
return $this->render( 'AcmeHelloBundle:Hello:index.html.twig', array('name' => $name) ); |
在上两个示例中,AcmeHelloBundle中的Resources/views/Hello/index.html.twig模板将被渲染。
symfony模板引擎的详细说明请看模板章节。
通过使用@Template注释,你甚至可以避免调用render方法,请查看 FrameworkExtraBundle文档 更多细节。
renderView方法是直接使用模板服务的快捷方式。也可以直接使用模板服务:
12345 $templating = $this->get('templating');$content = $templating->render('AcmeHelloBundle:Hello:index.html.twig',array('name' => $name));
可以渲染模板的更深层次的子目录,但是应该注意避免让你的目录结构进入过于精心的陷阱:
123456 $templating->render('AcmeHelloBundle:Hello/Greetings:index.html.twig',array('name' => $name));// index.html.twig found in Resources/views/Hello/Greetings// is rendered.
访问其他服务
当继承controller基类后,你可以通过get()方法访问任何Symfony2的服务。下面列举了一些常见服务:
1 2 3 4 5 |
$templating = $this->get('templating'); $router = $this->get('router'); $mailer = $this->get('mailer'); |
Symfony2可以有无数个其它的服务,也鼓励你定义自己的服务。使用 container:debug 命令能够列举所有有效的服务。
1 |
$ php app/console container:debug |
更多详细信息请查看服务容器章节。
管理错误和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响应。
当然,你也可以自由地抛出你控制器中的任何异常类,Symfony2将自动返回HTTP响应代码500。
1 |
throw new \Exception('Something went wrong!'); |
在每个示例中,一个带格式的错误页被显示给最终用户,而一个全调试错误页会被显示给开发者(当在调试模式查看该页时)。这些错误页都是可以自定义的。要想知道更多请阅读“如何自定义错误页”。
管理会话
Symfony2提供了一个好的会话对象,它能够存储有关用户的信息(它可以是使用浏览器的人、bot或Web服务)之间的请求。默认情况下,Symfony2通过使用PHP的本地会话来保存cookie中的属性。
这个会话能很容易的从任何控制器存储和取回信息。
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(); // 存储一个属性 $session->set('foo', 'bar'); // 读取一个属性 $foobar = $session->get('foobar'); // 如果属性不存在可以给他一个默认值 $filters = $session->get('filters', array()); } |
这些属性将会在该用户会话的有效期内保留。
Flash消息
你也可以为了一个额外的请求,在用户会话中保存一些小消息。这在处理表单时很有用:你想重定向并在下个请求中显示一个特定的消息。这些类型的消息被称为“Flash”消息。
让我们看看我们处理表单提交的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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->get('session')->getFlashBag()->add( 'notice', 'Your changes were saved!' ); |
在处理请求之后,控制器设置了一个名为notice的flash消息,然后重定向。这个名称(notice)并不重要 – 他就是一个确定消息类型的识别符。
在下一个action的模板中,下列代码用来渲染该消息:
1 2 3 4 5 |
{% for flashMessage in app.session.flashbag.get('notice') %} <div class="flash-notice"> {{ flashMessage }} </div> {% endfor %} |
按照设计,flash消息意味着仅存活一个请求(它们总是“稍纵即逝”)。在本例中它们被设计用来跨跃重定向。
Response对象
对于控制器,唯一的要求就是返回一个Response对象。Response类是一个PHP对于HTTP响应的一个抽象,一个基于文本的消息填充HTTP头,其内容发返客户端:
1 2 3 4 5 6 7 8 |
use Symfony\Component\HttpFoundation\Response; // 创建一个简单的202状态码响应 (默认) $response = new Response('Hello '.$name, Response::HTTP_OK); // 创建一个202状态码的JOSN响应 $response = new Response(json_encode(array('name' => $name))); $response->headers->set('Content-Type', 'application/json'); |
2.4 在symfony2.4 引入了http状态码常量。
headers属性是一个HeaderBag对象,那么他有许多有用的方法来读取和改变Response头。头名称的规范化使得 Content-Type等于content-type甚至等于content_type,他们都是相同的。
也有一些特殊的类能够简化某种响应:
- 对于JSON:这是一个JosnResponse。可查看 创建一个JOSN Response。
- 对于文件:这是 BinaryFileResponse。可查看 Serving Files。
Request(请求)对象
除了路由占位符的值,控制器也可先后获得Request对象。在框架中,如果变量是type-hinted的Request,Request 对象会注入到控制器:
1 2 3 4 5 6 7 8 9 10 11 12 |
use Symfony\Component\HttpFoundation\Request; public function indexAction(Request $request) { $request->isXmlHttpRequest(); // is it an Ajax request? $request->getPreferredLanguage(array('en', 'fr')); $request->query->get('page'); // get a $_GET parameter $request->request->get('page'); // get a $_POST parameter } |
如同Response对象一样,请求头被保存在HeaderBag对象中,并可以轻易访问到。
本知识点结束语
当你创建了一个页面,你需要在页面中写一些业务逻辑的代码。在symfony中,这些就是Controller,它是一个能够做任何事情的php函数,为了方便,他需要最终返回给用户Response对象。
而且你能够继承Controller类,使工作变得轻松。例如,你不用把html代码写到控制器,您可以使用render()方法来渲染模板,并返回模板的内容。
在其他章节中,你会看到控制器是如何使用数据库持久化和读取数据的,还有表单的提交,使用缓存和更多。