If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeUse 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.
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?
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.
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!
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
/**
/**
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
/**
/**
/**
/**
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!
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!
// 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
}
}
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 ?