Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Collection "Types" and readableLink

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 $12.00

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

Login Subscribe

Something a bit odd just happened: in order for API Platform to correctly serialize the mostPopularListings collection, we had to explicitly tell it what was inside the collection. Why?

To learn what's going on, let's look at another example. Inside User, we have a cheeseListings property, which is writable in our API, but isn't readable:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 77
/**
... line 79
* @Groups({"user:write"})
... line 81
*/
private $cheeseListings;
... lines 84 - 286
}

There is also a getPublishedCheeseListings() method, which is part of the API and we actually gave it the cheeseListings name:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 209
/**
* @Groups({"user:read"})
* @SerializedName("cheeseListings")
*/
public function getPublishedCheeseListings(): Collection
{
... lines 216 - 218
}
... lines 220 - 286
}

Let's put in our lab coats and do an experiment! Science! Start by removing the SerializedName annotation:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 209
/**
... line 211
* @SerializedName("cheeseListings")
*/
public function getPublishedCheeseListings(): Collection
{
... lines 216 - 218
}
... lines 220 - 286
}

We're still going to expose this method, but it will use its natural name: publishedCheeseListings. Then, up on the cheeseListings property add user:read to also expose this:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 77
/**
... line 79
* @Groups({"user:write", "user:read"})
... line 81
*/
private $cheeseListings;
... lines 84 - 286
}

Let's see what it looks like! Head over to /api/users.jsonld and... cool! Each User now has cheeseListings and publishedCheeseListings properties and they're both embedded objects. The reason why is that the $title and $price properties in CheeseListing have the user:read group.

Let's remove those temporarily. Go into CheeseListing and take user:read off of $title and user:read off of $price:

... lines 1 - 58
class CheeseListing
{
... lines 61 - 67
/**
... line 69
* @Groups({"cheese:read", "cheese:write", "user:write"})
... lines 71 - 76
*/
private $title;
... lines 79 - 86
/**
... lines 88 - 90
* @Groups({"cheese:read", "cheese:write", "user:write"})
... line 92
*/
private $price;
... lines 95 - 217
}

Thanks to this change, when API Platform goes to serialize these two array fields, it will realize that there are no embedded properties and return an array of IRI strings.

But... surprise! When we refresh, cheeseListings is an array of IRI strings, but check out publishedCheeseListings! It's still an array of embedded objects! Other than the fact that publishedCheeseListings may have less items in it, these two fields return the same thing! And yet, they're being serialized in different ways!

Property Metadata for Collections

Here's the deal. We know that API Platform collects a lot of metadata about each property, like its type and whether it's required. And it gets that from many different sources like Doctrine metadata and our own PHPDoc.

And because collecting all of this can take time, it caches it. Now, API Platform is really good in dev mode at knowing when it needs to rebuild that cache. Like, if we add more PHPDoc to a property, it rebuilds. And so, even though it's caching all of this, we don't really notice it.

And this metadata collection process happens before API Platform starts handling any request, which means the metadata is built purely by looking at our code. Right now, when it looks at the cheeseListings property, it knows that this is an array of CheeseListing objects thanks to the Doctrine annotations:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 77
/**
* @ORM\OneToMany(targetEntity="App\Entity\CheeseListing", mappedBy="owner", cascade={"persist"}, orphanRemoval=true)
... lines 80 - 81
*/
private $cheeseListings;
... lines 84 - 286
}

But it does not know that getPublishedCheeseListings() returns a collection of CheeseListing objects:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 209
/**
* @Groups({"user:read"})
... line 212
*/
public function getPublishedCheeseListings(): Collection
{
... lines 216 - 218
}
... lines 220 - 286
}

It does know that its a Collection... but not what's inside that Collection.

Why is this a problem? Well, whenever API platform serializes a collection, before it even starts, it asks its own metadata: what is this a collection of? If the "thing" that's being serialized is a collection of objects that are an API Resource class - like the cheeseListings property - then it calls one set of code that knows how to handle this. But if it's an array of anything else - which is what happens down in getPublishedCheeseListings() since it doesn't know what's inside this collection, then it runs a different set of code with different behavior.

This isn't a problem very often - especially if you're relying on Doctrine metadata - but whenever you have a collection field, you should think:

Does API Platform know what this is a collection of?

For getPublishedCheeseListings(), we already know the solution. Add @return Collection<CheeseListing>:

... lines 1 - 41
class User implements UserInterface
{
... lines 44 - 209
/**
... lines 211 - 212
* @return Collection<CheeseListing>
*/
public function getPublishedCheeseListings(): Collection
{
... lines 217 - 219
}
... lines 221 - 287
}

Try it! Refresh the endpoint and... we get an array of IRI strings in both cases.

Now, you can actually control this behavior directly... with an option that - honestly - makes my head spin a little bit. Instead of allowing API Platform to figure out if a property should be an embedded object or an IRI string, you can force it with @ApiProperty({readableLink=true}):

