Search code examples
symfonydoctrinesymfony4jmsserializerbundle

JMSSerializerBundle deserialization skip groups exclusion on id property using DoctrineObjectConstructor


I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application : when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups

see rather entity

class Entity
{
/**
 * @var int
 *
 * @ORM\Column(name="id", type="int")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="IDENTITY")
 * @Serializer\Groups({"GetEntity"})
 */
private $id;

/**
 * @var string
 *
 * @ORM\Column(name="name", type="string")
 * @Serializer\Groups({"GetEntity", "PostEntity"})
 */
private $name;
}

controller

/**
 * @Route("/entity", name="post_entity", methods={"POST"})
 */
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
    $deserializationContext = DeserializationContext::create();
    $deserializationContext->setGroups(['PostEntity']);

    $entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
    $entityManager->persist($entity);
    $entityManager->flush();

    return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}

I have some JMS configurations changes in services

jms_serializer.object_constructor:
    alias: jms_serializer.doctrine_object_constructor
    public: true

jms_serializer.unserialize_object_constructor:
    class: App\Serializer\ObjectConstructor

If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.

Regards and thanks for any help


Solution

  • To resolve, just add override in your services.yaml

    jms_serializer.doctrine_object_constructor:
        class: App\Serializer\DoctrineObjectConstructor
        arguments:
            - '@doctrine'
            - '@jms_serializer.unserialize_object_constructor'
    
    jms_serializer.object_constructor:
        alias: jms_serializer.doctrine_object_constructor
    

    and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property

    class DoctrineObjectConstructor implements ObjectConstructorInterface
    {
    const ON_MISSING_NULL      = 'null';
    const ON_MISSING_EXCEPTION = 'exception';
    const ON_MISSING_FALLBACK  = 'fallback';
    
    private $fallbackStrategy;
    private $managerRegistry;
    private $fallbackConstructor;
    
    /**
     * Constructor.
     *
     * @param ManagerRegistry            $managerRegistry     Manager registry
     * @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
     * @param string                     $fallbackStrategy
     */
    public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
    {
        $this->managerRegistry     = $managerRegistry;
        $this->fallbackConstructor = $fallbackConstructor;
        $this->fallbackStrategy    = $fallbackStrategy;
    }
    
    /**
     * {@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 = [];
    
        foreach ($classMetadata->getIdentifierFieldNames() as $name) {
            $propertyGroups = [];
    
            if ($visitor instanceof AbstractVisitor) {
                /** @var PropertyNamingStrategyInterface $namingStrategy */
                $namingStrategy = $visitor->getNamingStrategy();
                $dataName       = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
                $propertyGroups = $metadata->propertyMetadata[$name]->groups;
            } else {
                $dataName = $name;
            }
    
            if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }
    
            $identifierList[$name] = $data[$dataName];
        }
    
        // Entity update, load it from database
        $object = $objectManager->find($metadata->name, $identifierList);
    
        if (null === $object) {
            switch ($this->fallbackStrategy) {
                case self::ON_MISSING_NULL:
                    return null;
                case self::ON_MISSING_EXCEPTION:
                    throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
                case self::ON_MISSING_FALLBACK:
                    return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
                default:
                    throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
            }
        }
    
        $objectManager->initializeObject($object);
    
        return $object;
    }
    

    }