控制器

3.3 版本
维护中的版本

Symfony遵循的编程哲学是“thin controller and fat models/瘦控制器胖模型” 。这意味着控制器中是一个thin layer,仅含有少量被整合到程序其他部分的“线索代码”(clue-code)。

根据经验,你应该遵守5-10-20原则,也就是控制器中仅定义5个变量或更少,包含10个actions或更少,每个action里面的代码是20行或更少。这并非精确的科学结论,但这有助于使你认清何时该把代码重构到控制器之外或将其设为服务。

Best Practice

Best Practice

确保你的控制器继承自FrameworkBundle的基类,并尽可能地使用annotations来配置路由、缓存、安全等任何可配置内容。

将控制器藕合到框架内核,有助于你利用其功能并提高生产力。

由于你的控制器宜精简,除了“线索代码”之外再无其他,若花费数个小时来尝试将其与框架核心解藕,并无益于持续开发。时间上的开销得不尝失。

除此之外,使用annotation方式来处理路由、缓存、安全等层面可以简化配置。你毋需浏览大量不同格式的文件(YAML、XML、PHP):所有配置都在你需要的地方显示,并且只有一种格式。

总地说,这意味着你可以激进地将业务逻辑从框架核心进行解藕,同时还可以主动将控制器和路由与框架核心藕合以便得到更多。

路由配置 

为了在控制器中加载以annotations方式定义的路由,添加下列配置到路由的主配置文件中:

1
2
3
4
# app/config/routing.yml
app:
    resource: '@AppBundle/Controller/'
    type:     annotation

这个配置将加载存放在src/AppBundle/Controller/目录下甚至子目录下的任何一个controller中的annotation路由。因此当你的程序定义了许多controller时,将其组织到子目录中是极好的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<your-project>/
├─ ...
└─ src/
   └─ AppBundle/
      ├─ ...
      └─ Controller/
         ├─ DefaultController.php
         ├─ ...
         ├─ Api/
         │  ├─ ...
         │  └─ ...
         └─ Backend/
            ├─ ...
            └─ ...

模板配置 

Best Practice

Best Practice

不要使用@Template annotation来配置controller中的模板。

@Template annotation十分有用(译注:因为省事我还是喜欢用这个),但它也带来了迷惑性。我们不认为它的好处(译注:主要是省事)能够值回迷惑性,因此推荐不使用这种方式。

多数情况下,@Template根本不带参数(译注:与action名字关联,到底是省事),这就令人们很难获知它在渲染哪个模板(译注:懂才行)。同时,它还令初学者感到混淆,因为控制器必须要返回一个Response对象(除非你使用了view层)。

控制器看上去的样子 

综合考虑以上所有,下例呈现出我们程序首页的控制器应有的样子:

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

使用参数转换(ParamConverter) 

如果你在用Doctrine,你应该选用ParamConverter来自动查询某个entity,并将其作为参数传入你的控制器。

Best Practice

Best Practice

使用ParamConverter来自动查询简单实用的Doctrine entity。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 
/**
 * @Route("/{id}", name="admin_post_show")
 */
public function showAction(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);
 
    return $this->render('admin/post/show.html.twig', array(
        'post'        => $post,
        'delete_form' => $deleteForm->createView(),
    ));
}

通常你预期一个$id参数传入showAction()。取而代之的是,创建一个新的参数($post)并应用类型提示为Post类(这是个Doctrine entity),此时ParamConverter将自动查询出一个$id属性与路由{id}值相匹配的对象。如果查不到Post会显示404页。

复杂情况 

上面示例毋需额外的配置,因为通配符的名字{id}正好匹配entity的属性名。如果不是这种情况,或者你有更复杂的逻辑,最简单的办法就是手动查询实体。在我们的程序中,我们会有CommentController存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 */
public function newAction(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneBy(array('slug' => $postSlug));
 
    if (!$post) {
        throw $this->createNotFoundException();
    }
 
    // ...
}

此时你仍可使用@ParamConverter进行配置,它是相当弹性化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
 
/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
 */
public function newAction(Request $request, Post $post)
{
    // ...
}

关键点在于:ParamConverter更加适合于简单状况。然而你要记得,手动直接查询entity同样很简单。

掌控Pre和Post 

如果你希望在控制器的执行动作到来之前或之后去执行一些代码,你应当使用EventDispatcher组件来设置前/后过滤器

Seealso

译注:事件是Symfony的三大法宝之一,威力巨大,请学者多加研读。

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

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