-
-
Notifications
You must be signed in to change notification settings - Fork 585
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
Allow Constructed Object to be Passed to Deserialize #79
Comments
I would suggest you to use the Symfony Form component for this instead. Binding values in an existing object correspond to 80% of its codebase (the remaining part being the rendering of the HTML). Binding data in an existing object requires totally different work than deserializing. |
Forms could be used - but I lose a lot of control over the serialization/deserialization process that I currently have (and love) with JMS. Second, I can't say I agree that it's totally different work. Isn't the whole purpose of the object constructor to allow one to return already existing objects rather than creating new ones? For example, the Doctrine object constructor works by accepting identifiers in the payload and returning managed entities from that data. I don't see how this request falls outside the realm of that? The object constructor paradigm could also work for me, if I could easily pass arbitrary attributes to it. The only catch here is that I need to be able to pass my identifiers to construct the object from separate from the actual payload. I could hack it to work, I'd just prefer to not hack if there could be an API-supported way of doing it. To comment again on your final sentence: binding data to an existing object should really be the exact same as binding data to a new object. The only difference being that the "existing object" presumably has some attributes already set on it. If, instead of constructing a new object, the current serializer took an already existing object - the rest of the process would be the exact same. |
You can set the attributes on the DeserializationContext which you can access from your object constructor. Did you consider that? |
Am I missing how to get the context from the object constructor? I did consider that approach - and it would work fine - but it doesn't appear to get passed down into the constructor. |
You're right. I think we should add it there. |
I definitely support that approach. Except injecting the context would be a BC break. Thoughts? |
The ObjectConstructorInterface is not exactly user-facing. I think we can slightly change the arguments and add the context. |
+1 from me. I should have some time today to get this working and submit a PR if you're busy. |
@cmorelli The issue is that you would need to handle exisitng objects in many places of the graph, not only at the root. |
@stof That is true - I was only considering the passing of existing objects at the root (not considering the rest of the graph). That being said, I think the solution that @schmittjoh posted is the best way to go. The object constructor can choose if it wants to use any attributes from the DeserializationContext to create its objects. This would work at any level of the graph. |
+1 Any news on this? Same use case here, and I'm currently using a hacky solution where I parse the JMS metadata myself to manually recurse and set properties. It makes much more sense to be able to pass in an already constructed object to handle the same way as creation. Using the form component would work, sure, but... it's a lot of extra code to load for doing something that's almost already doable. I'm not super familiar with the internals, but if someone points me in the right direction I'd be happy to work on this if no one else currently is. I'm about to go on vacation for two weeks, but as soon as I get back I would have time to start on this around June 26th. |
Hi there! @schmittjoh would you accept a PR with that? |
What we can do easily is to add the context as an argument for the object constructor. Then, you can easily implement your own constructor as you need it. |
It looks like this issue was closed from #160, but are there any docs for it anywhere? What I'm curious about is, it looks like in order to deserialize into a preexisting object, I need to change the object constructor. In the SerializerBundle I can do this in the config - but I don't necessarily want to always use that object constructor - I only want to use it in the contexts where I'm receiving data via an API. Should I configure a new serializer service for this case? Edit: Also, does this only handle objects at the root? |
@evillemez you can pass a fallback constructor to your own constructor and call it when no object is passed with serialization context. See DoctrineObjectConstructor - it does the same thing. I think it would be much cleaner than having 2 serializer services . |
@eugene-dounar Ah, ok... I see. From the code, this looks like it only handles serializing into a pre-existing object at the root level of the graph, is that actually the case or am I misunderstanding it? |
Thanks @eugene-dounar -- this is so great! We were going crazy in my office trying to figure out how to do partial updates to Doctrine-ORM-managed entities. Using the deserializer for partial updates (eg We ended up copy/pasting the // module.config.php
'service_manager' => array(
'factories' => array(
// Create a jms serializer as a ZF2 service,
// configured to use the InitializedObjectCtor
'serializer' => function() {
// ...
$defaultObjCtor = new \JMS\Serializer\Construction\UnserializeObjectConstructor();
$initializedObjCtor = new \MyApp\path\to\copy\and\pasted\InitializedObjectCtor($defaultObjCtor);
return \JMS\Serializer\SerializerBuilder::create()
->setObjectConstructor($initializedObjCtor)
->build();
}
)
)
// MyApp\Controller\UserRestController
// ...
public function update($id, array $data) {
// Grab the existing user from the ORM
$user = $this->doctrineEntityManager->findById($id);
// Create a deserialization context, targeting the existing user
$context = new \JMS\Serializer\DeserializationContext();
$context->attributes->set('target', $user);
// Deserialize the data "on to" to the existing user
$this->serializer->deserialize(json_encode($data), 'MyApp\Model\User', 'json', $context);
// Save the updated user
$this->doctrineEntityManager->persist($user);
$this->doctrineEntityManager->flush();
// return ...
} The whole context injection thing is a little rough on the eyes, so we'll probably end up wrapping the JMS Serializer to accept an object as a second param (I'm a JS dev, so I'm totally cool with mixing up my parameter types :p ) |
I followed @eschwartz recommendation and just did a cp vendor/jms/serializer/tests/JMS/Serializer/Tests/Fixtures/InitializedObjectConstructor.php src/myApp/ Modified the namespace in the Can this class be moved into a more proper and permanent location so I don't have to maintain it myself? |
For anyone trying to do this in Symfony, I copied
|
Note about the Symfony helped a lot. Thanks! |
@RedEdgeKatelyn I am not sure code changed or not but your code snippet is not work. Here is the workable version:
|
Thanks @RedEdgeKatelyn and @tom10271 as a Symfony user your solution worked for me. But I couldn't help but feel that this should be soluble through configuration.
This worked for me when I then used JMS serializer: $this->serializer->deserialize($request->getContent(), 'MyApp\Entity\User', 'json'); Only thing to watch out for is that the id of the object must appear in the body of the request, otherwise instead of updating a pre-hydrated instance of User, a new instance of User will be created. Can also confirm that the additional changes suggested in the SO post suggested for Mongo also work:
Works with PHP 7 too :) |
ObjectConstructorInterface has been updated and is possible to get objects from the context attributes |
@goetas : if possible to have an example of implementation for that, i looking to use context attributes but until now $context->attributes->all() have an empty array, i don't know how can i set my object there. |
@karousn you can have a look at it on
|
@goetas I still confused. Could you provide with an example for Symfony 4.2, please? |
@goetas Could you provide with an example for Symfony 5 please? |
https://stackoverflow.com/a/56128315/3419751 @yakobabada this does work for my SF 4.4 @philippeamar |
@philippeamar, all In short : I have a simple working snippet to deserialize an object into another in Symfony 6 🙂 !! In long : I was reading this Open Classrooms great course to learn how to create an API with latest Symfony (6). During the course we needed to replace the Symfony native Serializer by JMS/Serializer in order to make Hateos The solution given in the course was to assign manually each attribute of the updated object to the existing objet. After testing many solutions, I found this which is working and I use it in real life, enjoy 🙂 !! Snippet
<?php
namespace App\Service;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
class InitializedObjectConstructor implements ObjectConstructorInterface
{
private $fallbackConstructor;
/**
* @param ObjectConstructorInterface $fallbackConstructorClassName Fallback object constructor
*/
public function __construct($fallbackConstructorClassName)
{
$this->fallbackConstructor = new $fallbackConstructorClassName();
}
/**
* {@inheritdoc}
*/
public function construct(
DeserializationVisitorInterface $visitor,
ClassMetadata $metadata,
$data,
array $type,
DeserializationContext $context
): ?object {
if ($context->hasAttribute('target') && 1 === $context->getDepth()) {
return $context->getAttribute('target');
}
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
}
services:
...
jms_serializer.object_constructor:
class: App\Service\InitializedObjectConstructor
arguments: ['\JMS\Serializer\Construction\UnserializeObjectConstructor']
#[Route('/api/todos/{id}', name: 'updateOneTodo', methods: 'PUT')]
#[IsGranted('ROLE_ADMIN', message: "You has no sufficient rights to perform this operation")]
public function updateOne(Request $request,
SerializerInterface $serializer,
Todo $todo,
EntityManagerInterface $em): JsonResponse
{
$context = new DeserializationContext();
$context->setAttribute('target', $todo);
$updatedTodo = $serializer->deserialize($request->getContent(), Todo::class, 'json', $context);
$em->persist($updatedTodo);
$em->flush();
return new JsonResponse($serializer->serialize($updatedTodo, 'json'), JsonResponse::HTTP_OK, [], true);
} |
It doesn't appear that there's a way to give an already constructed object to the deserialize method (in which case the serializer would just skip the object construction phase and begin mapping properties).
My use case is simple:
In a REST API, I want to allow a user to create or update objects. Each type of operation (create, update, delete, read) has a different set of security rules. However, with the current implementation of the deserializer (in which we are using the Doctrine object constructor), the user can issue a create request, but specify an "id" as part of the payload. The Doctrine object constructor will then return a mapped entity before deserialization begins, allowing the end user to perform an update (while the server still thinks it's doing a create).
Also, on this point:
I was considering making a separate object constructor - but I need to give some attributes to the object constructor on each use (such as the ID which it should construct an object for). Right now, the only way to get those attributes to the deserializer is to include them in the payload string that gets passed to it - which is precisely what I'd like to avoid.
There are ways to handle this in the actual REST endpoint, but none of them are really ideal.What I'd like to be able to do is pass an already-constructed object (which my REST endpoint will handle fetching) to the deserializer and just have it do it's property mapping onto the object. This way, I can always pass a blank object in create calls, while I can pass a constructed object in update calls.
Would this be possible? I could submit a PR if this is something you'd be willing to implement.
The text was updated successfully, but these errors were encountered: