2014-03-31 45 views
2

我在Symfony2中使用FMSRestBundle使用JMSSerializerBundle为实体序列化原型开发了一个REST API。通过GET请求,我可以使用SensioFrameworkExtraBundle的ParamConverter功能根据id请求参数获取实体的实例,并在使用POST请求创建新实体时,我可以使用FOSRestBundle body转换器基于实体创建实体的新实例请求数据。但是当我想更新现有的实体时,使用FOSRestBundle转换器会给出一个没有id的实体(即使id与请求数据一起发送),所以如果我坚持它,它会创建一个新的实体。并且使用SensioFrameworkExtraBundle转换器为我提供没有新数据的原始实体,因此我必须手动从请求中获取数据并调用所有setter方法来更新实体数据。如何使用FOSRestBundle处理REST API中的实体更新(PUT请求)

所以我的问题是,什么是处理这种情况的首选方式?感觉应该有一些方法来处理这个问题,使用请求数据的(反)序列化。我是否缺少与ParamConverter或JMS序列化程序相关的东西来处理这种情况?我意识到有很多方法可以做这种事情,而且没有一种方法适合每种用例,只需要寻找一些适合这种快速原型的东西,通过使用ParamConverter和需要编写的最小代码即可完成在控制器/服务中。

下面是如上所述的与GET和POST操作的控制器的一个示例:

namespace My\ExampleBundle\Controller; 

use My\ExampleBundle\Entity\Entity; 
use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\Validator\ConstraintViolationListInterface; 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 
use FOS\RestBundle\Controller\Annotations as Rest; 
use FOS\RestBundle\View\View; 

class EntityController extends Controller 
{ 
    /** 
    * @Route("/{id}", requirements={"id" = "\d+"}) 
    * @ParamConverter("entity", class="MyExampleBundle:Entity") 
    * @Method("GET") 
    * @Rest\View() 
    */ 
    public function getAction(Entity $entity) 
    { 
     return $entity; 
    } 

    /** 
    * @Route("/") 
    * @ParamConverter("entity", converter="fos_rest.request_body") 
    * @Method("POST") 
    * @Rest\View(statusCode=201) 
    */ 
    public function createAction(Entity $entity, ConstraintViolationListInterface $validationErrors) 
    { 
     // Handle validation errors 
     if (count($validationErrors) > 0) { 
      return View::create(
       ['errors' => $validationErrors], 
       Response::HTTP_BAD_REQUEST 
      ); 
     } 

     return $this->get('my.entity.repository')->save($entity); 
    } 
} 

而在config.yml我为FOSRestBundle以下配置:

fos_rest: 
    param_fetcher_listener: true 
    body_converter: 
     enabled: true 
     validate: true 
    body_listener: 
     decoders: 
      json: fos_rest.decoder.jsontoform 
    format_listener: 
     rules: 
      - { path: ^/api/, priorities: ['json'], prefer_extension: false } 
      - { path: ^/, priorities: ['html'], prefer_extension: false } 
    view: 
     view_response_listener: force 

回答

0

,我只是做实体手动合并:

public function patchMembersAction($memberId, Member $memberPatch) 
{ 
    return $this->members->updateMember($memberId, $memberPatch); 
} 

这就需要方法,做验证,然后手动调用所有必需的setter方法。无论如何,我想知道为这种情况编写我自己的param转换器。

1

如果您使用PUT,根据REST,您应该使用路由进行更新,并在路由本身中使用相关实体的标识,例如/ entity/{entity}。 FOSRestBundle也是这样做的。

你的情况,这应该是这样的:

/** 
* @Route("/{entityId}", requirements={"entityId" = "\d+"}) 
* @ParamConverter("entity", converter="fos_rest.request_body") 
* @Method("PUT") 
* @Rest\View(statusCode=201) 
*/ 
public function putAction($entityId, Entity $entity, ConstraintViolationListInterface $validationErrors) 

编辑:这实际上是更好的有注入两个实体。一个是当前数据库状态,另一个是从客户端发送的数据。您可以有两个ParamConverter的注解实现这一目标:

/** 
* @Route("/{id}", requirements={"id" = "\d+"}) 
* @ParamConverter("entity") 
* @ParamConverter("entityNew", converter="fos_rest.request_body") 
* @Method("PUT") 
* @Rest\View(statusCode=201) 
*/ 
public function putAction(Entity $entity, Entity $entityNew, ConstraintViolationListInterface $validationErrors) 

这将当前数据库状态加载到$实体和数据上传到$ entityNew。现在,您可以根据需要合并数据。

如果您没有合并/检查覆盖数据就可以了,那么请使用第一个选项。但请记住,如果客户端发送一个尚未使用的ID,如果您不阻止该ID,这将允许创建一个新的实体。

+0

