Deserialize an entity with a relationship with Symfony Serializer Component

前端 未结 5 1780
挽巷
挽巷 2020-12-16 01:27

I\'m trying to deserialize an entity with a relationship using the symfony serializer component. This is my entity:

namespace AppBundle\\Entity;

use Doctrin         


        
相关标签:
5条回答
  • 2020-12-16 01:42

    This is what the Symfony documentation calls "Recursive Denormalization", starting from version 3.3 up to the actual master, 4.0.

    In order for Symfony to find the property types of the serialized objects, it needs to use the PropertyInfo component, which, as @slk500 stated in his answer, has to be activated in the framework configuration.

    So, if you are using the full framework, all you need to do in order to deserialize nested json objects is this:

    1.Enable the serializer and the property info components in config.yml:

    framework:
        #...
        serializer: { enabled: true }
        property_info: { enabled: true }
    
    1. Then inject the serializer wherever you need it:
    <?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');
            // ...
        }
    }
    

    The default features of these components were enough for my needs.
    Autowiring takes care of the basic service declaration, so unless you need specific normalizers, you don't even have to edit the services.yml configuration file. Depending on your use cases, you may have to enable specific features. Check the Serializer and PropertyInfo documentation for (hopefully) more specific use cases.

    0 讨论(0)
  • 2020-12-16 01:51

    If you are using JMS Serializer, you can use this code and the serializer will search for relation in database.

    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 讨论(0)
  • 2020-12-16 01:55

    Yes and no. First, you shouldn't re-create a new instance of the serializer in your controller but use the serializer service instead.

    Second, no it's not possible out of the box with Symfony serializer. We are doing it in https://api-platform.com/ but there is a bit of magic there. That said, a PR has been made to support it: https://github.com/symfony/symfony/pull/19277

    0 讨论(0)
  • 2020-12-16 01:58

    It works now.You have to enable property_info in config.yml:

      framework:
                property_info:
                        enabled: true
    
    0 讨论(0)
  • 2020-12-16 02:01

    For anyone who is working on this in '18. I've managed to get this working using two different approaches.

    The associated entities I'm working with.

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

    Method 1: Using Form Classes

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

    Method 2: A Custom Normalizer

    The PropertyInfo Component needs to be enabled.

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

    Register the custom normalizer.

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

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

    Our controller's create action.

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

    This has worked for me. I received inspiration from: https://medium.com/@maartendeboer/using-the-symfony-serializer-with-doctrine-relations-69ecb17e6ebd

    I modified the normalizer to allow me to send the category as a child json object which is converted to a child array when the data is decoded from json. Hopefully this helps someone.

    0 讨论(0)
提交回复
热议问题