在Symfony 2控制器中抽象通用功能的正确方法是什么


What is the proper way to abstract common functionality in Symfony 2 controllers

我们有一个相当大的symfony2代码库。一般来说,我们的控制器操作看起来像

public function landingPageAction(Request $request) {
 //do stuff      
 return $this->render("view_to_render", $template_data);
}

我们有两个在所有控制器之间非常通用的功能:

  1. 我们倾向于将控制器级别的模板参数传递给特定控制器中的所有操作——让我们称之为"默认参数"
  2. 我们在每个Action的末尾设置HTTP缓存头

可以理解的是,我们想把这种逻辑抽象掉。在这样做的过程中,我们提出了两种方法。我们不确定哪种方法更好,无论是在通用OO和SOLID原则方面,还是在性能和SF2建议如何完成方面。

这两种方法都依赖于让控制器扩展一个接口,该接口指示控制器是否具有"默认参数"(稍后我们还考虑添加可缓存接口)

use Symfony'Component'HttpFoundation'Request;
interface InjectDefaultTemplateVariablesController {
public function getDefaultTemplateVariables(Request $request);
}

方法1

这种方法是基于事件的。我们定义了一个对象,它将存储我们的模板变量,以及(未来)缓存指标

class TemplateVariables {
protected $template_name;
protected $template_data;
public function __construct($template_name, $template_data) {
    $this->template_name = $template_name;
    $this->template_data = $template_data;
}
/**
 * @param mixed $template_data
 * @return $this
 */
public function setTemplateData($template_data) {
    $this->template_data = $template_data;
    return $this;
}
/**
 * @return mixed
 */
public function getTemplateData() {
    return $this->template_data;
}
/**
 * @param mixed $template_name
 * @return $this
 */
public function setTemplateName($template_name) {
    $this->template_name = $template_name;
    return $this;
}
/**
 * @return mixed
 */
public function getTemplateName() {
    return $this->template_name;
}
}

我们还定义了将在渲染时触发并调用视图的事件

class InjectDefaultTemplateVariablesControllerEventListener {
/** @var DelegatingEngine */
private $templating;
private $default_template_variables;
public function __construct($templating) {
    $this->templating = $templating;
}
public function onKernelController(FilterControllerEvent $event) {
    $controller = $event->getController();
    if (!is_array($controller)) {
        return;
    }
    if ($controller[0] instanceof InjectDefaultTemplateVariablesController) {
        $this->default_template_variables = $controller[0]->getDefaultTemplateVariables($event->getRequest());
    }
}
public function onKernelView(GetResponseForControllerResultEvent $event) {
    $controller_data = $event->getControllerResult();
    if ($controller_data instanceof TemplateVariables) {
        $template_data = (array)$controller_data->getTemplateData();
        $template_data = array_merge($this->default_template_variables, $template_data);
        $event->setResponse($this->templating->renderResponse($controller_data->getTemplateName(), $template_data));
    }
}
} 

最后,我们的行动现在变成了

public function landingPageAction(Request $request) {
 //do stuff      
 return new TemplateVariables("view_to_render", $template_data);
}

方法2

这种方法是基于将公共逻辑放入BaseController中,其他每个控制器都继承该BaseController。我们仍然保留让子控制器也扩展接口的方法,以防他们想要使用"默认参数"。

以下是基本控制器中的新方法,用于确定是否需要将"默认参数"与特定模板参数合并。稍后,此方法还将使用ttl参数处理缓存标头。

public function renderWithDefaultsAndCache($view, array $parameters = array(), Response $response = null, $ttl = null)
{
  $default_template_variables = array(); 
  if ($this instanceof InjectDefaultTemplateVariablesController ) {
    $default_template_variables = $this->getDefaultTemplateVariables();
  }
  $template_data = array_merge($default_template_variables, $parameters);
  return $this->render($view, $template_data, $response);
 }

行动现在变成

public function landingPageAction(Request $request) {
 //do stuff      
 return $this->renderWithDefaultsAndCache("view_to_render", $template_data);
}

讨论

到目前为止,第一种方法的主要论点是,它遵循SOLID原则,更容易扩展——如果要添加更常见的逻辑,它可以直接放入事件监听器中,而不会影响控制器。

第二种方法的主要论点是,我们试图抽象掉的逻辑实际上属于控制器,而不是外部事件。此外,还有人担心以这种方式使用事件会导致性能不佳。

如果能听到专家们关于哪种方法更好,或者可能提出我们错过的第三种方法,我们将不胜感激。

谢谢!

首先,我绝不声称自己是Symfony 2架构专家。

我有一个比赛时间表程序,它输出许多不同类型的时间表(公共、球队、裁判等)。不同的时间表都是相似的,因为它们处理一系列比赛,但在细节上有所不同。时间表需要以各种格式(html、pdf、xls等)显示。我还希望能够进一步调整个人锦标赛的内容。

我最初使用了你的第二种方法,创建了一个ScheduleBaseController,然后从中派生出各种单独的时间表控制器。它运行不好。我试图抽象通用功能,但时间表的不同程度使得通用功能变得复杂且难以更新。

因此,我采用了一种与您的方法非常相似的事件驱动方法。为了回答您的一个问题,添加一些事件侦听器不会对性能产生任何明显影响。

我没有把重点放在模板数据上,而是创建了一个我称之为"行动模型"的东西。动作模型负责根据请求参数加载游戏,并(在某些情况下)根据发布的数据更新游戏本身。

动作模型在Controller事件侦听器中创建,存储在请求对象中,然后作为参数传递给控制器的动作方法。

// KernelEvents::CONTROLLER listener
$modelFactoryServiceId = $request->attributes->get('_model');    
$modelFactory = $this->container->get($modelFactoryServiceId);
$model = $modelFactory->create($request);
$request->attributes->set('model',$model);
// Controller action
public function action($request,$model)
{
    // do stuff
    // No template processing at all, just return null
    return null;
}
// KernelEvents::VIEW listener
$model = $request->attributes->get('model')
$response = $view->renderResponse($model);

因此,控制器主要负责表单内容。如果需要,它可以从模型中获取数据,但让模型处理大多数与数据相关的东西。控制器根本不做模板处理。它只是返回null,从而启动VIEW事件进行渲染。

很多东西?当然。关键是在路线定义中把它连接起来:

// Referee Schedule Route
cerad_game__project__schedule_referee__show:
path:  /project/{_project}/schedule-referee.{_format}
defaults: 
    _controller: cerad_game__project__schedule_referee__show_controller:action
    _model:      cerad_game__project__schedule_referee__show_model_factory
    _form:       cerad_game__project__schedule_referee__show_form_factory
    _template: '@CeradGame'Project'Schedule'Referee'Show'ScheduleRefereeShowTwigPage.html.twig'
    _format:     html
    _views:
        csv:   cerad_game__project__schedule_referee__show_view_csv
        xls:   cerad_game__project__schedule_referee__show_view_xls
        html:  cerad_game__project__schedule_referee__show_view_html
requirements:
    _format:  html|csv|xls|pdf

每个部分都被分解为单独的服务,至少对我来说,这使得定制单独的部分和查看发生了什么更容易。这是一个好方法吗?我真的不知道,但它对我来说很好。