Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Edit Endpoint & Deserialization

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I want more fancy! Seriously, we're going to add pretty much everything we can think of to make this a sweet, flexible, sort of, file "gallery". What about allowing the user to update a file reference?

Okay, well, we're not going to allow the user to update the actual attached file, there's just no point. Want to upload a newer version of a file? Just delete the old one and upload the new one. Feature, done!

But we could allow them to change the filename. Remember: this is the original filename. And, yea, if they uploaded a file called astronaut.jpeg, it would be totally cool to let them change that to something else after. Let's do it!

The Update API Endpoint

Let's keep thinking about our ArticleReference routes as a set of nice, RESTful API endpoints. We already have an endpoint to create and delete an ArticleReference. This will be an endpoint to edit a reference... except that the only field the user will be allowed to edit will be the originalFilename.

Copy the beginning of our delete endpoint, paste, close it up and we'll call this updateArticleReference(). Keep the same URL, but change the route name to admin_article_update_reference - it should be reference, not references, let's fix that in both places - I don't think I'm referencing that route name anywhere. And instead of methods={"DELETE"}, use methods={"PUT"}.

... lines 1 - 19
class ArticleReferenceAdminController extends BaseController
{
... lines 22 - 132
/**
* @Route("/admin/article/references/{id}", name="admin_article_update_reference", methods={"PUT"})
*/
public function updateArticleReference(ArticleReference $reference, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
$article = $reference->getArticle();
$this->denyAccessUnlessGranted('MANAGE', $article);
}
}

Cool! Let's think about how we want this endpoint to work. First, our JavaScript will send a request with a JSON body that contains the data that should be updated on the ArticleReference. In this case, the data will have only one field: originalFilename.

Deserializing JSON

So far, we've been using $this->json() to turn an object or multiple objects into JSON. This uses Symfony's serializer behind the scenes. Now we're going to use the serializer to do the opposite: to turn JSON back into an ArticleReference object. That's called deserialization and... it's... pretty freakin' awesome!

Let's add a few more arguments: SerializerInterface $serializer and Request - the one from HttpFoundation - so we can read the raw JSON body.

... lines 1 - 15
use Symfony\Component\Serializer\SerializerInterface;
... lines 17 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 162
}
}

To automagically turn the JSON into an ArticleReference object, say $serializer->deserialize(). The serializer only has these two methods: serialize() and deserialize().

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 141
$serializer->deserialize(
... lines 143 - 149
);
... lines 151 - 162
}
}

This method needs the raw JSON from the request - that's $request->getContent(), what type of object to turn this into - ArticleReference::class - and the format of the data: json, because the serializer can also handle XML or any crazy format you dream up.

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 141
$serializer->deserialize(
$request->getContent(),
ArticleReference::class,
'json',
... lines 146 - 149
);
... lines 151 - 162
}
}

Finally, we can pass some options - called "context". By default, deserialize() will always create a new object... but we want it to update an existing object. To do that, pass an option called object_to_populate set to $reference.

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 141
$serializer->deserialize(
$request->getContent(),
ArticleReference::class,
'json',
[
'object_to_populate' => $reference,
... line 148
]
);
... lines 151 - 162
}
}

Oh, and when we've been serializing, we've been passing a groups option, which tells the serializer to put the properties from the "main" group into the JSON. We can do the same thing here: we don't want a clever user to be able to update the internal filename or the id: we need to restrict their power to changing the originalFilename.

Above $originalFilename, turn the groups value into an array and give it a second group: input.

... lines 1 - 11
class ArticleReference
{
... lines 14 - 33
/**
... line 35
* @Groups({"main", "input"})
*/
private $originalFilename;
... lines 39 - 100
}

In the controller, way back down here, set groups to input. So if any other fields or passed, they'll just be ignored.

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 141
$serializer->deserialize(
$request->getContent(),
ArticleReference::class,
'json',
[
'object_to_populate' => $reference,
'groups' => ['input']
]
);
... lines 151 - 162
}
}

And... yea, that's it! We do need to think about validation - but, pff, we'll handle that later - like in 2 minutes. Right now we can celebrate with $entityManager->persist($reference)... which we technically don't need because this isn't a new object, but I usually add it, and $entityManager->flush().

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 151
$entityManager->persist($reference);
$entityManager->flush();
... lines 154 - 162
}
}

What should we return? Typically after you edit a resource in an API, we return that resource again. Scroll all the way up to our upload endpoint and steal the JSON logic. We could also refactor this into a private method if we wanted to avoid duplication. Back down in our method, paste, rename the variable to $reference and use 200 as the status code: we're not creating a resource in this case.

... lines 1 - 20
class ArticleReferenceAdminController extends BaseController
{
... lines 23 - 136
public function updateArticleReference(ArticleReference $reference, EntityManagerInterface $entityManager, SerializerInterface $serializer, Request $request)
{
... lines 139 - 154
return $this->json(
$reference,
200,
[],
[
'groups' => ['main']
]
);
}
}

