Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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

Use the docs to check out the User with id=2. When we read a resource, we can decide to expose any property - and a property that holds a collection, like cheeseListings, is no different. We exposed that property by adding @Groups("user:read") above it. And because this holds a collection of related objects, we can also decide whether the cheeseListings property should be exposed as an array of IRI strings or as an array of embedded objects, by adding this same group to at least one property inside CheeseListing itself.

Great. New challenge! We can read the cheeseListings property on User... but could we also modify this property?

For example, well, it's a bit of a strange example, but let's pretend that an admin wants to be able to edit a User and make them the owner of some existing CheeseListing objects in the system. You can already do this by editing a CheeseListing and changing its owner. But could we also do it by editing a User and passing a cheeseListings property?

Actually, let's get even a bit crazier! I want to be able to create a new User and specify one or more cheese listings that this User should own... all in one request.

Making cheeseListings Modifiable

Right now, the cheeseListings property is not modifiable. The reason is simple: that property only has the read group. Cool! I'll make that group an array and add user:write.

... lines 1 - 22
class User implements UserInterface
{
... lines 25 - 58
/**
... line 60
* @Groups({"user:read", "user:write"})
*/
private $cheeseListings;
... lines 64 - 184
}

Now, go back, refresh the docs and look at the POST operation: we do have a cheeseListings property. Let's do this! Start with the boring user info: email, password doesn't matter and username. For cheeseListings, this needs to be an array... because this property holds an array. Inside, add just one item - an IRI - /api/cheeses/1.

In a perfect world, this will create a new User and then go fetch the CheeseListing with id 1 and change it to be owned by this user. Deep breath. Execute!

It worked? I mean, it worked! A 201 status code: it created the new User and that User now owns this CheeseListing! Wait a second... how did that work?

Adder and Remover Methods for Collections

Check it out: we understand how email, password and username are handled: when we POST, the serializer will call setEmail(). In this case, we're sending a cheeseListings field... but if we go look for setCheeseListings(), it doesn't exist!

Instead, search for addCheeseListing(). Ahhh. The make:entity command is smart: when it generates a collection relationship like this, instead of generating a setCheeseListings() method, it generates addCheeseListing() and removeCheeseListing(). And the serializer is smart enough to use those! It sees the one CheeseListing IRI we're sending, queries the database for that object, calls addCheeseListing() and passes it as an argument.

The whole reason make:entity generates the adder - instead of just setCheeseListings() - is that it lets us do things when a cheese listing is added or removed. And that is key! Check it out: inside the generated code, it calls $cheeseListing->setOwner($this). That is the reason why the owner changed to the new user, for this CheeseListing with id=1. Then... everything just saves!

Next: when we're creating or editing a user, instead of reassigning an existing CheeseListing to a new owner, let's make it possible to create totally new cheese listings. Yep, we're getting crazy! But this will let us learn even more about how the serializer thinks and works.

Leave a comment!

4
Login or Register to join the conversation

Hi, is there a way to disable removing in that case, without throwing any exception/validation ? kind of patching my array collection with new data, but i dont want old data be deleted, if I don't provide them inside my PUT payload ?

Reply

Hi ahmedbhs!

Apologies for the slow reply - vacation last week, and your question was left for me personally! :)

Yes, I think you CAN do this, but it's entirely up to YOUR code to do it. Behind the scenes, iirc, ApiPlatform (via the serializer I believe) looks at the current collection and the submitted collection and finds the "difference". It calls addCheeseListing() for any new items and removeCheeseListing() for any removed items. So, in theory, you could make your removeCheeseListing() do nothing ;) That feels... a bit odd and potentially dangerous to me, but I think that IS the path... I can't think of another way.

Cheers!

Reply
Sung L. Avatar
Sung L. Avatar Sung L. | posted 4 years ago | edited

Hi,

I have a question about adding items to a collection property in many-to-many relations with extra property in bridge table.
Example is sports players and teams. A player can be in multiple teams and teams can have multiple players. Also team can have player(s) as captains.

Here is the table schema:
`
Players
--
id
name
teams

Teams
--
id
name
players

PlayersTeams
--
player
team
is_captain
`

And here is the shortened Entities:
`
// App/Entity/Players.php

/**

  • @var \Teams
  • @ORM\OneToMany(targetEntity="PlayersTeams", mappedBy="player", fetch="EAGER")
  • @Groups({"players:read", "players:write"})
  • @Assert\Valid()
    */
    private $teams

/**

  • @return Collection|Teams[]
    */
    public function getTeams(): Collection {
    return $this->teams;
    }

public function addTeam(Teams $team): self {

if (!$this->teams->contains($team)) {
    $this->teams[] = $team;
    $playerTeam = new PlayersTeams();
    $playerTeam->setPlayer($this);
}

return $this;

}

public function removeTeam(Teams $team): self {

if ($this->teams->contains($team)) {
    $this->teams->removeElement($team);
    if ($team->getPlayer() === $this) {
        $team->setPlayer(null);
    }
}

return $this;

}

// App/Entity/PlayersTeams.php

/**

  • @ORM\Id()
  • @ORM\GeneratedValue()
  • @ORM\Column(type="integer")
    */
    private $id;

/**

  • @var \Teams
  • @ORM\ManyToOne(targetEntity="Teams", inversedBy="players")
  • @ORM\JoinColumn(name="team_id", referencedColumnName="id", nullable=false)
  • @Groups({"players:read", "players:write", "teams:read", "teams:write"})
    */
    private $team;

/**

  • @var \Players
  • @ORM\ManyToOne(targetEntity="Players", inversedBy="teams")
  • @ORM\JoinColumn(name="player_id", referencedColumnName="id", nullable=false)
  • @Groups({"players:read", "players:write", "teams:read", "teams:write"})
    */
    private $player;

/**

  • @var bool
  • @ORM\Column(type="boolean")
  • @Groups({"players:read", "players:write", "teams:read", "teams:write"})
    */
    private $isCaptain = false;

public function getId(): ?int {
return $this->id;
}

public function getTeam(): Teams {
return $this->team;
}

public function setTeam(Teams $team): self {
$this->team = $team;
return $this;
}

public function getPlayer(): Players {
return $this->player;
}

public function setPlayer(Players $player): self {
$this->player = $player;
return $this;
}

public function getIsCaptain(): bool {
return $this->isCaptain;
}

public function setIsCaptain(bool $isCaptain): self {
$this->isCaptain = $isCaptain;
return $this;
}
`

When I call /player/1 PUT API, I get the following response: "Expected value of type \"App\Entity\PlayersTeams\" for association field \"App\Entity\Players#$teams\", got \"App\Entity\Teams\" instead."

How can I add items to the embedded objects in this entity relations?

Thank you for your tips!

Reply

Hey Sung,

It sounds like you're trying to set a Teams entity instead of PlayersTeams entity to the Players::$teams property. It sounds like you have an invalid annotation mapping for this property, shouldn't it be "App\Entity\Teams" instead? Otherwise, you should set PlayersTeams entity instead of Teams.

Btw, I'd recommend you also to check your Doctrine mapping configuration with the command:

$ bin/console doctrine:schema:validate

Make sure you have a valid mapping in your application. If you do - most probably you have a logic mistake somewhere.

I hope this helps!

Cheers!

Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}
userVoice