web-dev-qa-db-ja.com

Symfony Serializer Componentとの関係を持つエンティティを逆シリアル化します

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 data

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

しかし、私は次のerrorを得ました:

タイプ「AppBundle\Entity\Genre」の予期された引数、「配列」が指定されました(500内部サーバーエラー)

内部に関係があるエンティティを使用してjsonリクエストをデシリアライズすることは可能ですか?

よろしくお願いします。

12
dacuna

はいといいえ。まず、コントローラーでシリアライザーの新しいインスタンスを再作成するのではなく、代わりにserializerサービスを使用してください。

第二に、Symfonyシリアライザーを使用した状態では、それは不可能ではありません。 https://api-platform.com/ でそれを行っていますが、少し魔法があります。とはいえ、それをサポートするPRが作成されています: https://github.com/symfony/symfony/pull/19277

8
Théo

18年にこれに取り組んでいる人のために。私は2つの異なるアプローチを使用してこれをうまく機能させることができました。

連携している関連エンティティ。

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);
    }
}

コントローラーのcreateアクション。

#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オブジェクトとしてカテゴリを送信できるようにしました。うまくいけば、これは誰かを助けます。

4
Gimsly

これで動作します.config.ymlでproperty_infoを有効にする必要があります:

  framework:
            property_info:
                    enabled: true
3
slk500

これは、Symfonyのドキュメントが " Recursive Denormalization "と呼んでいるバージョン3.3から実際のマスター4.0までです。

Symfonyがシリアル化されたオブジェクトのプロパティタイプを見つけるためには、PropertyInfoコンポーネントを使用する必要があります。これは、@ slk500が彼の回答で述べたように、 framework configuration でアクティブにする必要があります。

したがって、完全なフレームワークを使用している場合、ネストされたjsonオブジェクトを逆シリアル化するために必要なことは次のとおりです。

1. 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');
        // ...
    }
}

これらのコンポーネントのデフォルト機能は、私のニーズには十分でした。
自動配線は基本的なサービス宣言を処理するので、特定のノーマライザが必要でない限り、services.yml構成ファイルを編集する必要さえありません。ユースケースによっては、特定の機能を有効にする必要がある場合があります。シリアライザとPropertyInfoのドキュメントで、(できれば)より具体的な使用例を確認してください。

1
vctls

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;
    }
}
0
Rafael Rocha