【问题标题】:How to handle entity update (PUT request) in REST API using FOSRestBundle如何使用 FOSRestBundle 在 REST API 中处理实体更新(PUT 请求)
【发布时间】:2014-03-31 07:29:23
【问题描述】:

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

所以我的问题是,处理这种情况的首选方法是什么?感觉应该有某种方法可以使用请求数据的(反)序列化来处理这个问题。我是否遗漏了与处理这种情况的 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

【问题讨论】:

    标签: rest symfony fosrestbundle jmsserializerbundle


    【解决方案1】:

    如果您使用 PUT,根据 REST,您应该使用路由进行更新,其中包含路由本身中相关实体的 ID,例如 /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-annotations 来实现这一点:

    /**
     * @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)
    

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

    如果您可以只覆盖数据而不进行合并/检查,请使用第一个选项。但请记住,如果您不阻止客户端发送尚未使用的 id,这将允许创建新实体。

    【讨论】:

    • 我不同意“您应该使用 PUT 路由进行更新”,PUT 主要用于替换。您可以使用 PATCH 进行部分更新。
    • 这里有点误会。我的意思是“如果您使用 PUT 路由,REST 建议按照我的描述进行操作”,它指的是 URL 结构。我澄清了这一点。除此之外,PUT 或 POST 仍然是很好的解决方案,具体取决于具体的用例。
    • 如何在你的编辑中将当前数据库状态注入到 $entity 中?它如何确定它必须检索 URL 中给出的 id 实体?我不能让它自己工作。
    【解决方案2】:

    似乎一种方法是使用 http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/#post-it 中所述的 Symfony 表单组件(带有 SimpleThingsFormSerializerBundle

    引自 SimpleThingsFormSerializerBundle README:

    此外,所有当前的序列化程序组件都有一个共同的缺陷:它们不能反序列化(更新)到现有的对象图中。更新对象图是 Form 组件已经解决的问题(完美!)。

    【讨论】:

      【解决方案3】:

      我在使用 JMS 序列化程序处理 PUT 请求时也遇到了问题。首先,我想使用序列化程序自动处理查询。 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();
      }
      

      方法 ma​​pDataOnEntity 您可以定位在某个特征或中间控制器类中。这是他对这个方法的实现:

      /**
       * @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);
      }
      

      【讨论】:

        【解决方案4】:

        最好的方法是使用 JMSSerializerBundle

        问题是 JMSSerializer 使用默认的 ObjectConstructor 初始化以进行反序列化(将不在请求中的字段设置为 null,并使该合并方法也将保持为 null属性到数据库)。所以你需要用 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

        【讨论】:

        • 这看起来是一个很有前途的解决方案。我将不得不尝试一下,看看它是如何工作的。
        • 如果有人偶然发现这个问题并尝试使用教义对象构造器:您必须在请求正文中提供 id 参数,而不仅仅是在路由参数中!
        【解决方案5】:

        我遇到了和你描述的一样的问题,我只是手动进行实体合并:

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

        这会调用执行验证的方法,然后手动调用所有必需的 setter 方法。无论如何,我想为这种情况编写自己的参数转换器。

        【讨论】:

          【解决方案6】:

          另一个对我帮助很大的资源是http://welcometothebundle.com/symfony2-rest-api-the-best-2013-way/。一步一步的教程,填补了我在上一条评论中的资源之后的空白。祝你好运!

          【讨论】:

          • 而不是仅仅分享一个链接,解释一下那个链接的代码的哪一部分对OP有帮助。
          猜你喜欢
          • 1970-01-01
          • 2017-02-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-25
          • 1970-01-01
          • 2023-02-10
          • 2017-03-11
          相关资源
          最近更新 更多