Relating Resources
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn our app, each DragonTreasure should be owned by a single dragon... or User in our system. To set this up, forget about the API for a moment and let's just model this in the database.
Adding the ManyToOne Relation
Spin over to your terminal and run:
php bin/console make:entity
Let's modify the DragonTreasure entity to add an owner property... and then this will be a ManyToOne relation. If you're not sure which relation you need, you can always type relation and get a nice little wizard.
This will be a relation to User... and then it asks if the new owner property is allowed to be null in the database. Every DragonTreasure must have an owner... so say "no". Next: do we we want to map the other side of the relationship? So basically, do we want the ability to say, $user->getDragonTreasures() in our code? I'm going to say yes to this. And you might answer "yes" for two reasons. Either because being able to say $user->getDragonTreasures() would be useful in your code or, as we'll see a bit later, because you want to be able to fetch a User in your API and instantly see what treasures it has.
Anyways, the property - dragonTreasures inside of User is fine.... and finally, for orphanRemoval, say no. We'll also talk about that later.
And... done! Hit enter to exit.
So this had nothing to do with API Platform. Our DragonTreasure entity now has a new owner property with getOwner() and setOwner() methods.
| // ... lines 1 - 51 | |
| class DragonTreasure | |
| { | |
| // ... lines 54 - 93 | |
| #[ORM\ManyToOne(inversedBy: 'dragonTreasures')] | |
| #[ORM\JoinColumn(nullable: false)] | |
| private ?User $owner = null; | |
| // ... lines 97 - 197 | |
| public function getOwner(): ?User | |
| { | |
| return $this->owner; | |
| } | |
| public function setOwner(?User $owner): self | |
| { | |
| $this->owner = $owner; | |
| return $this; | |
| } | |
| } |
And over in User we have a new dragonTreasures property, which is a OneToMany back to DragonTreasure. At the bottom, it generated getDragonTreasures(), addDragonTreasure(), and removeDragonTreasure(). Very standard stuff.
| // ... lines 1 - 6 | |
| use Doctrine\Common\Collections\ArrayCollection; | |
| use Doctrine\Common\Collections\Collection; | |
| // ... lines 9 - 22 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 25 - 50 | |
| #[ORM\OneToMany(mappedBy: 'owner', targetEntity: DragonTreasure::class)] | |
| private Collection $dragonTreasures; | |
| public function __construct() | |
| { | |
| $this->dragonTreasures = new ArrayCollection(); | |
| } | |
| // ... lines 58 - 140 | |
| /** | |
| * @return Collection<int, DragonTreasure> | |
| */ | |
| public function getDragonTreasures(): Collection | |
| { | |
| return $this->dragonTreasures; | |
| } | |
| public function addDragonTreasure(DragonTreasure $treasure): self | |
| { | |
| if (!$this->dragonTreasures->contains($treasure)) { | |
| $this->dragonTreasures->add($treasure); | |
| $treasure->setOwner($this); | |
| } | |
| 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) { | |
| $treasure->setOwner(null); | |
| } | |
| } | |
| return $this; | |
| } | |
| } |
Let's create a migration for this:
symfony console make:migration
We'll do our standard double-check to make sure the migration isn't trying to mine bitcoin. Yep, all boring SQL queries here.
| // ... lines 1 - 12 | |
| final class Version20230104200643 extends AbstractMigration | |
| { | |
| // ... lines 15 - 19 | |
| public function up(Schema $schema): void | |
| { | |
| // this up() migration is auto-generated, please modify it to your needs | |
| $this->addSql('ALTER TABLE dragon_treasure ADD owner_id INT NOT NULL'); | |
| $this->addSql('ALTER TABLE dragon_treasure ADD CONSTRAINT FK_9E31BF5F7E3C61F9 FOREIGN KEY (owner_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | |
| $this->addSql('CREATE INDEX IDX_9E31BF5F7E3C61F9 ON dragon_treasure (owner_id)'); | |
| } | |
| // ... lines 27 - 35 | |
| } |
Run it with:
symfony console doctrine:migrations:migrate
Resetting the Database
And it explodes in our face. Rude! But... it shouldn't be too surprising. We already have about 40 DragonTreasure records in our database. So when the migration tries to add the owner_id column to the table - which does not allow null - our database is stumped: it has no idea what value to put for those existing treasures.
If our app were already on production, we'd have to do a bit more work to fix this. We talk about that in our Doctrine tutorial. But since this isn't on production, we can cheat and just to turn the database off and on again. To do that run:
symfony console doctrine:database:drop --force
Then:
symfony console doctrine:database:create
And the migration, which should work now that our database is empty.
symfony console doctrine:migrations:migrate
Setting up the Fixtures
Finally, re-add some data with:
symfony console doctrine:fixtures:load
And oh, this fails for the same reason! It's trying to create Dragon Treasures without an owner. To fix that, there are two options. In DragonTreasureFactory, add a new owner field to getDefaults() set to UserFactory::new().
| // ... lines 1 - 29 | |
| final class DragonTreasureFactory extends ModelFactory | |
| { | |
| // ... lines 32 - 46 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // ... lines 50 - 55 | |
| 'owner' => UserFactory::new(), | |
| ]; | |
| } | |
| // ... lines 59 - 73 | |
| } |
I'm not going to go into the specifics of Foundry - and Foundry has great docs on how to work with relationships - but this will create a new User each time it creates a new DragonTreasure... and then will relate them. So that's nice to have as a default.
But in AppFixtures, let's override that to do something cooler. Move the DragonTreasureFactory call after UserFactory... then pass a second argument, which is a way to override the defaults. By passing a callback, each time a DragonTreasure is created - so 40 times - it will call this method and we can return unique data to use for overriding the defaults for that treasure. Return owner set to UserFactory::random():
| // ... lines 1 - 9 | |
| class AppFixtures extends Fixture | |
| { | |
| public function load(ObjectManager $manager): void | |
| { | |
| UserFactory::createMany(10); | |
| DragonTreasureFactory::createMany(40, function () { | |
| return [ | |
| 'owner' => UserFactory::random(), | |
| ]; | |
| }); | |
| } | |
| } |
That'll find a random User object and set it as the owner. So we'll have 40 DragonTreasures each randomly hoarded by one of these 10 Users.
Let's try it! Run:
symfony console doctrine:fixtures:load
This time... success!
Exposing the "owner" in the API
Ok, so now DragonTreasure has a new owner relation property... and User has a new dragonTreasures relation property.
Will... that new owner property show up in the API? Try the GET collection endpoint for treasure. And... the new field does not show up! That makes sense! The owner property is not inside the normalization group.
So if we want to expose the owner property in the API, just like any other field, we need to add groups to it. Copy the groups from coolFactor... and paste them here.
| // ... lines 1 - 51 | |
| class DragonTreasure | |
| { | |
| // ... lines 54 - 95 | |
| (['treasure:read', 'treasure:write']) | |
| private ?User $owner = null; | |
| // ... lines 98 - 209 | |
| } |
This makes the property readable and writable. And yes, later, we'll learn how to set the owner property automatically so that the API user doesn't need to send that manually. But for now, having the API client send the owner field will work great.
Anyways, what does this new owner property look like? Hit "Execute" and... woh! The owner property is set to a URL! Well, really, the IRI of the User.
I love this. When I first started working with API Platform, I thought relationship properties might just use the object's id. Like owner: 1. But this is way more useful... because it tells our API client exactly how they could get more information about this user: just follow the URL!
Writing a Relation Property
So, by default, a relation is returned as a URL. But what does it look like to set a relation field? Refresh the page, open the POST endpoint, try it, and I'll paste in all of the fields except for owner. What do we use for owner? I don't know! Let's try setting it to an id, like 1.
Moment of truth. Hit execute. Let's see... a 400 status code! And check out the error:
Expected IRI or nested document for attribute
owner, integer given.
So I passed the ID of the owner and... it doesn't like that. What should we put here? Well, the IRI of course! Let's find out more about that next.
8 Comments
Under Setting up the Fixtures the sentence
should be:
Hey Alex,
You're right, thanks! Well, we still say it like
User::factory()... but that's a misspell and we clearly useUserFactory::random()on the video, so I fixed it https://github.com/SymfonyCasts/api-platform3/commit/71f42603109698c1ebad04bce8fe08636013e780Cheers!
Hi mate, I have really weird behavior when I tested the relationship with ApiTestCase. From the swagger I'm able to debug my processor state, but when I run the test, I have the following exceptio: ""Failed to denormalize attribute "protocol" value for class "App\Entity\Tramit": Expected argument of type "?App\Entity\Protocol", "Symfony\Component\HttpFoundation\Response" given at property path "protocol"."
The swagger POST request which is running fine
https://ibb.co/WvksJFk
The test which is getting error
https://ibb.co/TrcVVmv
I noticed that when I execute the request from the swagger, the content looks like this:
https://ibb.co/sPSqdtm
But when I executed the test, the same content looks a little different:
https://ibb.co/VB107mm
Is a little weird because I have another tests running well, the only different thing is that on this entities (Tramit and Protocol) I have inverseBy field in Protocol:
https://ibb.co/ZxtXk8w
What I'm trying to achieve is the capability to run a processor in the POST action, this capability is working fine from the swagger but no in the unit test, any help will be ver helpful.
Thanks in advance
Hey @Zahit,
First, have you tried to debug the
$irivariable? does it look like expected?My second thought is about the test data, it maybe not the same as your local copy, I mean probably some relations are not set, that's why it's failing to denormalize objects
Cheers!
Hi!
Something really strange happened to me.
The migration was failing, so I looked at the error and went to the migration files to find the statements causing them.
To my surprise, in the first document, there were references to a table from a previous tutorial interspersed with these.
Any idea how this could have happened?
Maybe something in the Symfony binary cache?
Thanks!
`
`
Yo @Oleguer-C!
Hmm. I don't see those extra
vinyl_mixlines in the migration files we have in the download code for the project, but I CAN think of a general workflow that could add these... it just depends on what actually happened :). A flow might be:A) You previously work on the Doctrine tutorial (with
vinyl_mix)B) You boot up this project, but point it to the same database.
C) Run
make:migrationDoctrine will see the
vinyl_mixstuff and think "that shouldn't be there" and try to drop it. Does this sound feasible? If I'm off the mark, it's still probably some silly detail - unless we have something wrong in our code and I'm not seeing it (definitely possible).Cheers!
Thanks for the reply.
That's what i thought, but in principle shouldn't the dbs of each project be isolated?
Maybe i hadn't downloaded the last container and since the port was exposed i connected to the old container the first time. Who knows.
Thanks
If you're using the Docker integration, then yes, the dbs should, in theory, be in their own container. However, the names of the containers, by default, come from your directory name (not the full directory name - just the last, top-level directory name). So if you put both projects into a directory called
training, for example, then they would share container names and thus share containers :). I have a lot of projects locally, so I hit this sometimes!"Houston: no signs of life"
Start the conversation!