Login to bookmark this video
Buy Access to Course
34.

Writing to a Collection Relation

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We are so close to completely re-implementing our API using these custom classes. So excited!

Let's run every test to see where we stand.

symfony php bin/phpunit

And... everything passes except one. This trouble-maker test is UserResourceTest::testTreasuresCannotBeStolen. Let's go check it out!

Open tests/Functional/UserResourceTest.php and search for testTreasuresCannotBeStolen(). Here it is.

91 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCannotBeStolen(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $otherUser]);
$this->browser()
->actingAs($user)
->patch('/api/users/' . $user->getId(), [
'json' => [
'username' => 'changed',
'dragonTreasures' => [
'/api/treasures/' . $dragonTreasure->getId(),
],
],
'headers' => ['Content-Type' => 'application/merge-patch+json']
])
->assertStatus(422);
}
// ... lines 76 - 89
}

Let's read the story. We update a user and attempt to change its dragonTreasures property to contain a treasure owned by someone else. The test looks for a 422 status code - because we want to prevent stealing treasures - but the test fails with a 200.

But apart from the whole stealing thing, this is the first test that we've seen that writes to a collection relation field. And that is an interesting topic all on its own.

Avoid Writable Collection Fields?

First, if you can, I'd recommend against allowing collection relationship fields like this to be writable. I mean, you absolutely can... but it adds complexity. For example, like this test shows, we need to worry about how setting the dragonTreasures property changes the owner on that treasure. And there's already a different way to do this: make a patch() request to this treasure and... change the owner. Simple!

But, if you still want to allow your collection relation to be writable in your DTO system, fine, here's how to do it. I'm kidding - it's not too bad.

Testing the Collection Write

Start by duplicating this test. Rename it to testTreasuresCanBeRemoved. I totally typo'ed that - mine says cannot, which is the opposite of what I want to test - so make sure you get that right in your code.

111 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
// ... lines 59 - 74
}
// ... lines 76 - 109
}

Now we can dress this up a bit. Make the first $dragonTreasure owned by $user. Then create a second $dragonTreasure also owned by $user, but we won't need a variable for it... you'll see. Finally, add a third $dragonTreasure called $dragonTreasure3 that's owned by $otherUser.

119 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $user]);
DragonTreasureFactory::createOne(['owner' => $user]);
$dragonTreasure3 = DragonTreasureFactory::createOne(['owner' => $otherUser]);
// ... lines 64 - 82
}
// ... lines 84 - 117
}

So we have three dragonTreasures, two owned by $user, and one by $otherUser. Down here, we patch to modify $user. Remove username - we don't care about that - then send two dragonTreasures: the first and the third: /api/treasures/ $dragonTreasure3->getId().

119 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $user]);
DragonTreasureFactory::createOne(['owner' => $user]);
$dragonTreasure3 = DragonTreasureFactory::createOne(['owner' => $otherUser]);
$this->browser()
->actingAs($user)
->patch('/api/users/' . $user->getId(), [
'json' => [
'dragonTreasures' => [
'/api/treasures/' . $dragonTreasure->getId(),
'/api/treasures/' . $dragonTreasure3->getId(),
],
],
'headers' => ['Content-Type' => 'application/merge-patch+json']
])
// ... lines 76 - 81
;
}
// ... lines 84 - 117
}

We're going to test for two things. First, that the second treasure is removed from this user. Think about it: $user started with these two treasures... and the fact that this second treasure's IRI is not sent means that we want it to be removed from $user.

Second, I added $dragonTreasure3 temporarily to prove that treasures can be stolen. This is currently owned by $otherUser, but we pass it to dragonTreasures... and we're going to verify that the owner of $dragonTreasure3 changes from $otherUser to $user. That's not the end behavior we want, but it'll help us get all the relation writing working. Then we'll worry about preventing that.

Down here, ->assertStatus(200) then extend the test by saying ->get('/api/users/' . $user->getId()) and ->dump().

119 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $user]);
DragonTreasureFactory::createOne(['owner' => $user]);
$dragonTreasure3 = DragonTreasureFactory::createOne(['owner' => $otherUser]);
$this->browser()
->actingAs($user)
->patch('/api/users/' . $user->getId(), [
'json' => [
'dragonTreasures' => [
'/api/treasures/' . $dragonTreasure->getId(),
'/api/treasures/' . $dragonTreasure3->getId(),
],
],
'headers' => ['Content-Type' => 'application/merge-patch+json']
])
->assertStatus(200)
->get('/api/users/' . $user->getId())
->dump()
// ... lines 79 - 81
;
}
// ... lines 84 - 117
}

I want to see what the user looks like after the update. Finally, assert that the length of the dragonTreasures field - I need quotes on that - is 2, for treasures 1 and 3. Then assert that dragonTreasures[0] is equal to '/api/treasures/'., followed by $dragonTreasure->getId(). Copy that, paste, and assert that the 1 key is $dragonTreasure3.

119 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $user]);
DragonTreasureFactory::createOne(['owner' => $user]);
$dragonTreasure3 = DragonTreasureFactory::createOne(['owner' => $otherUser]);
$this->browser()
->actingAs($user)
->patch('/api/users/' . $user->getId(), [
'json' => [
'dragonTreasures' => [
'/api/treasures/' . $dragonTreasure->getId(),
'/api/treasures/' . $dragonTreasure3->getId(),
],
],
'headers' => ['Content-Type' => 'application/merge-patch+json']
])
->assertStatus(200)
->get('/api/users/' . $user->getId())
->dump()
->assertJsonMatches('length("dragonTreasures")', 2)
->assertJsonMatches('dragonTreasures[0]', '/api/treasures/' . $dragonTreasure->getId())
->assertJsonMatches('dragonTreasures[1]', '/api/treasures/' . $dragonTreasure3->getId())
;
}
// ... lines 84 - 117
}

Lovely! That test took some work, but it'll be super useful. Let's... just run it and see what happens! Copy the method name and, over at your terminal, run:

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

And by "cannot be removed", I, of course, mean that it can be removed. That was some good 'ol copy/paste madness right there. There we go. And... it fails, on line 81. This means that the request was successful... but the dragonTreasures are still the original two: /api/treasures/2 instead of /api/treasures/3. No changes were made to the treasures.

Why? Let's find out next and leverage the property accessor component to make sure the changes save correctly.