Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Removing Items from a Collection

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

Our brand-new user is the proud owner of two treasures with IDs 7 and 44. Let's update this user to see if we can make some changes to $dragonTreasures. Use the PUT endpoint, click "Try it out", and... let's see... the id we need is 14... so I'll enter that. I'll also remove every field except for dragonTreasures so we can focus.

We know that this currently has two dazzling treasures - /api/treasures/7 and /api/treasures/44. So if we send this request, in theory, that should do... nothing! And if we look down here... yeah: it made no changes at all.

Suppose we want to add a new DragonTreasure to this resource. To do that, we list the two that it already has, along with /api/treasures/8. I'm totally guessing that's a valid id. When we hit "Execute"... that works beautifully. The serializer system noticed that it already had these first two, so it didn't do anything with those. It just added the new one with id 8.

Removing an Item from a Collection

That's cool, but what I really want to talk about is removing a treasure. Let's say that our dragon left one of these treasures in their pants pocket and accidentally washed it in the laundry. I can't blame them. I lose my lip balm in there all the time. Since the treasure is soggy and useless now, we need to remove it from the list of treasures. No problem! We'll just mention the two our dragon still has and remove the other one. When we hit "Execute"... it explodes!

An exception occurred while executing a query: [...] Not null violation: 7. null value in column "owner_id"

What happened? Well, our app set the $owner property for the DragonTreasure we just removed to null... and is now trying to save it. But since we have it set to nullable: false, it's failing.

... lines 1 - 55
class DragonTreasure
{
... lines 58 - 97
#[ORM\ManyToOne(inversedBy: 'dragonTreasures')]
#[ORM\JoinColumn(nullable: false)]
... lines 100 - 101
private ?User $owner = null;
... lines 103 - 214
}

But... let's take a step back and look at the whole picture. First, the serializer noticed that treasures 7 and 8 are already owned by the User... so it did nothing with those. But then it noticed that the treasure with id 44 - which was owned by this User - is missing!

Because of that, over on our User class, the serializer called removeDragonTreasure(). What's really important is that it takes that DragonTreasure and set the owner to null to break the relationship. Depending on your app, that might be exactly what you want. Maybe you allow dragonTreasures to have no owner... like... they're still undiscovered and waiting for a dragon to find them. If that's the case, you'll just want to make sure that your relationship allows null... and everything will save just fine.

But in our case, if a DragonTreasure no longer has an owner, we want to delete it completely. We can do that in User... way up on the dragonTreasures property. After cascade, add one more option here: orphanRemoval: true.

... lines 1 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 25 - 50
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: DragonTreasure::class, cascade: ['persist'], orphanRemoval: true)]
... lines 52 - 53
private Collection $dragonTreasures;
... lines 55 - 171
}

This tells Doctrine that if any of these dragonTreasures become "orphaned" - meaning they no longer have any owner - they should be deleted.

Let's try it. When we hit "Execute" again... got it! It saves just fine.

Next: Let's circle back to filters and see how we can use them to search across related resources.

Leave a comment!

2
Login or Register to join the conversation

Hey there,

I'm a bit confused, my user put doesn't have a list of IRIs but a list of objects. I don't know if I missed something.

Looking at your DragonTreasure.php file, I feel like you should too.

Anyway, I removed the user:write group from my DragonTreasure's properties, but this is confusing.

Cheers

Reply

Hey Julien!

Sorry for the slow reply - we had a hiccup in our notification system!

I'm a bit confused, my user put doesn't have a list of IRIs but a list of objects. I don't know if I missed something.

Just to be totally clear, when I make a PUT request, for the dragonTreasures property, I am:

A) SENDING an array of IRI strings
B) RECEIVING an array of objects

Let's look at each side more deeply (probably some of this you already understand, but just to be safe):

A) Since we're writing to User, the user:write group is being used. DragonTreasure DOES have user:write above name and value. So, should we be sending an object instead of IRI strings? The answer is that, because those 2 fields have user:write, we have the OPTION to send an "object" to the dragonTreasures property, but we're not forced to. We showed that off in an earlier chapter where we even sent a mixture or objects and IRI strings: https://symfonycasts.com/screencast/api-platform/collections-create#sending-embedded-objects-and-iri-strings-at-the-same-time

B) We are using the PUT operation. But at this point, we're now talking about what data to RETURN. So that is "normalization". And so, the group that's used here is user:read. Once again, inside DragonTreasure, both name and value have this group. And so, the response contains an array of objects for dragonTreasures.

Let me know if that helps :)

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice