【问题标题】:Deserialize an entity with a relationship with Symfony Serializer Component反序列化与 Symfony 序列化器组件有关系的实体
【发布时间】:2016-10-11 00:08:33
【问题描述】:

我正在尝试使用 symfony 序列化程序组件反序列化具有关系的实体。这是我的实体:

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Document
 *
 * @ORM\Table(name="document")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\DocumentRepository")
 */
class Document
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Genre", inversedBy="documents")
     * @ORM\JoinColumn(name="id_genre", referencedColumnName="id")
     */
    private $genre;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=100)
     */
    private $name;

    //getters and setters down here
    ...
}

以及体裁实体

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Genre
 *
 * @ORM\Table(name="genre")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\GenreRepository")
 */
class Genre
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=50, nullable=true)
     */
    private $name;

    /**
     * @ORM\OneToMany(targetEntity="Document", mappedBy="genre")
     */
    private $documents;

    public function __construct()
    {
        $this->documents= new ArrayCollection();
    }

    //getters and setters down here
    ....
}

在我的控制器操作中,我现在正在尝试这个:

$encoders = array(new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);

$document = $serializer->deserialize($request->getContent(), 'AppBundle\Entity\Document', 'json');

还有我的 json 数据

{"name": "My document", "genre": {"id": 1, "name": "My genre"}}

但我得到了下一个错误

“AppBundle\Entity\Genre”类型的预期参数,“array”给定(500 内部服务器错误)

是否可以反序列化带有内部关系的实体的 json 请求?

提前感谢。

【问题讨论】:

  • 我不知道 Sf Serializer 是否处理嵌套反序列化(可能是)。但是你也可以试试 JMSSerializerBundle(众所周知它有更多的功能)。

标签: json doctrine-orm deserialization symfony


【解决方案1】:

是和不是。首先,您不应在控制器中重新创建序列化程序的新实例,而应使用 serializer 服务。

第二,不,Symfony 序列化器不可能开箱即用。我们在https://api-platform.com/ 做这件事,但那里有一点魔力。也就是说,已经制作了一个 PR 来支持它:https://github.com/symfony/symfony/pull/19277

【讨论】:

  • 谢谢,很好的答案。 PR 是几天前合并的,所以在最近的版本中,我想应该支持它。
  • 我很好奇你是否找到了解决方案;我面临同样的问题。最新版本是否支持此功能?
  • 在开发主机上。恐怕它可能只会从 3.2 开始。
  • 好的,谢谢泰奥!你可以合并 3.1 中拉取请求中的编辑文件吗?我看到有 6 个文件已编辑..?我会试一试,如果它有效,会告诉你。
  • 你可以试试,但由于它是一个新功能,它应该登陆 3.2 而不是 3.1(Symfony 是 semver)
【解决方案2】:

现在可以了。你必须在config.yml中启用property_info:

  framework:
            property_info:
                    enabled: true