我不同意“你应该使用PUT路由进行更新”,PUT主要用于替换。您可以使用PATCH进行部分更新。 – Alcalyn

+0

这里有一点误会。我的意思是“如果你使用PUT路由,REST建议像我描述的那样做”,它指的是URL结构。我澄清说。除此之外,PUT或POST仍然是很好的解决方案,具体取决于具体的用例。 – marsbear

1

我对使用JMS序列化程序处理PUT请求也有问题。首先,我想使用序列化器自动处理查询。放入请求可能不包含完整的数据。部分数据必须映射到实体上。你可以使用我的简单的解决方案:

/** 
* @Route(path="/edit",name="your_route_name", methods={"PUT"}) 
* 
* This parameter is using for creating a current fields of request 
* @RequestParam(
*  name="id", 
*  requirements="\d+", 
*  nullable=false, 
*  allowBlank=true, 
*  strict=true, 
*) 
* @RequestParam(
*  name="some_field", 
*  requirements="\d{13}", 
*  nullable=true, 
*  allowBlank=true, 
*  strict=true, 
*) 
* @RequestParam(
*  name="some_another_field", 
*  requirements="\d{13}", 
*  nullable=true, 
*  allowBlank=true, 
*  strict=true, 
*) 
* @param Request $request 
* @param ParamFetcher $paramFetcher 
* @return Response 
*/ 
public function editAction(Request $request, ParamFetcher $paramFetcher) 
{ 
    //validate parameters 
    $paramFetcher->all(); 
    /** @var EntityManager $em */ 
    $em = $this->getDoctrine()->getManager(); 
    $yourEntity = $em->getRepository('YourBundle:SomeEntity')->find($paramFetcher->get('id')); 
    //get request params (param fetcher has all params, but we need only params from request) 
    $data = $request->request->all(); 
    $this->mapDataOnEntity($data, $yourEntity, ['some_serialized_group','another_group']); 

    $em->flush(); 

    return new JsonResponse(); 
} 

方法mapDataOnEntity你可以在某些性状或你中间控制器类定位。这里是他此方法的实现:

/** 
* @param array $data 
* @param object $targetEntity 
* @param array $serializationGroups 
*/ 
public function mapDataOnEntity($data, $targetEntity, $serializationGroups = []) 
{ 
    /** @var object $source */ 
    $sourceEntity = $this->get('jms_serializer') 
     ->deserialize(
      json_encode($data), 
      get_class($targetEntity), 
      'json', 
      DeserializationContext::create()->setGroups($serializationGroups) 
     ); 
    $this->fillProperties($data, $targetEntity, $sourceEntity); 
} 

/** 
* @param array $params 
* @param object $targetEntity 
* @param object $sourceEntity 
*/ 
protected function fillProperties($params, $targetEntity, $sourceEntity) 
{ 
    $propertyAccessor = new PropertyAccessor(); 
    /** @var PropertyMetadata[] $propertyMetadata */ 
    $propertyMetadata = $this->get('jms_serializer.metadata_factory') 
     ->getMetadataForClass(get_class($sourceEntity)) 
     ->propertyMetadata; 
    foreach ($propertyMetadata as $realPropertyName => $data) { 
     $serializedPropertyName = $data->serializedName ?: $this->fromCamelCase($realPropertyName); 
     if (array_key_exists($serializedPropertyName, $params)) { 
      $newValue = $propertyAccessor->getValue($sourceEntity, $realPropertyName); 
      $propertyAccessor->setValue($targetEntity, $realPropertyName, $newValue); 
     } 
    } 
} 

/** 
* @param string $input 
* @return string 
*/ 
protected function fromCamelCase($input) 
{ 
    preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); 
    $ret = $matches[0]; 
    foreach ($ret as &$match) { 
     $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); 
    } 

    return implode('_', $ret); 
} 
1

最好的办法是使用JMSSerializerBundle

问题是JMSSerializer使用默认ObjectConstructor反序列化(设置不在领域的初始化该请求为空,并且使该合并方法也将空值属性持久化到数据库)。所以你需要用DoctrineObjectConstructor来切换这个。

services: 
    jms_serializer.object_constructor: 
     alias: jms_serializer.doctrine_object_constructor 
     public: false 

然后只是反序列化并坚持实体,它会被缺少的字段填充。当您保存到数据库中只有更改将在数据库中更新的属性:

$foo = $this->get('jms_serializer')->deserialize(
      $request->getContent(), 
      'AppBundle\Entity\Foo', 
      'json'); 
$em = $this->getDoctrine()->getManager(); 
$em->persist($foo); 
$em->flush(); 

贷:Symfony2 Doctrine2 De-Serialize and Merge Entity issue

+0

这看起来像一个有前途的解决方案。我将不得不尝试一下,看看它是如何工作的。 – Cvuorinen

相关问题