Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial update with RequestBodyParamConverter #1817

Open
ThomasLabstep opened this issue Dec 11, 2017 · 0 comments
Open

Partial update with RequestBodyParamConverter #1817

ThomasLabstep opened this issue Dec 11, 2017 · 0 comments

Comments

@ThomasLabstep
Copy link

Hello,

I've implemented a few years ago a partial update version of RequestBodyParamConverter.

    /*
     * @ParamConverter("entity", converter="my_bundle.request_body")
     * @Put("/{id}", requirements={"id" = "\d+"})
     */
    public function putEntityAction(Request $request, object $entity)
    {
        return $this->put($request, $entity);
    }

What I expect from this method is to be able to update one of multiple fields of an entity and the validation should only check these fields.
The problem being that we deserialize the entity, so we actually lose a lot of information.

The way I solved this is with this method:

    protected function put(Request $request, $deserializedEntity)
    {
        $id = $request->get('id');

        if (!isset($id)) {
            throw $this->createNotFoundException('Parameter required: id');
        }

        $em = $this->getDoctrine()->getManager();
        $entityClassName = $em->getClassMetadata(ClassUtils::getRealClass(get_class($deserializedEntity)))->getName();

        $entity = $em->getRepository($entityClassName)->find($id);
        if (!$entity) {
            throw $this->createNotFoundException('No such entity '.$entityClassName.' with id: '.$id);
        }

        $this->partialUpdate($request, $deserializedEntity, $entity);

        $this->validateEntityField($entity, $request->request->all());

        $em->persist($entity);
        $em->flush();

        return $entity;
    }

But for this to work, I needed to override RequestBodyParamConverter with my own implementation, just to add two lines and two methods:


    /**
     * {@inheritdoc}
     *
     * @param Request        $request
     * @param ParamConverter $configuration
     */
    public function apply(Request $request, ParamConverter $configuration)
    {
        $options = (array) $configuration->getOptions();

        if (isset($options['deserializationContext']) && is_array($options['deserializationContext'])) {
            $arrayContext = array_merge($this->context, $options['deserializationContext']);
        } else {
            $arrayContext = $this->context;
        }
        $this->configureContext($context = new Context(), $arrayContext);

        try {
            $object = $this->serializer->deserialize(
                $request->getContent(),
                $configuration->getClass(),
                $request->getContentType(),
                $context
            );
        } catch (UnsupportedFormatException $e) {
            return $this->throwException(new UnsupportedMediaTypeHttpException($e->getMessage(), $e), $configuration);
        } catch (JMSSerializerException $e) {
            return $this->throwException(new BadRequestHttpException($e->getMessage(), $e), $configuration);
        } catch (SymfonySerializerException $e) {
            return $this->throwException(new BadRequestHttpException($e->getMessage(), $e), $configuration);
        }

        // Added
        $this->addAssociations($request, $object);
        $this->addDefaultValues($object);
        // End

        $request->attributes->set($configuration->getName(), $object);

        if (null !== $this->validator) {
            $validatorOptions = $this->getValidatorOptions($options);

            $errors = $this->validator->validate($object, null, $validatorOptions['groups']);

            $request->attributes->set(
                $this->validationErrorsArgument,
                $errors
            );
        }

        return true;
    }



    /**
     * Initializes an entity by calling its constructor after unserialization.
     *
     * @param object $object The object to update
     */
    protected function addDefaultValues(&$object)
    {
        $accessor = PropertyAccess::createPropertyAccessor();

        $entityClass = ClassUtils::getRealClass(get_class($object));
        $newEntity = new $entityClass();

        // Add author if not set
        if ($accessor->isReadable($object, 'author')) {
            $author = $accessor->getValue($object, 'author');
            $user = $this->tokenStorage->getToken()->getUser();
            if (is_null($author) && !is_null($user)) {
                $accessor->setValue($object, 'author', $user);
            }
        }

        $this->duplicateFields($object, $newEntity);
    }

    /**
     * Initializes an entity by calling its constructor after unserialization.
     *
     * @param object $dest The object to update
     * @param object $src  The object from which to duplicate all fields
     */
    protected function duplicateFields(&$dest, $src)
    {
        $accessor = PropertyAccess::createPropertyAccessor();

        $destClass = ClassUtils::getRealClass(get_class($dest));
        $srcClass = ClassUtils::getRealClass(get_class($src));
        if ($destClass != $srcClass) {
            throw new \Exception('Cannot duplicate fields. Source and destination are not the same class.');
        }

        $metadataFactory = $this->em->getMetadataFactory();
        $class = $metadataFactory->getMetadataFor($destClass);
        foreach ($class->getFieldNames() as $fieldName) {
            try {
                $destValue = $accessor->getValue($dest, $fieldName);
                $srcValue = $accessor->getValue($src, $fieldName);
                if (is_null($destValue) && !is_null($srcValue)) {
                    $accessor->setValue($dest, $fieldName, $srcValue);
                }
            } catch (\Exception $e) {
                // Ignore
            }
        }
    }

Do you see a better and cleaner way to handle partial update ?

Thank you for your feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant