Десериализация сущности со связью с компонентом сериализатора Symfony
Я пытаюсь десериализовать сущность с отношением, используя компонент сериализатора 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"}}
но я получил следующее :
ожидаемый аргумент типа " AppBundleEntityGenre", "массив" с учетом (500 Внутренняя Ошибка Сервера)
можно ли десериализовать запрос json с сущностью с отношениями внутри?
Спасибо в advace.
5 ответов
да и нет. Во-первых, вы не должны повторно создавать новый экземпляр сериализатора в своем контроллере, но использовать сервис.
во-вторых, нет, это невозможно из коробки с сериализатором Symfony. Мы делаем это в https://api-platform.com/ но там есть немного магии. Что сказал, пиар был сделан, чтобы поддержать его: https://github.com/symfony/symfony/pull/19277
теперь работает.Вы должны включить property_info в config.в формате YML:
framework:
property_info:
enabled: true
для тех, кто работает над этим в 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;
}
Метод 1: Использование Классов Форм
#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. Надеюсь, это кому-то поможет.
это то, что документация Symfony называет"Рекурсивные Денормализации", начиная с версии 3.3 до фактического master, 4.0.
чтобы Symfony нашел типы свойств сериализованных объектов, ему необходимо использовать компонент PropertyInfo, который, как указано в его ответе @slk500, должен быть активирован в база конфигурации.
Итак, если вы используете полную структуру, все, что вам нужно сделать, чтобы десериализовать вложенные объекты json так:
1.Включите сериализатор и компоненты свойств info в config.в формате YML:
framework:
#...
serializer: { enabled: true }
property_info: { enabled: true }
- затем ввести сериализатор везде, где вам это нужно:
<?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');
// ...
}
}
функции по умолчанию этих компонентов было достаточно для моих потребностей.
Autowiring заботится о базовой декларации службы, поэтому, если вам не нужны конкретные нормализаторы, вам даже не нужно редактировать services.yml
конфигурационный файл.
В зависимости от вариантов использования может потребоваться включить определенные функции.
Проверьте документацию Serializer и PropertyInfo для (надеюсь) более конкретных случаев использования.
Если вы используете сериализатор JMS, вы можете использовать этот код, и сериализатор будет искать связь в базе данных.
услуги.в формате 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;
}
}