Chapters
48 Chapters
|
5:13:28
|
Login to bookmark this video
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
47.
Setting the UUID on POST
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Subscribe to jump to this part in the video!
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.
This tutorial also works great with API Platform 2.6.
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.5.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.1.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.4.5", // 2.8.2
"nelmio/cors-bundle": "^2.1", // 2.1.0
"nesbot/carbon": "^2.17", // 2.39.1
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
"ramsey/uuid-doctrine": "^1.6", // 1.6.0
"symfony/asset": "5.1.*", // v5.1.5
"symfony/console": "5.1.*", // v5.1.5
"symfony/debug-bundle": "5.1.*", // v5.1.5
"symfony/dotenv": "5.1.*", // v5.1.5
"symfony/expression-language": "5.1.*", // v5.1.5
"symfony/flex": "^1.1", // v1.21.6
"symfony/framework-bundle": "5.1.*", // v5.1.5
"symfony/http-client": "5.1.*", // v5.1.5
"symfony/monolog-bundle": "^3.4", // v3.5.0
"symfony/security-bundle": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/validator": "5.1.*", // v5.1.5
"symfony/webpack-encore-bundle": "^1.6", // v1.8.0
"symfony/yaml": "5.1.*" // v5.1.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
"symfony/browser-kit": "5.1.*", // v5.1.5
"symfony/css-selector": "5.1.*", // v5.1.5
"symfony/maker-bundle": "^1.11", // v1.23.0
"symfony/phpunit-bridge": "5.1.*", // v5.1.5
"symfony/stopwatch": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/web-profiler-bundle": "5.1.*", // v5.1.5
"zenstruck/foundry": "^1.1" // v1.8.0
}
}
10 Comments
Hi Symfony Casts,
Right now I'm working with Symfony 7.
Can i use a different tool now, instead of ramsey?
Because i have the same problems as described by Nathanael.
Thank you, keep up the good work!
Annemieke
Hey @Annemieke-B
Yes, you can use the Symfony UID component by running
composer require symfony/uid- There are a few ways to set up the id on your record but the right approach depends on your use-case. Here are the docs for reference https://symfony.com/doc/current/components/uid.htmlCheers!
Hello!
I am finding that using
UuidInterfaceas a constructor argument type is not "magically" resulting in passed strings being converted into UUIDs. When I attempt to use the endpoint (and pass a UUID) I end up with the following error message. This error message makes perfect sense, but is contrary to the behavior this tutorial describes. Instead, I need to use strings.Error message:
Argument #1 ($uuid) must be of type ?Ramsey\\Uuid\\UuidInterface, string givenI tried creating my own normalizer, but I could not get it to fire the way it's "supposed" to. Is there some undocumented setting or something that I'm missing? My understanding is this should work out of the box with no additional service declarations.
Example code:
Hey Nathanael!
That's interesting. Yes, the
UuidInterfacetype should make it so that this denormalizer - https://github.com/api-platform/core/blob/2.7/src/RamseyUuid/Serializer/UuidDenormalizer.php - is called to convert the string into a Uuid. I'd try adding some debug code directly to this method. What I'm curious about is (A) issupportsDenormalization()called when you make the POST request (B) does it return true appropriately and (C) if it does, isdenormalize()doing its job correctly?Let me know what you find out - I'm not sure what could be going wrong.
Cheers!
Hey Ryan!
Nope, already tried that. It's not called at all. I also tried to manually (re)declare it in
services.yamlto no effect.A possibly helpful hint: when I create my own denormalizer, it does fire, but only on the entity class. In other words,
$datais the request payload (as an array), and$typeisApp\Entity\Message(per the above example). At no point is the actual constructor argument being passed through the denormalizer.Hey Nathanael!
Ok, so I played with the
finishcode from the repo to see what was going on. First, just to clarify (and I also couldn't remember that this was the case), the denormalization from a UUID string to an object does not happen due to theUuidInterfacetype-hint in the constructor. Once you have it working (like it is in thefinishcode, well, once I undid some changes from the next chapter that cloud things), you can remove theUuidInterfacetype-hint and it'll still work. Instead, denormalization works because the argument is called$uuid, and so the serializer looks at theuuidproperty and gets the metadata from there (and, in this case, iirc, it's theORM\Columntype that provides the metadata needed for the serializer).Anyways, the more important detail is that I was wrong about the class that handles the denormalization. It is actually
UuidDenormalizer- https://github.com/api-platform/core/blob/2.7/src/RamseyUuid/Serializer/UuidDenormalizer.php - so not theUuidNormalizerthat I had linked to earlier - that one is in theIdentifiernamespace and is used for something different.So, that is where I would look for debugging. When I run
debug:container uuid, the denormalizer's service id isapi_platform.serializer.uuid_denormalizer, though it's possible the service id is slightly different in newer versions. I'd check to make sure that service is there.If the
UuidDenormalizerservice IS present and it's not being called, then this statement becomes interesting. This would tell me that either (A) you're missing some metadata to tell the serializer that the$uuidargument is aUuidInterfaceOR the serializer thinks that the$uuidshouldn't be allowed to be passed at all. This is pretty simple to test: remove theUuidInterfacetype-hint from the constructor argument thendd($uuid). If this dumpsnull, then it points to the idea that theuuidfield is not a valid field to send with the request at all. In that case, triple-check your serialization groups. If it dumps a string, then the field is being allowed, but for some reason, the serializer doesn't know it should be aUuidInterfaceand so it is not triggering the denormalizer.Let me know what you find out!
Cheers!
It looks like we're in "interesting" territory, then!
First, thanks for the additional insight. I'm wondering now what metadata is required—I had assumed (maybe correctly...?) that all denormalizers (probably with some tag) would be checked in sequence, and then the first one for which
DenormalizerInterface#supportsDenormalization()returned true would be used to mutate the data passed with the request. This would then either be used to set a property, or in my case, pass an argument to__construct().To answer your questions quickly:
A) My property is called
$uuidand my constructor argument is also called$uuid.B) The
$uuidproperty is type-hinted as being an instance ofUuidInterface.C) The Doctrine column has the
uuidtype (though I feel like that shouldn't matter—not all API fields need to be mapped to the database).D) The
$uuidconstructor argument is being passed to the constructor as expected. However, it is not being denormalized. When I remove the type-hint, I get no errors, but I receive a string. I then have to manually "denormalize" that string myself, in the constructor, like so:This is obviously wrong, and causes some complications (such being unable to properly validate
$uuidduring POST with validation constraints).Your
debug:containeridea was a good one, though, and here's what I found:Two things stand out to me here:
A) The service does exist.
B) The "service or alias has been removed" message—I'm not sure what that means, and I couldn't find any explanation online. I've never seen it before.
Again,
UuidDenormalizer#supportsDenormalization()is not being called at any point during POST.For what it's worth, here is a complete entity with which I can reproduce the issue, and here is the stack trace for this simple request:
Let me know if you have any insights. I appreciate the help so far.
Hey Nathanael!
An interesting mystery indeed! Fortunately, you seem like you're quite comfortable doing some deep debugging, so I think we (you) should be able to figure this out :). First:
This is ok... or more accurately, it is probably ok, though the message doesn't really tell us for sure. If a service is not referenced by ANYONE else, it is removed (and that WOULD be a hint that something is wrong, though I don't think this is what's happening). If a service is only referenced by ONE other service, then it is "inlined", which is a super not important thing to know, honestly :). It is an internal optimization to how the container is dumped in the cache. It is highly likely that this service is being referenced by just one other service (the normalizer) and thus is being "inlined". That is totally fine.
My instinct is that the service is OK, but some "type" issue is causing the uuid to not be denormalized, though I don't see any issues with your code. Fortunately, you sent me that very nice stack trace, which I think we can use to debug! Specifically, the
AbstractItemNormalizerseems to be responsible for creating the constructor arguments - one of the errors in your stacktrace comes from this line - https://github.com/api-platform/core/blob/52a72743028d78255c14870b80eeb195e91740d8/src/Serializer/AbstractItemNormalizer.php#L286 (it's line 283 in your stack trace, due to a slightly different version).I'd recommend adding some debug code ABOVE this to figure out what's going wrong. Just by reading the code (and it's complex, so I could be wrong), my guess is that, for uuid, the following happens:
A) https://github.com/api-platform/core/blob/52a72743028d78255c14870b80eeb195e91740d8/src/Serializer/AbstractItemNormalizer.php#L272 is called
B) That calls
createAttributeValue()- https://github.com/api-platform/core/blob/52a72743028d78255c14870b80eeb195e91740d8/src/Serializer/AbstractItemNormalizer.php#L315C) Something goes wrong in
createAttributeValue(): https://github.com/api-platform/core/blob/52a72743028d78255c14870b80eeb195e91740d8/src/Serializer/AbstractItemNormalizer.php#L673In
createAttributeValue(), we would hope that the UUID value would be sent through the denormalizer system - e.g. this line https://github.com/api-platform/core/blob/52a72743028d78255c14870b80eeb195e91740d8/src/Serializer/AbstractItemNormalizer.php#L743 - but I'm not sure that's happening. But if it IS happening, then it's possible that your uuid IS being denormalized... but then some OTHER denormalizer with a higher priority is returningtruefrom its supports method... which is why the one for the UUID never has a chance to be called. You can get a list of all of the denormalizers by running:Let me know what you find out!
Cheers!
Hello again,
Unfortunately, for all my trying, I've still had no luck. I did discover a mistake in something I said previously—the denormalizer is being called, and an environment issue was preventing me from seeing the result of my kill script—but what the denormalizer is receiving is an array (i.e., the data passed to the endpoint, as strings), not a single value. As a result, the
uuidproperty is never being convered to a UUID object, and when it's finally passed to the constructor (see: the pieces of code you linked), it's still a string. That mistake of mine is probably a big hint, but it's not one I was able to make any headway with.I took a look at the normalizer list by running the command you recommended and nothing really stood out to me. I noticed that
api_platform.serializer.uuid_denormalizerhas no priority, but the service is explicitly declared without one in/vendor/api-platform/core/src/Symfony/Bundle/Resources/config/ramsey_uuid.xmlso I'm guessing that's not relevant.Hey Nathanael!
Well, let me see if I can give a few more hints to help :). I'm playing with the "final" version of the code from this tutorial. So, it is possible that something has changed in newer versions. By comparing my results to your's, perhaps you can find that difference.
As I mentioned earlier, the class that's responsible for getting the constructor arguments to
UserisAbstractItemNormalizer. For testing, my constructor looks like this:And I'm using the
testCreateUserWithUuid(), which looks like this (notice I'm still passinguuid:Both of these represent tiny differences from the end of the tutorial (this is basically how the code look 5 minutes before the end of the tutorial).
Anyways,
AbstractItemNormalizer::instantiateObject()is where we're looking. So let's look at some debugging facts:A) If I
dd($constructorParameters)here - https://github.com/api-platform/core/blob/022fb6a05701fa5a08f6357c82c1b95fbf0fe4b6/src/Serializer/AbstractItemNormalizer.php#L401 - I get:No surprise there.
B) If I
dd($data)on that same line, again, no surprises:C) For the one argument -
uuid- I get into thisifstatement: https://github.com/api-platform/core/blob/022fb6a05701fa5a08f6357c82c1b95fbf0fe4b6/src/Serializer/AbstractItemNormalizer.php#L419 - and if Idd($data[$key]), I get (again, no surprise), some string like76e8daa1-d01b-48b7-bfaf-c7e07664370bD) So, we follow this into the
createConstructorArgument()method. Btw, it doesn't seem to matter, but just an FYI. When Idd($this), the actual instance isApiPlatform\Core\Serializer\ItemNormalizer. Just keep that in mind, in case you follow some method and, unlike my code, it's overridden in a sub-class. Anyways, we followcreateConstructorArgument()... which leads us tocreateAttributeValue(): https://github.com/api-platform/core/blob/022fb6a05701fa5a08f6357c82c1b95fbf0fe4b6/src/Serializer/AbstractItemNormalizer.php#L465E) We're now in
AbstractItemNormalizer::createAttributeValue(). So let'sdd($propertyMetadata)right at the start - right after this - https://github.com/api-platform/core/blob/022fb6a05701fa5a08f6357c82c1b95fbf0fe4b6/src/Serializer/AbstractItemNormalizer.php#L939 - when I do that, I see:This is the first spot where, possibly, you might see something different that matters. The most important thing is the
type.F) Following the logic, my code eventually ends up in this
ifstatement: https://github.com/api-platform/core/blob/022fb6a05701fa5a08f6357c82c1b95fbf0fe4b6/src/Serializer/AbstractItemNormalizer.php#L1000 - which means, not surprisingly, that the string uuid goes through the denormalizer system.G) The question is, WHICH denormalizer handles the uuid? The answer is, in my project,
ApiPlatform\Core\Bridge\RamseyUuid\Serializer\UuidDenormalizer. This is the same class I mentioned earlier, except in 2.7 it has a new namespace - https://github.com/api-platform/core/blob/2.7/src/RamseyUuid/Serializer/UuidDenormalizer.php - if Ivar_dump($data)(for some reason, in this situation,dump()got swallowed and showed nothing) on the first line ofsupports()and run the test, it is called TWO times:First time:
Second time:
The 2nd time is when the UUID is actually being denormalized. The first time is when the entire object is being denormalized, and then this returns false.
Soooooo, that's the FULL story of how my string UUID becomes a Uuid object. I hope this helps you see why and where your situation is different.
Cheers!
"Houston: no signs of life"
Start the conversation!