如何把控制器定义为服务

3.4 版本
维护中的版本

定义容器作为服务是不被symfony所正式推荐的。所使用他们的一些开发人员有非常具体的用例,比如 DDD(domain-driven design-[译]领域模型驱动设计)和应用程序(Hexagonal Architecture-[译]六角架构)。

控制器指南中,您已经了解了当控制器继承 Controller 基类时,如何轻松地使用一个控制器。他不仅能工作的很好,控制器也能被指定为一个服务。即使你不指定控制器作为服务,你可能看到它们会被用于一些开源Symfony Bundle,所以去了解下面两点可能是有用的。

定义控制器为服务的主要优势如下:

  • 整个控制器和任何通过服务传递给它的服务都可以通过服务容器配置来修改。当开发可重用bundle时他很有用;

  • 你的控制器更加被“沙盒”化。通过观察构造参数,很容易看到控制器可以做或者不可以做什么。

  • 因为必须手动注入依赖项,当你的控制器变大时,他会很明显(如,你有很多的构造参数)。

定义控制器为服务的主要缺点如下:

  • 它需要更多的工作来创建控制器,因为他们没有自动访问服务或基本控制器的捷径;

  • 控制器的构造函数会迅速变得极复杂,因为你必须注入他们所需要的每一个依赖;

  • 控制器代码会更加冗长,因为你不能使用该基础控制器的快捷方式并且你必须用几行代码来替换他们;

最佳实践也推荐正当的将控制器定义为服务:避免你的业务逻辑放置在控制器中。相反,应该让注入服务做大部分工作。

定义控制器为服务 

相反,注入服务是工作的主要内容。例如,你有简单控制器如下:

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>');
    }
}

你可以定义它作为服务如下:

1
2
3
4
# app/config/services.yml
services:
    app.hello_controller:
        class: AppBundle\Controller\HelloController
1
2
3
4
<!-- app/config/services.xml -->
<services>
    <service id="app.hello_controller" class="AppBundle\Controller\HelloController" />
</services>
1
2
3
4
5
6
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$container->setDefinition('app.hello_controller', new Definition(
    'AppBundle\Controller\HelloController'
));

引用服务 

要引用一个定义为服务的控制器,使用一个冒号(:)符号。比如,通过app.hello_controller id,转发上面服务定义的 indexAction()方法。

1
$this->forward('app.hello_controller:indexAction', array('name' => $name));

当使用这个语法时你不能丢弃方法名称的 Action 部分。

当定义路由 _controller 值的时候,你也可以通过使用相同的符号来指定路由到服务:

1
2
3
4
# app/config/routing.yml
hello:
    path:     /hello
    defaults: { _controller: app.hello_controller:indexAction }
1
2
3
4
<!-- app/config/routing.xml -->
<route id="hello" path="/hello">
    <default key="_controller">app.hello_controller:indexAction</default>
</route>
1
2
3
4
// app/config/routing.php
$collection->add('hello', new Route('/hello', array(
    '_controller' => 'app.hello_controller:indexAction',
)));

你也可以使用注释去配置,让路由使一个定义好的控制器为服务。你要确保在 @Route 注释中指定服务ID。详细内容请看 FrameworkExtraBundle文档。

如果你的控制器实现了__invoke()方法,你可以仅引用服务id(app.hello_controller

基础的控制器方法替代品 

当使用被定义为服务的控制器时,大多可能不会去继承基础的Controller类。而不是依赖于它的快捷方式方法,您将直接与您需要的服务进行交互。幸运的是,这通常是非常容易的并且这个基础的控制器类源代码在“如何执行许多常见任务”是一个很好的资源。

例如,你想渲染一个模板去替代直接创建Response对象,如果你继承symfony基础控制器代码看起来应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class HelloController extends Controller
{
    public function indexAction($name)
    {
        return $this->render(
            'AppBundle:Hello:index.html.twig',
            array('name' => $name)
        );
    }
}

如果你看symfony基础控制器类render函数源代码,你会看到这个方法实际上是用的是templating服务:

1
2
3
4
public function render($view, array $parameters = array(), Response $response = null)
{
    return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}

在一个被定义为服务的控制器中,你可以注入templating服务,并直接使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
 
class HelloController
{
    private $templating;
 
    public function __construct(EngineInterface $templating)
    {
        $this->templating = $templating;
    }
 
    public function indexAction($name)
    {
        return $this->templating->renderResponse(
            'AppBundle:Hello:index.html.twig',
            array('name' => $name)
        );
    }
}

这个服务定义也需要去修改指定的构造参数:

1
2
3
4
5
# app/config/services.yml
services:
    app.hello_controller:
        class:     AppBundle\Controller\HelloController
        arguments: ['@templating']
1
2
3
4
5
6
<!-- app/config/services.xml -->
<services>
    <service id="app.hello_controller" class="AppBundle\Controller\HelloController">
        <argument type="service" id="templating"/>
    </service>
</services>
1
2
3
4
5
6
7
8
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('app.hello_controller', new Definition(
    'AppBundle\Controller\HelloController',
    array(new Reference('templating'))
));

而不是从容器中获取templating服务,你只需要把你需要的服务直接注入到控制器。

这并不意味着你不能继承你的基础控制器。舍弃标准的基础控制器是因为这些辅助(helper)方法依赖于容器,有了这些就不能被定义为服务的控制器。提取普通的代码到一个服务然后再注入是一个好主意,而不是把这些代码放到基础的控制器中让你继承。这两种方式都是可行的,确切的说,你想如何组织你的可重用代码那是你的事情。

基础的控制器方法和他们的服务替代品 

此列表解释了如何替换基础的控制器的便利方法:

createForm() (service: form.factory)

1
$formFactory->create($type, $data, $options);

createFormBuilder() (service: form.factory)

1
$formFactory->createBuilder('form', $data, $options);

createNotFoundException()

1
new NotFoundHttpException($message, $previous);

forward() (service: http_kernel)

1
2
3
4
5
6
7
use Symfony\Component\HttpKernel\HttpKernelInterface;
// ...
 
$request = ...;
$attributes = array_merge($path, array('_controller' => $controller));
$subRequest = $request->duplicate($query, null, $attributes);
$httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);

generateUrl() (service: router)

1
$router->generate($route, $params, $referenceType);

$referenceType参数必须是一个在UrlGeneratorInterface 定义的常数。

getDoctrine() (service: doctrine)

简单地注入doctrine,而不是从容器中取出它。

getUser() (service: security.token_storage)

1
2
3
4
5
$user = null;
$token = $tokenStorage->getToken();
if (null !== $token && is_object($token->getUser())) {
     $user = $token->getUser();
}

isGranted() (service: security.authorization_checker)

1
$authChecker->isGranted($attributes, $object);

redirect()

1
2
3
use Symfony\Component\HttpFoundation\RedirectResponse;
 
return new RedirectResponse($url, $status);

render() (service: templating)

1
$templating->renderResponse($view, $parameters, $response);

renderView() (service: templating)

1
$templating->render($view, $parameters);

stream() (service: templating)

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\StreamedResponse;
 
$templating = $this->templating;
$callback = function () use ($templating, $view, $parameters) {
    $templating->stream($view, $parameters);
}
 
return new StreamedResponse($callback);

getRequest 已经被弃用了。作为替代,有一个参数到你的控制器action方法他就是Request $request。参数的顺序是不重要的,但类型提示必须提供。

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

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