... lines 1 - 5
use ApiPlatform\Core\Annotation\ApiProperty;
... lines 7 - 42
class User implements UserInterface
{
... lines 45 - 210
/**
* @ApiProperty(readableLink=true)
... lines 213 - 215
*/
public function getPublishedCheeseListings(): Collection
{
... lines 219 - 221
}
... lines 223 - 289
}

Refresh now. Yep! This forces it to be an embedded object. readableLink is an internal option that's set on every API field, and it's normally determined automatically. API Platform sets it by looking to see if there are intersecting normalization groups between User and CheeseListing. Basically it says:

Hey! I can see that this property will hold an array of CheeseListing objects. Let's see if any of the CheeseListing properties are in the user:read group. If there are any, set readableLink to false to force it to be embedded.

By using the @ApiProperty annotation, we're overriding this and taking control ourselves.

Now, readableLink is super weird... at least for me - I can't quite wrap my mind around it. The name almost seems backwards: readableLink=true says that you want to embed and readableLink=false says to use an IRI link... though I've seen some odd behavior in some cases. If you have any questions, let us know down in the comments.

Ok, let's undo everything: take off readableLink, but leave the @return because that's actually helpful. Put back the @SerializedName():

... lines 1 - 42
class User implements UserInterface
{
... lines 45 - 210
/**
* @Groups({"user:read"})
* @SerializedName("cheeseListings")
* @return Collection<CheeseListing>
*/
public function getPublishedCheeseListings(): Collection
{
... lines 218 - 220
}
... lines 222 - 288
}

And, on the cheeseListings property, remove user:read:

... lines 1 - 42
class User implements UserInterface
{
... lines 45 - 78
/**
... line 80
* @Groups({"user:write"})
... line 82
*/
private $cheeseListings;
... lines 85 - 288
}

Back in CheeseListing, I'll undo to re-add the user:read groups:

... lines 1 - 58
class CheeseListing
{
... lines 61 - 67
/**
... line 69
* @Groups({"cheese:read", "cheese:write", "user:read", "user:write"})
... lines 71 - 76
*/
private $title;
... lines 79 - 86
/**
... lines 88 - 90
* @Groups({"cheese:read", "cheese:write", "user:read", "user:write"})
... line 92
*/
private $price;
... lines 95 - 217
}

Go over and refresh to make sure things are back to normal.

Next, let's get back to our custom DailyStats API Resource. We've implemented the collection operation, now let's add the get "item" operation so that we can fetch stats for a single day.

Leave a comment!

20
Login or Register to join the conversation
Default user avatar
Default user avatar Ivanis Kouamé | posted 6 months ago

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.

2 Reply

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!

1 Reply

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!

Reply
Auro Avatar

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


class WorkOrderTaskOutput
{
/**
* @Groups({"work-order-task:read"})
*/
public WorkOrder $workOrder;

/**
* @Groups({"work-order-task:read"})
*/
public ?string $description;

/**
* @Groups({"work-order-task:read"})
*/
public Employee $worker;
}

final class EmployeeOutput
{
/**
* @Groups({"employee:read", "work-order:read"})
*/
public string $firstName;

/**
* @Groups({"employee:read", "work-order:read"})
*/
public string $lastName;
}


{
"@context": "/contexts/WorkOrderTask",
"@id": "/work-orders/3fb33d5a-9d0f-45d7-8444-0e460113c8b0/tasks",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/work-order-tasks/d4b769a4-37ec-4910-8494-aaa5e5bbfe51",
"workOrder": {
"@id": "/work-orders/3fb33d5a-9d0f-45d7-8444-0e460113c8b0"
},
"description": null,
"worker": {
"@id": "/employees/28e69861-d5f9-40e6-b3a2-d6007180b536"
},
"realTime": "20"
}
],
"hydra:totalItems": 1
}
1 Reply

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-plat...

But we work around this by adding @ApiProperty(readableLink=false).

Just to make sure, where are you putting the readableLink=false? Above the workOrder and worker properties, 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!

Reply
Auro Avatar

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

Reply

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!

Reply
Auro Avatar
Auro Avatar Auro | weaverryan | posted 2 years ago | edited

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 ?

Reply

Hey Auro!

> 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.

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 :).

> Unfortunately I haven't fine anything useful in the provided context, and either a way to pass some extra information in the context,

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!

Reply
Auro Avatar
Auro Avatar Auro | weaverryan | posted 2 years ago | edited

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:


final class OutputNormalizer implements ContextAwareNormalizerInterface
{
private NormalizerInterface $normalizer;

public function __construct(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}

/**
* @param mixed $object
*
* @return array|\ArrayObject|bool|float|int|mixed|string|null
*/
public function normalize($object, string $format = null, array $context = [])
{
$data = $this->normalizer->normalize($object, $format, $context);

if (\is_array($data)) {
$keys = array_filter(array_keys($data), fn ($key) => !str_starts_with($key, '@'));

if (empty($keys) && isset($data['@id']) && $data['@id']) {
return $data['@id'];
}
}

return $data;
}

public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return $data instanceof OutputInterface;
}

