Десериализация сущности со связью с компонентом сериализатора 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 }
  1. затем ввести сериализатор везде, где вам это нужно:
<?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;
    }
}