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!
20.
Collection "Types" and readableLink
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
}
}
20 Comments
Hi,
There is a typo in the section Property Metadata for Collections where the first code snippet miss a @ on the SerializedName annotation.
Also, for that section (Property Metadata for Collections), for it to work you need to have the package "phpdocumentor/reflection-docblock" in your project. That package is necessary for the PhpDocExtractor.
Hey Ivanis,
Yeah, that "@" was removed on purpose - it the same as remove the line completely. We removed it completely in the video, but in the code we only removed that "@" in front of it. But to avoid misleading, I updated the code block hiding this, I think it's better now. Thanks!
Cheers!
Hey Ivanis,
Yeah, we have that "phpdocumentor/reflection-docblock" package in project's composer.json file - we had it installed even before started working on this course project. But if you're following this course on a fresh Symfony version from scratch - you need to install it yourself I suppose, thanks for mentioning it!
Cheers!
Hi folks , we are having trouble to accomplish to get only the IRI using DTOs,
This is the result we get, even using the readableLink to false does not change the behavior.
This is the code we are using, any idea on what we are doing wrong ? Thank you in advance
{
}
Hey Auro !
Hmm, yea, this looks quite a lot like the problem that we were solving in this tutorial, though "output" objects have their own, extra "quirks" (some of which have been recently fixed but not released yet - we'll talk about them later in the tutorial). Here is one of the bugs we discovered - and work around later in the tutorial - https://github.com/api-platform/core/pull/3696
But we work around this by adding
@ApiProperty(readableLink=false).Just to make sure, where are you putting the readableLink=false? Above the
workOrderandworkerproperties, right? If so, I can't think of why that wouldn't work... though you have output objects on top of output objects, which could be causing some problem (but I can't picture what problem that would cause).Let me know - sorry I can't be much help on this yet.
Cheers!
Hy Ryan, thank you for the answer.
Yes we tried putting @ApiProperty(readableLink=false) above the workOrder & worker properties. In fact putting it to true do have some effect.
You cited "you have output objects on top of output objects", is there an other way to achieve this ? We could'nt find any example on the web on how to use DTO with embedded properties.
Thank you so much
Hey Auro!
> In fact putting it to true do have some effect.
What effect did it have?
> You cited "you have output objects on top of output objects", is there an other way to achieve this ? We could'nt find any example on the web on how to use DTO with embedded properties.
Well... the problem is just that there are some "quirks" about how DTO's are serialized. So, the other way to achieve this would be to not use DTO's... but I'm guessing you don't want to do that just to fix this small issue ;).
The problem may be that the logic to "use an IRI vs embedded object" lives (I believe) in the AbstractItemNormalizer - https://github.com/api-plat...
The problem is that (again, I "think" this is true, but it's complex and I haven't double-checked) that when a DTO is being serialized, it does not use the "item normalizer" because it is not technically an "API Resource item". And so, when WorkOrderTaskOutput is serialized, it doesn't use this normalizer... and thus never hits the code I linked to. I would say that this IS a bug... but it's complicated.
I'm honestly not sure of a solution... there IS a solution if you're serializing a non-item resource and the property you're serializing IS an "item resource", but in this case, neither are true "item resources" (they're both DTO's). I just can't see a hook for accomplishing this.
You might be able to create a custom normalizer to do this. It would do something like this:
A) Make it support only EmployeeOutput to start (but you could maybe make it generic)
B) Call the "main" normalizers and get the result
C) If the result is an array and it's empty (or, maybe it will have @id and @type on it), then generate an IRI string and return *that* instead of the array of data.
Let me know if that works or if you find something else :).
Cheers!
Hey weaverryan
I've made more digging and this is the conclusion.
- If I comment the output of employee which is EmployeeOutput it works fine, it returns the IRI.
- If I disable the EmployeeOutputDataTransformer, it also works fine and returns the IRI.
So basically the problem is that the EmployeeOutputDataTransformer returns true and it uses its behavior, the solution would be to be able to return true or false in function of some parameters so it does not always use the EmployeeOutputDataTransformer, only if deserializing data for the employee resources and not when it's embedded. Unfortunately I haven't fine anything useful in the provided context, and either a way to pass some extra information in the context,
The effect of putting @ApiProperty(readableLink=true) is regarding the swagger documentation, by default it shows a string even if it serializes the whole entity.
Do you have any Idea of how I could solve this ?
Hey Auro!
Yea, both of these have the same effect: they disable the output. If you return false from supportsTransformation, then the DTO is skipped - it's (as you're correctly seeing) the same as not having a DTO. And since DTO's are the problem, yea, it makes sense this would solve it :).
Hmm, context is what I was also going to look at first. What about checking
$request->attributes->get('data')? That should contain the "top-level" object that is currently being serialized. So you should be able to use this to know if Employee is being serialized or some other entity (and so, Employee is embedded).We're definitely working with some hackery at this point. So even if this works, it doesn't feel super great - I would just check closely for unintended side effects. Also, there was my custom normalizer solution above as another option.
Cheers!
Hey weaverryan,
I've tried your idea of creating a custom normalizer solution and it works perfectly. Here is the code if it can help someone else:
Just one more question, do you know if there is a way to add some personalized information in the context using the @ApiProperty annotation or any other mechanism?
Thanks a lot for your help
Hey Auro
Thanks for your code.
I'm having hard time implementing your solution :
When I serialize the parent attribute (let's call it
categorywhich refered to theCategoryOutput) of the child DTO (productOutput), I get this behaviors :CategoryOutput->nameproperty is tagged with "product:read" serialization group, the$datareturned by the default serialization process inside theSocietyNormalizer::transformmethod is and object containing "@id" and "name" keys. I believe it's expected .$datain empty. Even the "@type" or "@id" aren't set.I tried to set
@ApiProperty(readableLink=true|false)but it doesn't have any effect.Any ideas what I do wrong?
Thanks
EDIT :
After some digging, I realize I "exit" the
ApiPlatform\Core\JsonLd\Serializer\ObjectNormalizer::normalizemethod on line 86 on :`
$data = $this->decorated->normalize($object, $format, $context);
[...]
return $metadata + $data
`.
The metadata that you use in your code (
$data['@id']) are set after that. How did you manage to normalize your "product" DTO with metadata?Cheers ! (as Lucien D. would say)
Hey Lucien D. !
I'm not sure if Julien will also comment to help... but it's been a LONG time since I had my brain wrapped around this deep situation :). I can kind of understand what you're talking about... but it's super complex. It's possible that the solution might need to (or not need to!) use the "normalizer aware" strategy: https://symfonycasts.com/screencast/api-platform-security/normalizer-aware... or maybe that will make no difference :p.
In theory, your OutputNormalizer "should" be able to call into the "main normalizer" system and let it completely finish. This should, iirc, return with the JSON ld fields like @id, just like normalizing normally would. Then you could add your logic. It seems like your situation is where the "main normalizer system" is called first... and then
$this->decorated->normalize(...)is what calls your code (this is a guess, but that's what I'm thinking). We want this to happen in reverse: we want your normalizer to be called first and then you call INTO the normal "normalizer".This was a pretty fluffy and non-specific reply... just trying to "poke" at the problem to see if anything else :).
Cheers ;)
Hey weaverryan ,
Thanks for the quick reply. The "poking" strategy seems to me the good one, I don't have any more wall to hit with my head, need some new leads... :D
As you clarified it, the problem happens before the custom logic. Even before, I get an empty array for the
Categoryattribute of theProductobject. Worst: when I deactivate all the custom normalizer, I face the same problem.As you suggest earlier, I made the custom normalizer only support the
CategoryOutputto simplify and apply the "normalizer aware" strategy.Some ideas :
There are several <i>peripherical</i> class around the
Categoryclass (the proxy, the output DTO) and maybe I try to apply my custom normalizer on the wrong one. ThesupportNormalizationmethod return$data instanceof CategoryOutput && !isset($context[self::ALREADY_CALLED]);and theProductOutputclass has aCategoryattribute.The wrong normalizer is called : it's the
AbstractObjectNormalizerfrom the namespaceSymfony\Component\Serializer\Normalizerwich seems responsable to normalize theCategoryOutputand I don't figure out the when and how the '@id' are <i>calculated</i>. The serialization chain seems to be :ApiPlatform\Core\Serializer\AbstractItemNormalizer::normalizemethod is hit on theCategoryobjectCategoryOutput)CategoryNormalizeris hit on this callCategoryNormalizercall the main normalization process on theCategoryOutputobjectApiPlatform\Core\JsonLd\Serializer\ObjectNormalizeris hit$context['api_empty_resource_as_iri'] = true. There is a big comment in the Api Platform code wich stipulate <i>Converts the normalized data array of a resource into an IRI, if the normalized data array is empty</i>. It's intended for theApiPlatform\Core\Serializer\AbstractItemNormalizer::normalize()method : line 163Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer) is hit and return an empty array.CategoryNormalizer.I try to create a
UserOutputstarting from the finish code of this course and face the same problem...Don't have any more walls to hit ...
Thanks
Hey Lucien D.!
Hmm. Indeed, you may not have any more walls to hit! It seems like (and forgive me if i'm forgetting something obvious, I'm not nearly as deep on the code as you are at the moment) the problem is the output class. Inside
JsonLd\Serializer\ObjectNormalizer, when it calls$this->decorated->normalize($object, $format, $context);, you WANT that to ultimately callApiPlatform\Serializer\AbstractItemNormalizer- https://github.com/api-platform/core/blob/60240f506802883a8f02b7015b9d676f8a2897d5/src/Serializer/AbstractItemNormalizer.php - because this is where thatapi_empty_resource_as_iriis read and used to return the IRI. But if the underlying object isCategoryOutput, then it makes sense that AbstractItemNormalizer is NOT called... because this only operates on "API Resources" (Category is an ApiResource, but not CategoryOutput). And so, instead of AbstractItemNormalizer being called, AbstractObjectNormalizer (from Symfony's core) is called... and you never get your IRI.In general, input & output DTO objects have, unfortunately, "quirks" like this. And so, it doesn't surprise me that this doesn't quite act right. My best advice would be to see if you can work around it, perhaps by generating the IRI manually inside of your custom normalizer - iirc there IS a service that does this: IriConverterInterface.
Sorry I can't help more - but at least I don't see any wall you are missing ;).
Cheers!
Hey Auro !
Ah, brilliant! Thanks for much for trying this and sharing your solution. That's awesome!!!
> Just one more question, do you know if there is a way to add some personalized information in the context using the @ApiProperty annotation or any other mechanism?
I'm not sure I understand the question. Do you mean that you want to add a custom field to your resource whose data depends on the currently-authenticated user? I have a feeling I'm guessing wrong... let me know :).
Cheers!
Hey weaverryan,
The DataTransformerInterface has the following method:
`
`
I was wondering if I can add some custom information in the <b>$context</b> variable, maybe through the <b>@ApiProperty</b>
annotation. Something like embedded=false that I can retrieve in the context later. I saw that it has <b>openapiContext</b> & <b>jsonldContext</b> attributes but I didn't find information on how api-platform use them internally.
The normalizer solution has one drawback, If I have a DTO that serializes a Category and I want to serialize a property parent which is of the same type, then the solution does not work because they share the same groups.
Thanks
Hey Auro!
Hmmm. So I assume that you're thinking of this setup. Let's use Product and Category as a simpler example, where Product has a category property.
A) You want to add something like
@ApiProperty(embedded=false)(or something similar) to thecategoryproperty onProduct.B) Then, when the data transformer for Category is called (from a situation where Product is actually being serialized, and now due to the
categoryproperty, Category is being serialized) you would like to read that embedded false from the$contextsomehow so that you can return false from supportsTransformation().Do I understand correctly? If so... hmmm. I'm having a hard time seeing a hook for this. Here's one idea... but even as I type it, I'm not sure it will work:
1) You setup a custom resource metadata factory, like we did here: https://symfonycasts.com/screencast/api-platform-security/resource-metadata-factory
2) Inside, on a high level, you would look for your custom "embedded=false" thing on all of the properties and add a NEW key to the context - something like
$context['non_embedded_properties'] = [...]. This would hold, in our situation, an array with a single key - "category". I'm not talking yet about how to do this - I think we should test it first in a simple way before going there. But the idea would be that you would be adding this new context key to the Product object's context. But then, I think when a Category is being normalized, it will share part of the context. It would be a bit weak - in supportsTransformation() for Category, you would be looking for anon_embedded_propertieskey on the $context with the entry "category" in it... so you would be looking for this exact string. You can see that it's pretty weak - it's not that we're directly passing something to the context that says "do not embed this object", we're merely creating a list of properties on Product that should not be embedded... and then expecting Category to use this :/.So... I'm not really sure. Maybe another option is to create a custom normalizer that works for all objects (or at least ApiPlatform resources / items) and adds something to the context that is, kind of, a "stack" of what class is currently being serialized - something like:
If you had something like this, inside supportsTransformation(), you could check to see what class was normalized before you - was it Product? Or nothing. But, there could be snags with this approach - I'm just dreaming it up.
Cheers!
In those two quotes from the script, which one is right?
> set readableLink to false to force it to be embedded.
> and readableLink=false says to use an IRI link...
Hey @niahoo!
Oh geez - I see it - it looks like I confused myself. This property has always felt backwards to me. The correct is:
> and readableLink=false says to use an IRI link
or, to say it the other way:
> readableLink=true says to use an embedded
I hope that helps - and sorry about that!
Cheers!
"Houston: no signs of life"
Start the conversation!