如何使用FOSRestBundle处理REST API中的实体更新(PUT请求)

时间:2014-03-31 07:29:24

标签: rest symfony fosrestbundle jmsserializerbundle

我使用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

6 个答案:

答案 0 :(得分:2)

如果您正在使用PUT,那么根据REST,您应该使用路由本身中有关实体的ID的更新路由,例如/ entity / {entity}。 FOSRestBundle也是这样做的。

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

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

编辑:注入两个实体实际上会更好。一个是当前数据库状态,一个是来自客户端的已发送数据。您可以使用两个ParamConverter-annotations实现此目的:

{{1}}

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

如果您只是在不合并/检查的情况下覆盖数据,那么请使用第一个选项。但请记住,如果客户端发送一个尚未使用的id,如果你不阻止它,那么这将允许创建一个新的实体。

答案 1 :(得分:1)

似乎有一种方法是使用Symfony Form组件(SimpleThingsFormSerializerBundle),如http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/#post-it

中所述

引用SimpleThingsFormSerializerBundle自述文件:

  

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

答案 2 :(得分:1)

我还遇到了使用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();
}

方法 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);
}

答案 3 :(得分:1)

最好的方法是使用 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

答案 4 :(得分:0)

我遇到与您描述的问题相同的问题,我只是手动合并实体:

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

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

答案 5 :(得分:-1)

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