2014-03-31 79 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

相關問題