Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Adding Items to a Collection Property

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

Let's fetch a single user in our API: I know one exists with ID 2. And cool!

As we learned earlier, exposing a collection relation property is just like any other field: simply make sure that it's in the correct serialization group. And then you can go further with serialization groups to choose between making it return as an array of IRI strings or as an array of embedded objects, like we have now.

New question: could we also modify the dragonTreasures that a user owns from one of the user operations? The answer is, of course, yea! And we're going to do this in increasingly crazy ways.

Making the Collection Field Writable

Look at the POST endpoint. We don't see a dragonTreasures field right now because... the field simply isn't writable: it's not in the correct group. To remedy that, we know what to do: add user:write.

... lines 1 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
... lines 25 - 51
#[Groups(['user:read', 'user:write'])]
private Collection $dragonTreasures;
... lines 54 - 170

Easy peasy! When we refresh the docs, and check that endpoint... there we go: dragonTreasures. And it says that this field should be an array of strings: an array of IRI strings.

Let's try crafting a new user. Fill in the email and username. Then, let's assign the new user to a few existing treasures. Let's sneak up to the GET collection endpoint for treasures... and awesome. We have ids 2, 3 and 4.

Back down here, assign owner to an array with /api/treasures/2, /api/treasures/3 and /api/treasures/4.

Makes sense, right? If the API can return dragonTreasures as an array of IRI strings, why can't we send an array of IRI strings? When we hit Execute... indeed! It worked perfectly!

And since each treasure can have only one owner... it means that we kinda stole those treasures from someone else! Sorry!

The adder & remover Methods for Collections

But... wait a second, how did that work? We know that when we send fields like email, password, and username, because those are private properties, the serializer calls the setter methods. When we pass username, it calls setUsername().

... lines 1 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
... lines 25 - 134
public function setUsername(string $username): self
$this->username = $username;
return $this;
... lines 141 - 170

So when we pass dragonTreasures, it must call setDragonTreasures, right?

Well guess what? We don't have a setDragonTreasures() method! But we do have an addDragonTreasure() method and a removeDragonTreasure() method.

... lines 1 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
... lines 25 - 149
public function addDragonTreasure(DragonTreasure $treasure): self
if (!$this->dragonTreasures->contains($treasure)) {
return $this;
public function removeDragonTreasure(DragonTreasure $treasure): self
if ($this->dragonTreasures->removeElement($treasure)) {
// set the owning side to null (unless already changed)
if ($treasure->getOwner() === $this) {
return $this;

The serializer is really smart. It sees that the new User object has no dragonTreasures. So it recognizes that each of these three objects are new to this user and so it calls addDragonTreasure() once for each.

And the way that MakerBundle generated these methods is critical. It takes the new DragonTreasure and sets the owner to be this object. That's important because of how Doctrine handles relationships: setting the owner sets what's called the "owning" side of the relationship. Basically, without this, Doctrine wouldn't save this change to the database.

The takeaway is that, thanks to addDragonTreasure() and its magical powers, the owner of the DragonTreasure is changed from its old owner to the new User, and everything saves exactly like we want.

Next, let's get more complex by allowing treasures to be created when we're creating a new User. We're also going to allow treasures to be removed from a User... for the unlikely event that the dwarves take back the mountain. As if.

Leave a comment!

Login or Register to join the conversation
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