public function hasCacheableSupportsMethod(): bool
{
return false;
}
}

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

1 Reply
Lucien D. Avatar
Lucien D. Avatar Lucien D. | Auro | posted 5 months ago | edited

Hey Auro

Thanks for your code.

I'm having hard time implementing your solution :

When I serialize the parent attribute (let's call it category which refered to the CategoryOutput) of the child DTO (productOutput), I get this behaviors :
* if the CategoryOutput->name property is tagged with "product:read" serialization group, the $data returned by the default serialization process inside the SocietyNormalizer::transform method is and object containing "@id" and "name" keys. I believe it's expected .
* if netiher property is tagged in CategoryOutput is tagged with the serialization group, the $data in 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

Reply
Lucien D. Avatar
Lucien D. Avatar Lucien D. | Lucien D. | posted 5 months ago | edited

EDIT :
After some digging, I realize I "exit" the ApiPlatform\Core\JsonLd\Serializer\ObjectNormalizer::normalize method on line 86 on :

$data = $this->decorated->normalize($object, $format, $context);
if (!\is_array($data) || !$data) {
return $data;
}
[...]
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)

Reply

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/sc...... 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 ;)

Reply
Lucien D. Avatar
Lucien D. Avatar Lucien D. | weaverryan | posted 5 months ago | edited

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 Category attribute of the Product object. 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 CategoryOutput to simplify and apply the "normalizer aware" strategy.

Some ideas :
* There are several peripherical class around the Category class (the proxy, the output DTO) and maybe I try to apply my custom normalizer on the wrong one. The supportNormalization method return $data instanceof CategoryOutput && !isset($context[self::ALREADY_CALLED]); and the ProductOutput class has a Category attribute.

* The wrong normalizer is called : it's the AbstractObjectNormalizer from the namespace Symfony\Component\Serializer\Normalizer wich seems responsable to normalize the CategoryOutput and I don't figure out the when and how the '@id' are calculated. The serialization chain seems to be :
1. The ApiPlatform\Core\Serializer\AbstractItemNormalizer::normalize method is hit on the Category object
2. It call the main normalization process on the transformed object (aka CategoryOutput)
2. The CategoryNormalizer is hit on this call
3. The CategoryNormalizer call the main normalization process on the CategoryOutput object
4. This time, the ApiPlatform\Core\JsonLd\Serializer\ObjectNormalizer is hit
5. It call his decorated serializer after set $context['api_empty_resource_as_iri'] = true. There is a big comment in the Api Platform code wich stipulate Converts the normalized data array of a resource into an IRI, if the normalized data array is empty. It's intended for the ApiPlatform\Core\Serializer\AbstractItemNormalizer::normalize() method : line 163
6. The Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer) is hit and return an empty array.
7. (In my dream where the $data array is not empty) : I apply the custom logic inside the CategoryNormalizer.

I try to create a UserOutput starting from the finish code of this course and face the same problem...

Don't have any more walls to hit ...

Thanks

Reply

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 call ApiPlatform\Serializer\AbstractItemNormalizer - https://github.com/api-plat... - because this is where that api_empty_resource_as_iri is read and used to return the IRI. But if the underlying object is CategoryOutput, 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!

Reply

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!

Reply
Auro Avatar
Auro Avatar Auro | weaverryan | posted 2 years ago | edited

Hey weaverryan,

The DataTransformerInterface has the following method:


/**
* Checks whether the transformation is supported for a given data and context.
*
* @param object|array $data object on normalize / array on denormalize
*/
public function supportsTransformation($data, string $to, array $context = []): bool;

I was wondering if I can add some custom information in the $context variable, maybe through the @ApiProperty
annotation. Something like embedded=false that I can retrieve in the context later. I saw that it has openapiContext & jsonldContext 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

Reply

Hey Auro!

> I was wondering if I can add some custom information in the $context variable, maybe through the @ApiProperty
annotation.

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 the category property on Product.

B) Then, when the data transformer for Category is called (from a situation where Product is actually being serialized, and now due to the category property, Category is being serialized) you would like to read that embedded false from the $context somehow 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/sc...

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 a non_embedded_properties key 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:


$context['class_stack'] = $context['class_stack'] ?? [];
$context['class_stack'][] = get_class($object);

// I'm not showing it, but you would need to do something like the
// $context[self::ALREADY_CALLED] = true; trick, but since we want
// this called for multiple objects, probably this will need to be an array
// specifically keeping track of which *objects* this normalizer has been
// called for, using spl_object_hash() as a key

// call the main normalizer system
$data = $this->normalizer->normalize($object, $format, $context);

// pop the last item (this class) off of the stack
array_pop($context['class_stack']);

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!

Reply
Default user avatar

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...

Reply

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!

Reply
Cat in space

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

This tutorial also works great with API Platform 2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.0 || ^8.0",
        "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.18.7
        "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
    }
}