从请求填充实体的更好方法


Better way to populate entity from request?

我正在使用Symfony 2.1应用程序,我有很多参数通过POST请求发送,我正在寻找一种更智能的方法来获取每个请求参数并填充我的实体类。我希望避免为n请求参数编写$entity->setMyParam($my_param)表达式。例如,下面是我的实体的一个片段:

namespace Brea'ApiBundle'Entity;
use Doctrine'ORM'Mapping as ORM;
use Symfony'Component'Validator'Constraints as Assert;
/**
 * Brea'ApiBundle'Entity'Distributions
 *
 * @ORM'Table(name="distributions")
 * @ORM'Entity
 */
class Distributions
{
  /**
   * @var string $recordType
   *
   * @ORM'Column(name="record_type", type="string", nullable=false)
   * @Assert'NotBlank()
   * @Assert'Choice(choices = {"a", "b", "c", "d", "e"}, message = "Choose a valid record type")
   */
  private $recordType;
  /**
   * Set recordType
   *
   * @param string $recordType
   */
  public function setRecordType($recordType)
  {
    $this->recordType = $recordType;
  }
  /**
   * Get recordType
   *
   * @return string 
   */
  public function getRecordType()
  {
    return $this->recordType;
  }
}

我的控制器尝试接受每个请求,将参数camelcase并将请求参数的值设置为实体:

public function createRecordAction(Request $request, $id)
{
  $distribution = new Distributions();
  $params = $request->request;
  foreach ($request->request->all() as $param=>$value)
  {
    if ($param == "_method")
      continue;
    $function = "set".str_replace(' ','',ucwords(preg_replace('/[^A-Z^a-z^0-9]+/',' ',$param)));
    $distribution->$function($value);
  }
}

它可以工作,但我对这种方法的担忧是,我需要在每个做类似事情的控制器中运行这段代码。我可以将其重构为父类作为方法以避免重复代码,但我很好奇这是否是一个好的实践。我在Symfony框架中寻找一些已经做到这一点的东西,但我所能找到的都是将请求绑定到表单的示例。

首先:警告!!

正如我之前评论的那样,我会非常小心地使用您原始帖子中提供的代码,因为您说它是来自POST请求的数据,这意味着客户端可以在其中注入任何类型的数据并调用您可能不想要的对象上的函数(或者只是通过发送您不存在的函数名称而导致脚本失败)。

我实际上会先读结论…!:)然后再回到Alt &2 .


替代1:

话虽如此,你的问题的另一种解决方案是让对象负责获取自己的数据。有了足够细粒度的对象,就不会出现臃肿的代码,而且可以在每个类中定义要查找的参数和要调用的函数(并在对类进行更改时对更改进行本地化):

class BookInformation{
  private $publisher;
  private $name;
  private $price;
  public static createFromRequest($req){
    $publisher = Publisher::createFromRequest($req);
    $book = new BookInformation($publisher, $req['book_name'], $req['book_price']);
    $book->setABC($req['abc']);
    //...
    return $book;
  }
  public __construct($publisher, $name, $price){
    //...
  }
}
class Publisher{
  private $name;
  private $address;
  public static createFromRequest($req){
    return new Publisher($req['publisher_name'], $req['publisher_address']);
  }
  public __construct($name, $address){
    //...
  }
}

就像我之前说的,这个方法的一个很大的优点是,如果你需要添加新的属性到任何这些类,你不需要编辑控制器,可以编辑你的"从请求方法初始化"。

当然,不要忘记验证从用户请求发送的任何数据(但这只是常识)。


替代2:

请注意,第一种选择非常类似于工厂模式(基于GoF的抽象工厂),并且您也可以使用该模式实现解决方案:
class BookFactory{
  public createBookInformation($req){
    $publisher = $this->createPublisher($req);
    $book = new BookInformation($publisher, $req['book_name'], $req['book_price']);
    $book->setABC($req['abc']);
    //...
    return $book;
  }
  public createPublisher($req){
    return new Publisher($req['publisher_name'], $req['publisher_address']);
  }
  //createAnythingRelatedToBooks($req)...
}

这样,在非常内聚的类中拥有所有初始化过程,该类的唯一职责是基于请求对象初始化特定的对象族(这是一件非常好的事情)。但是,如果向其中一个类添加属性,则还必须编辑相应的Factory方法。


结论

请注意,这两个选择实际上并不是真正的选择…它们可以与初始代码(尤其是Factory代码)一起使用。它们实际上只解决了你的最后一个问题("把代码放在哪里"的问题)。

然而,即使您确实对POST请求进行了处理,并且只调用被注释的函数(如前所述),我也不建议这样做,因为我觉得更复杂的业务规则会很快破坏设计(但也许您已经全部覆盖了)。也就是说,我不认为您可以轻松地在初始化过程中插入业务规则,因为它是自动的(它不能对值进行任何验证,因为它可以是任何类型的值),而且我觉得您最终会在初始化后"撤销"一些东西(我个人讨厌……有很多错误的空间)!

例如,取选项1中相同的两个类(BookInformationPublisher)。

假设Book只能有Publisher,如果Publisher已经在数据库中注册并且他们的地址已经被确认(新的出版商需要使用另一个接口创建,然后在他们可以链接到一本书之前确认他们的地址)。

否则,不管请求数据是什么,publisher都应该设置为XYZ。我有一种感觉(我可能是错的),要支持这些规则,您必须实际构建对象(自动),然后销毁/重新分配publisher属性,如果它不匹配某些规则。现在,如果内存中有一个由这些Publisher对象组成的池,那么还需要记住删除该池中错误创建的Publisher。这只是一条规则!

你可以用你的代码做的一件事来"修复"这个问题是有一个验证方法为每个setter (validXYZ()),但这开始看起来像一个设计,会崩溃很快,如果验证是依赖于其他对象/数据…

我真的没有任何其他的东西来阻止你使用这些代码,但是如果你这样做了,请在一两年后(一旦有一些维护/添加了新功能等……)让我们了解它是如何工作的。

我在Symfony框架中寻找一些已经做到这一点的东西,但我所能找到的都是将请求绑定到表单的示例。

我将使用Form s。即使HTTP请求不是从HTMl表单执行的,您也可以将Request绑定到表单实例:它将负责所有的数据注入和验证。

另外,万一你需要HTML表单,你就准备好了^^.