【讨论】:

    【解决方案3】:

    从 3.3 版开始,Symfony 文档所称的“Recursive Denormalization”使您能够对关联对象及其属性进行非规范化,并将专门解决问题中提到的类型错误。

    但是这不会使对象由 Doctrine 管理! 这意味着尚未规范化的关联将是null,并且不会自动从数据库中获取。
    如果您想要的是序列化时数据的快照,此方法可能适合您。
    如果您需要管理对象,则必须使用EntityManager 获取或合并它们。
    This solution 将通过从数据库中获取标识符将标识符非规范化为相应的托管实体。 当然,您必须记住,规范化的数据可能与数据库的当前状态不对应。 ID 可能已更改或删除等。

    为了让 Symfony 找到序列化对象的属性类型,它需要使用 PropertyInfo 组件,正如@slk500 在他的回答中所说,必须在framework configuration 中激活该组件。

    所以,如果您使用的是完整的框架,那么为了反序列化嵌套的 json 对象,您需要做的就是:

    1.在config.yml中启用序列化器和属性信息组件:

    framework:
        #...
        serializer: { enabled: true }
        property_info: { enabled: true }
    
    1. 然后inject the serializer 在任何你需要的地方:
    <?php
    // src/AppBundle/Controller/DefaultController.php
    namespace AppBundle\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\Serializer\SerializerInterface;
    use Symfony\Component\HttpFoundation\Request;
    
    class DefaultController extends Controller
    {
        public function indexAction(SerializerInterface $serializer, Request $request)
        {
            $document = $serializer->deserialize($request->getContent(), 'AppBundle\Entity\Document', 'json');
            // ...
        }
    }
    

    这些组件的默认功能足以满足我的需求。
    自动装配负责基本的服务声明,因此除非您需要特定的规范器,否则您甚至不必编辑 services.yml 配置文件。 根据您的用例,您可能必须启用特定功能。 查看 Serializer 和 PropertyInfo 文档以了解(希望)更具体的用例。

    【讨论】:

    • 嗨,对我不起作用。 Doctrine 的 ArrayCollection 被反序列化为数组,它们包含的实体也被渲染为数组。 SF 4+,序列化器工厂:new Serializer([new ObjectNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())), null, null, new ReflectionExtractor())], [new JsonEncoder()]))
    • 您有独立序列化程序组件的工作示例吗?
    【解决方案4】:

    对于 18 年从事此工作的任何人。我已经设法使用两种不同的方法来完成这项工作。

    我正在使用的关联实体。

    class Category
    {
         /**
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\Column(type="string", name="name", length=45, unique=true)
         */
        private $name;
    }
    
    class Item
    {
         /**
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\Column(type="string", name="uuid", length=36, unique=true)
         */
        private $uuid;
    
        /**
         * @ORM\Column(type="string", name="name", length=100)
         */
        private $name;
    
        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\Category", fetch="EAGER")
         * @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
         */
        private $category;
    }
    

    方法一:使用表单类

    #ItemType.php
    namespace App\Form;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\Form\FormTypeInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Bridge\Doctrine\Form\Type\EntityType;
    use App\Entity\Category;
    use App\Entity\Item;
    
    class ItemType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('name')
                ->add('category', EntityType::class, [
                    'class' => Category::class,
                    'choice_label' => 'name',
                ])
            ;
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => Item::class,
            ));
        }
    }
    
    #ItemController.php
    namespace App\Controller;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\Annotation\Route;
    use Symfony\Component\Serializer\Exception\NotEncodableValueException;
    use App\Entity\Item;
    use App\Form\ItemType;
    
    class ItemController extends BaseEntityController
    {
        protected $entityClass = Item::class;
    
        /**
         * @Route("/items", methods="POST")
         */
        public function createAction(Request $request)
        {
            $data = $request->getContent();
            $item = new Item();
            $form = $this->createForm(ItemType::class, $item);
            $decoded = $this->get('serializer')->decode($data, 'json');
            $form->submit($decoded);
    
            $object = $form->getData();
    
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($object);
            $entityManager->flush();
    
            return $this->generateDataResponse("response text", 201);
        }
    }
    

    方法 2:自定义规范器

    需要启用 PropertyInfo 组件。

    #/config/packages/framework.yaml
    framework:
        property_info:
            enabled: true
    

    注册自定义规范器。

    #/config/services.yaml
    services:
        entity_normalizer:
            class: App\SupportClasses\EntityNormalizer
            public: false
            autowire: true
            autoconfigure: true
            tags: [serializer.normalizer]
    

    自定义规范器。

    #EntityNormalizer.php
    namespace App\SupportClasses;
    
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
    use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
    use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
    use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
    use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
    
    
    class EntityNormalizer extends ObjectNormalizer
    {
        protected $entityManager;
    
        public function __construct(
            EntityManagerInterface $entityManager,
            ?ClassMetadataFactoryInterface $classMetadataFactory = null,
            ?NameConverterInterface $nameConverter = null,
            ?PropertyAccessorInterface $propertyAccessor = null,
            ?PropertyTypeExtractorInterface $propertyTypeExtractor = null
        ) {
            $this->entityManager = $entityManager;
    
            parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
        }
    
        public function supportsDenormalization($data, $type, $format = null)
        {
            return (strpos($type, 'App\\Entity\\') === 0) && 
            (is_numeric($data) || is_string($data) || (is_array($data) && isset($data['id'])));
        }
    
        public function denormalize($data, $class, $format = null, array $context = [])
        {
            return $this->entityManager->find($class, $data);
        }
    }
    

    我们的控制器的创建动作。

    #ItemController.php
    namespace App\Controller;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\Annotation\Route;
    use Symfony\Component\Serializer\Exception\NotEncodableValueException;
    use App\Entity\Item;
    use App\Form\ItemType;
    
    class ItemController extends BaseEntityController
    {
        protected $entityClass = Item::class;
    
        /**
         * @Route("/items", methods="POST")
         */
        public function createAction(Request $request)
        {
            $data = $request->getContent();
            $object = $this->get('serializer')->deserialize($data, $this->entityClass, 'json');
    
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($object);
            $entityManager->flush();
    
            return $this->generateDataResponse('response text', 201);
        }
    }
    

    这对我有用。我从以下方面获得灵感: https://medium.com/@maartendeboer/using-the-symfony-serializer-with-doctrine-relations-69ecb17e6ebd

    我修改了规范器以允许我将类别作为子 json 对象发送,当数据从 json 解码时,该对象将转换为子数组。希望这对某人有所帮助。

    【讨论】:

    • 我已将您的修改用于supportsDenormalization(),但我花了一段时间才明白为什么它对我不起作用,它寻找类名在“App”中而不是“AppBundle”,一旦改变,效果很好
    • @Gimsly 你确定你的自定义规范器类中的denormalize() 方法吗? 首先。 您使用$data 之类的实体标识符,但是您的规范化器可以将['id' =&gt; 1, 'someField' =&gt; 'someValue'] 获取为$data 并且Doctrine 的find() 方法不期望这个数组作为它的第二个参数。 其次。 即使您更改denormalize() 方法以提取id 以防$data 是一个数组,您也不能简单地从数据库返回实体作为denormalize() 的结果。在这种情况下,您将丢失对原始 $data 中实体字段的所有更改,不是吗?
    • 我建议不要扩展 ObjectNormalizer。通过这样做,您将创建另一个ObjectNormalizer,它将对所有对象应用通用数组非规范化,并且具有比默认规范化器更高的优先级,这可能会导致特定对象(如DateTimes)出现问题。如果您不打算覆盖规范化部分,只需让您的EntityNormalizer(实际上只是一个_de_normalizer)实现DenormalizerInterface。这就是你所需要的,这样它就不会干扰其他规范器。
    【解决方案5】:

    如果您使用的是 JMS 序列化器,您可以使用此代码,序列化器将在数据库中搜索关系。

    services.yml

    services:
        app.jms_doctrine_object_constructor:
            class: AppBundle\Services\JMSDoctrineObjectConstructor
            arguments: ['@doctrine', '@jms_serializer.unserialize_object_constructor']
    
        jms_serializer.object_constructor:
            alias: app.jms_doctrine_object_constructor
            public: false
    

    AppBundle\Services\JMSDoctrineObjectConstructor.php

    <?php
    
    namespace AppBundle\Services;
    
    use Doctrine\Common\Persistence\ManagerRegistry;
    use JMS\Serializer\DeserializationContext;
    use JMS\Serializer\Metadata\ClassMetadata;
    use JMS\Serializer\VisitorInterface;
    use JMS\Serializer\Construction\ObjectConstructorInterface;
    
    /**
     * Doctrine object constructor for new (or existing) objects during deserialization.
     */
    class JMSDoctrineObjectConstructor implements ObjectConstructorInterface
    {
        private $managerRegistry;
        private $fallbackConstructor;
    
        /**
         * Constructor.
         *
         * @param ManagerRegistry $managerRegistry Manager registry
         * @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
         */
        public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor)
        {
            $this->managerRegistry = $managerRegistry;
            $this->fallbackConstructor = $fallbackConstructor;
        }
    
        /**
         * {@inheritdoc}
         */
        public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
        {
            // Locate possible ObjectManager
            $objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
    
            if (!$objectManager) {
                // No ObjectManager found, proceed with normal deserialization
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }
    
            // Locate possible ClassMetadata
            $classMetadataFactory = $objectManager->getMetadataFactory();
    
            if ($classMetadataFactory->isTransient($metadata->name)) {
                // No ClassMetadata found, proceed with normal deserialization
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }
    
            // Managed entity, check for proxy load
            if (!is_array($data)) {
                // Single identifier, load proxy
                return $objectManager->getReference($metadata->name, $data);
            }
    
            // Fallback to default constructor if missing identifier(s)
            $classMetadata = $objectManager->getClassMetadata($metadata->name);
            $identifierList = array();
    
            foreach ($classMetadata->getIdentifierFieldNames() as $name) {
                if (!array_key_exists($name, $data)) {
                    return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
                }
    
                $identifierList[$name] = $data[$name];
            }
    
            // Entity update, load it from database
    
            if (array_key_exists('id', $identifierList) && $identifierList['id']) {
                $object = $objectManager->find($metadata->name, $identifierList);
            } else {
                $object = new $metadata->name;
            }
    
            $objectManager->initializeObject($object);
    
            return $object;
        }
    }
    

    【讨论】:

      【解决方案6】:

      以防其他人现在偶然发现这个问题。 我根据@Gimsly 的回答创建了一个解决方案。我的解决方案使用 Symfony 5.3 并且还添加了自定义 Denormalizer 来处理 Doctrine 实体的加载。它还允许通过使用OBJECT_TO_POPULATE 上下文调用ObjectNormalizer::denormalize 来更新相关的现有实体。如果应该允许或不允许更新现有的相关对象,可以通过序列化程序组配置进行配置。

      namespace App\Serializer;
      
      use Doctrine\Persistence\ManagerRegistry;
      use Doctrine\Persistence\ObjectRepository;
      use InvalidArgumentException;
      use Symfony\Component\Serializer\Exception\BadMethodCallException;
      use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
      use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
      use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
      use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
      
      class DoctrineEntityDenormalizer implements ContextAwareDenormalizerInterface
      {
          use DenormalizerAwareTrait;
      
          protected $doctrine;
      
          public function __construct(ObjectNormalizer $denormalizer, ManagerRegistry $doctrine)
          {
              $this->setDenormalizer($denormalizer);
              $this->setDoctrine($doctrine);
          }
      
          public function denormalize($data, string $type, string $format = null, array $context = [])
          {
              if (null === $this->denormalizer) {
                  throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
              }
              $repository = $this->getRepository($type);
              if (!$repository instanceof ObjectRepository) {
                  throw new InvalidArgumentException('No repository found for given type, '.$type.'.');
              }
              $entity = null;
              if (is_numeric($data) || is_string($data)) {
                  $entity = $repository->find($data);
              } elseif (is_array($data) && isset($data['id'])) {
                  $entity = $repository->find($data['id']);
              }
              if (is_null($entity)) {
                  throw new InvalidArgumentException('No Entity found for given id of type, '.$type.'.');
              }
              // Denormalize into the found entity with given data by using the default ObjectNormalizer
              $tmpContext = array_merge($context, [
                  AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
              ]);
              $entity = $this->denormalizer->denormalize($data, $type, $format, $tmpContext);
      
              return $entity;
          }
      
          public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
          {
              if (null === $this->denormalizer) {
                  throw new BadMethodCallException(sprintf('The nested denormalizer needs to be set to allow "%s()" '
                          . 'to be used.', __METHOD__));
              }
      
              $repository = $this->getRepository($type);
              // Check that it s an Entity of our App and a Repository exist for it
              // Also only use the denormalizer if an ID is set to load from the Repository.
              return strpos($type, 'App\\Entity\\') === 0 && !is_null($repository) && (is_numeric($data) || is_string($data)
                      || (is_array($data) && isset($data['id'])));
          }
      
          protected function getDoctrine(): ManagerRegistry
          {
              return $this->doctrine;
          }
      
          protected function setDoctrine(ManagerRegistry $doctrine): void
          {
              $this->doctrine = $doctrine;
          }
      
          protected function getRepository(string $class): ?ObjectRepository
          {
              $result = null;
              try {
                  $entityManager = $this->getDoctrine()->getManagerForClass($class);
                  if (!is_null($entityManager)) {
                      $result = $entityManager->getRepository($class);
                  }
              } catch (\Exception $ex) {
                  // Manager could not be resolved
              }
              return $result;
          }
      }
      

      实体的示例序列化程序定义:

      App\Entity\Group:
          attributes:
              id:
                  groups: ['group:read']
              name:
                  groups: ['group:read', 'group:write']
      
      App\Entity\Account:
          attributes:
              id:
                  groups: ['account:read']
              name:
                  groups: ['account:read', 'account:write']
              branchGroups:
                  groups: ['account:read', 'account:write']
                  max_depth: 1
      

      实体如下所示:

      namespace App\Entity;
      
      use Doctrine\Common\Collections\ArrayCollection;
      use Doctrine\Common\Collections\Collection;
      
      class Account
      {
          protected $id;
      
          protected $name;
      
          protected $groups;
      
          public function __construct()
          {
              $this->groups = new ArrayCollection();
          }
      
          public function getId(): ?int
          {
              return $this->id;
          }
      
          public function getName(): string
          {
              return $this->name;
          }
      
          public function setName(string $name)
          {
              $this->name = $name;
              return $this;
          }
      
          public function addGroup(BranchGroup $group)
          {
              $this->groups->add($group);
              return $this;
          }
      
          public function removeGroup(Group $Group)
          {
              $this->groups->removeElement($group);
              return $this;
          }
      
          public function getGroups()
          {
              return $this->groups;
          }
      
          public function setGroups(iterable $groups)
          {
              $this->groups->clear();
              foreach ($groups as $group) {
                  $this->addGroup($group);
              }
              return $this;
          }
      }
      
      namespace App\Entity;
      
      class Group
      {
          private $id;
      
          protected $name = '';
      
          public function getId(): ?int
          {
              return $this->id;
          }
      
          public function getName(): string
          {
              return $this->name;
          }
      
          public function setName(string $name)
          {
              $this->name = $name;
              return $this;
          }
      
      }
      

      使用现有组创建/更新帐户将如下所示:

      $entity = $this->fetchEntityToUpdate();
      jsonData = json_encode([
          'name' => 'newAccountName',
          'groups' => [
              [
                 'id' => 1
              ]
          ]
      ]);
      $context = [
          'groups' => ['account:write'],
          AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
      ];
      $entity = $serializer->deserialize($jsonData, Account::class, 'json', $context);
      

      现在这只会在Account 中添加/删除Group。如果我现在想额外更新相关的 Group 对象,我可以将额外的序列化组 group:write 添加到序列化上下文中。

      希望这对偶然发现此问题的人有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-09-27
        • 2018-07-12
        • 1970-01-01
        • 2014-05-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多