反序列化与Symfony Serializer Component有关系的实体



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

namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
/**
* Document
*
* @ORMTable(name="document")
* @ORMEntity(repositoryClass="AppBundleRepositoryDocumentRepository")
*/
class Document
{
/**
* @var int
*
* @ORMColumn(name="id", type="integer")
* @ORMId
* @ORMGeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORMManyToOne(targetEntity="Genre", inversedBy="documents")
* @ORMJoinColumn(name="id_genre", referencedColumnName="id")
*/
private $genre;
/**
* @var string
*
* @ORMColumn(name="name", type="string", length=100)
*/
private $name;
//getters and setters down here
...
}

流派实体

namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
use DoctrineCommonCollectionsArrayCollection;
/**
* Genre
*
* @ORMTable(name="genre")
* @ORMEntity(repositoryClass="AppBundleRepositoryGenreRepository")
*/
class Genre
{
/**
* @var int
*
* @ORMColumn(name="id", type="integer")
* @ORMId
* @ORMGeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORMColumn(name="name", type="string", length=50, nullable=true)
*/
private $name;
/**
* @ORMOneToMany(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(), 'AppBundleEntityDocument', 'json');

还有我的json 数据

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

但是我得到了下一个错误

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

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

谢谢。

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

其次,不,使用Symfony序列化器不可能开箱即用。我们在 https://api-platform.com/这样做,但那里有一点魔力。也就是说,已经进行了公关来支持它:https://github.com/symfony/symfony/pull/19277

对于在 18 年从事这项工作的任何人。我设法使用两种不同的方法使其工作。

我正在使用的关联实体。

class Category
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;
/**
* @ORMColumn(type="string", name="name", length=45, unique=true)
*/
private $name;
}
class Item
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;
/**
* @ORMColumn(type="string", name="uuid", length=36, unique=true)
*/
private $uuid;
/**
* @ORMColumn(type="string", name="name", length=100)
*/
private $name;
/**
* @ORMManyToOne(targetEntity="AppEntityCategory", fetch="EAGER")
* @ORMJoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
private $category;
}

方法 1:使用窗体类

#ItemType.php
namespace AppForm;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentFormFormTypeInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
use SymfonyBridgeDoctrineFormTypeEntityType;
use AppEntityCategory;
use AppEntityItem;
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 AppController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyComponentSerializerExceptionNotEncodableValueException;
use AppEntityItem;
use AppFormItemType;
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:自定义规范化器

需要启用属性信息组件。

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

注册自定义规范化程序。

#/config/services.yaml
services:
entity_normalizer:
class: AppSupportClassesEntityNormalizer
public: false
autowire: true
autoconfigure: true
tags: [serializer.normalizer]

自定义规范化程序。

#EntityNormalizer.php
namespace AppSupportClasses;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentPropertyAccessPropertyAccessorInterface;
use SymfonyComponentPropertyInfoPropertyTypeExtractorInterface;
use SymfonyComponentSerializerNameConverterNameConverterInterface;
use SymfonyComponentSerializerMappingFactoryClassMetadataFactoryInterface;
use SymfonyComponentSerializerNormalizerObjectNormalizer;

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 AppController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyComponentSerializerExceptionNotEncodableValueException;
use AppEntityItem;
use AppFormItemType;
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 解码时,该对象将转换为子数组。希望这对某人有所帮助。

它现在可以工作了。您必须在 config.yml 中启用property_info:

framework:
property_info:
enabled: true

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

namespace AppSerializer;
use DoctrinePersistenceManagerRegistry;
use DoctrinePersistenceObjectRepository;
use InvalidArgumentException;
use SymfonyComponentSerializerExceptionBadMethodCallException;
use SymfonyComponentSerializerNormalizerAbstractNormalizer;
use SymfonyComponentSerializerNormalizerContextAwareDenormalizerInterface;
use SymfonyComponentSerializerNormalizerDenormalizerAwareTrait;
use SymfonyComponentSerializerNormalizerObjectNormalizer;
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;
}
}

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

AppEntityGroup:
attributes:
id:
groups: ['group:read']
name:
groups: ['group:read', 'group:write']
AppEntityAccount:
attributes:
id:
groups: ['account:read']
name:
groups: ['account:read', 'account:write']
branchGroups:
groups: ['account:read', 'account:write']
max_depth: 1

实体如下所示:

namespace AppEntity;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
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 AppEntity;
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添加到序列化上下文中。

希望这能帮助那些偶然发现这一点的人。

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

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

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

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

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

framework:
#...
serializer: { enabled: true }
property_info: { enabled: true }
  1. 然后将序列化程序注入到任何需要的地方:
<?php
// src/AppBundle/Controller/DefaultController.php
namespace AppBundleController;
use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentSerializerSerializerInterface;
use SymfonyComponentHttpFoundationRequest;
class DefaultController extends Controller
{
public function indexAction(SerializerInterface $serializer, Request $request)
{
$document = $serializer->deserialize($request->getContent(), 'AppBundleEntityDocument', 'json');
// ...
}
}

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

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

服务.yml

services:
app.jms_doctrine_object_constructor:
class: AppBundleServicesJMSDoctrineObjectConstructor
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 AppBundleServices;
use DoctrineCommonPersistenceManagerRegistry;
use JMSSerializerDeserializationContext;
use JMSSerializerMetadataClassMetadata;
use JMSSerializerVisitorInterface;
use JMSSerializerConstructionObjectConstructorInterface;
/**
* 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;
}
}

最新更新