Как обновить сущность Doctrine из сериализованного JSON?

Мы используем Symfony2 для создания API. При обновлении записи мы ожидаем, что вход JSON будет представлять сериализованную обновленную сущность. Данные JSON не будут содержать некоторые поля (например, CreatedAt должен быть установлен только один раз при создании объекта - и никогда не обновляться). Например, вот пример запроса JSON PUT:

{"id":"1","name":"anyname","description":"anydescription"}

Вот код PHP на контроллере, который должен обновить сущность в соответствии с JSON выше (мы используем сериализатор JMS Bundle):

$supplier = $serializer->deserialize(
    $this->get('request')->getContent(),
    'WhateverEntity',
    'json'
);

EntityManger понимает (правильно), что это запрос на обновление (фактически, запрос SELECT запускается неявно). EntityManager также предполагает (не правильно), что свойство CreatedAt должно быть NULLified - вместо этого оно должно сохранять предыдущее.

Как исправить эту проблему?

2 ответа

Решение

Я бы использовал Doctrine\ORM\Mapping\ClassMetadata API для обнаружения существующих полей в вашей сущности. Вы можете сделать следующее (я не знаю, как работает JMSSerializerBundle):

//Unserialize data into $data
$metadata = $em->getMetadataFactory()->getMetadataFor($FQCN);
$id = array();
foreach ($metadata->getIdentifierFieldNames() as $identifier) {
    if (!isset($data[$identifier])) {
        throw new InvalidArgumentException('Missing identifier');
    }
    $id[$identifier] = $data[$identifier];
    unset($data[$identifier]);
}
$entity = $em->find($metadata->getName(), $id);
foreach ($metadata->getFieldNames() as $field) {
    //add necessary checks about field read/write operation feasibility here
    if (isset($data[$field])) {
        //careful! setters are not being called! Inflection is up to you if you need it!
        $metadata->setFieldValue($entity, $field, $data[$field]);
    }
}
$em->flush();

Также возможно сделать это с Symfony Serializer, используя опцию object_to_populate.

Пример: я получаю запрос JSON. Если запись существует в базе данных, я хочу обновить поля, полученные в теле, если она не существует, я хочу создать новую.

/**
 * @Route("/{id}", methods={"PUT"})
 */
public function upsert(string $id, Request $request, SerializerInterface $serializer)
{
  $content = $request->getContent(); // Get json from request

  $product = $this->getDoctrine()->getRepository(Product::class)->findOne($id); // Try to find product in database with provided id

  if (!$product) { // If product does not exist, create fresh entity
      $product = new Product();
  }

  $product = $serializer->deserialize(
            $content,
            Product::class,
            'json',
            ['object_to_populate' => $product] // Populate deserialized JSON content into existing/new entity
        );
  // validation, etc...
  $this->getDoctrine()->getManager()->persist($product); // Will produce update/instert statement 
  $this->getDoctrine()->getManager()->flush($product);

// (...)

С помощью JMSSerializerBundle следуйте инструкциям по установке на http://jmsyst.com/bundles/JMSSerializerBundle

либо создайте свою собственную службу сериализатора, либо измените JMSSerializerBundle, чтобы использовать конструктор объекта doctrine вместо конструктора простого объекта.

<service id="jms_serializer.object_constructor" alias="jms_serializer.doctrine_object_constructor" public="false"/>

Это в основном обрабатывает именно то, что делает решение Ocramius, но с использованием десериализации JMSSerializerBundles.

Другие вопросы по тегам