Ok, that endpoint should be good! Or at least, we're ready to hook up our JavaScript so we can find out if it explodes when we use it! That's next.

Leave a comment!

7
Login or Register to join the conversation
Diana E. Avatar
Diana E. Avatar Diana E. | posted 1 year ago | edited

Hi Ryan, thank you for the video. This block is not updating the passed reference obj


$serializer->deserialize(
            $request->getContent(), // raw JSON from request
            ArticleReference::class, // type of obj to turn it into
            'json', // data format
            [
                'object_to_populate' => $reference, // by default creates new obj. Instead update passed obj
                'groups' => ['input']
            ]
        );

If I dump $reference right after, I get the same old values and the file name is not updated. Any thoughts?

P.S. I even copy/pasted your code to make sure I hadn't mistyped something, but it's the same thing.

Your help would be very much appreciated.

Thanks

Reply

Hi Diana E.!

Hmmm. I'm not sure what's going on here, so let's try a few things:

A) Try setting the return value of deserialize() to a value and dump it. Also dump $reference:


$deserializedReference = $serializer->deserialize(
    $request->getContent(), // raw JSON from request
    ArticleReference::class, // type of obj to turn it into
    'json', // data format
    [
        'object_to_populate' => $reference, // by default creates new obj. Instead update passed obj
        'groups' => ['input']
    ]
);
dump($deserializedReference);
dump($reference);

The question: are these objects identical or are these 2 different objects?

B) If they ARE identical, then my guess is that the object_to_populate system IS working... but for some reason, the filename is not being updated. To test this theory, remove the object_to_populate option and dump($deserializedReference);. Is the filename set in this new object or not?

Let me know what you find out!

Cheers!

Reply
Diana E. Avatar
Diana E. Avatar Diana E. | weaverryan | posted 1 year ago | edited

Thanks for your response Ryan. In A, I do get identical objects:


ArticleReference {#786 ▼
-id: 1
-article: Article {#833 ▶}
-filename: "mg-6694_6124e8eb927da.jpeg"
-originalFilename: "_MG_6694.jpg"
-mimeType: "image/jpeg"
}

ArticleReference {#786 ▼
-id: 1
-article: Article {#833 ▶}
-filename: "mg-6694_6124e8eb927da.jpeg"
-originalFilename: "_MG_6694.jpg"
-mimeType: "image/jpeg"
}




The originalFilename above is the old value, not the updated one.


In B, I get the following error if I remove this line `'object_to_populate' => $reference,`

Cannot create an instance of App\Entity\ArticleReference from serialized data because its constructor requires parameter "article" to be present.
Reply

Hey Diana E. !

Hmm. Ok! This tells me that the object_to_populate system IS working: the serializer IS trying to update your existing object.

Ok, so to review, the goal is to be able to send an originalFilename JSON property and have it update the originalFilename property on your entity. There are a few things to check out (you may have checked some of these already, but just in case):

1) Make sure you have a setOriginalFilename() method
2) Make sure that the originalFilename property is in the input serialization group
3) dump($request->getContent()) on your controller to be absolutely sure that this contains a originalFilename field.
4) Add a temporary dump($originalFilename) inside of your setOriginalFilename() method to make sure that this code is being executed.

As a reminder, to see of those dump() lines are being hit, after triggering the AJAX call, you can use this trick to see the profiler (and then the "dump" tab on the profiler) to see what those output: https://symfonycasts.com/screencast/symfony-uploads/dropzone

Let me know what you find out!

Cheers!

Reply
Diana E. Avatar

Thanks a lot for your help, Ryan. The issue was no. 2 (serialisation group).

1 Reply
Default user avatar
Default user avatar Hamza Yahiaoui | posted 2 years ago

i dunno if you guys check comments for this article yet but i have a question what if we use react instead of twig ? how can our api send files to our front so we can update them and send them back ? any suggestions would be appreciated !

Reply

Hey @Hamza!

> how can our api send files to our front so we can update them and send them back

Hmm. So this sounds like 2 parts.

First "how can can the API return files that we've already uploaded". The answer to this is no different if you're using React or something else - it's more of a general "API question". I think there are generally 2 approaches:

1) You have an endpoint that returns information about the file (e.g. filename) including one field that is the base64 encoded version of the binary string. This works great for smaller files (base64 encoding makes file sizes a bit bigger).

2) You have an endpoint that returns information about the file (e.g. filename) including one field that is the URL to where the resource could be downloaded, like a URL on your site (e.g. even /uploads/file/foo.jpg) or on a CDN, like cloudfront.

Second "how can we send a new file back to the server"?

We talk about that here: https://symfonycasts.com/sc... - and it shouldn't be any different if you're using React.

Let me know if that helps :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}
userVoice