Setting the UUID on POST
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 SubscribeThe UUID is now the identifier inside of our User resource:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 54 | |
/** | |
// ... line 56 | |
* @ApiProperty(identifier=true) | |
*/ | |
private $uuid; | |
// ... lines 60 - 304 | |
} |
Awesome! But it still works exactly like the old ID. What I mean is, only the server can set the UUID. If we tried to send UUID as a JSON field when creating a user, it would be ignored.
How can I be so sure? Well, look at the User
class: $uuid
is not settable anywhere. It's not an argument to the constructor and there's no setUuid()
method:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 58 | |
private $uuid; | |
// ... lines 60 - 121 | |
public function __construct() | |
{ | |
// ... line 124 | |
$this->uuid = Uuid::uuid4(); | |
} | |
// ... lines 127 - 300 | |
public function getUuid(): UuidInterface | |
{ | |
return $this->uuid; | |
} | |
} |
Time to change that!
Setting the UUID in a Test
Let's describe the behavior we want in a test. In UserResourceTest
, go up to the top and copy testCreateUser()
. Paste that down here and call it testCreateUserWithUuid()
:
// ... lines 1 - 9 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
// ... lines 12 - 33 | |
public function testCreateUserWithUuid() | |
{ | |
// ... lines 36 - 50 | |
} | |
// ... lines 52 - 108 | |
} |
The key change we want to make is this: in the JSON, we're going to pass a uuid
field. For the value, go up and say $uuid = Uuid
- the one from Ramsey
- ::uuid4()
. Then below, send that as the uuid
:
// ... lines 1 - 9 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
// ... lines 12 - 33 | |
public function testCreateUserWithUuid() | |
{ | |
// ... lines 36 - 37 | |
$uuid = Uuid::uuid4(); | |
$client->request('POST', '/api/users', [ | |
'json' => [ | |
'uuid' => $uuid, | |
// ... lines 42 - 44 | |
] | |
]); | |
// ... lines 47 - 50 | |
} | |
// ... lines 52 - 108 | |
} |
I technically could call ->toString()
... but since the Uuid
object has an __toString()
method, we don't need to. Assert that the response is a 201 and... then we can remove the part that fetches the User
from the database. Because... we know that the @id
should be /api/users/
and then that $uuid
. I'll also remove the login part, only because we have that in the other test:
// ... lines 1 - 9 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
// ... lines 12 - 33 | |
public function testCreateUserWithUuid() | |
{ | |
// ... lines 36 - 37 | |
$uuid = Uuid::uuid4(); | |
$client->request('POST', '/api/users', [ | |
'json' => [ | |
'uuid' => $uuid, | |
// ... lines 42 - 44 | |
] | |
]); | |
$this->assertResponseStatusCodeSame(201); | |
$this->assertJsonContains([ | |
'@id' => '/api/users/'.$uuid | |
]); | |
} | |
// ... lines 52 - 108 | |
} |
So this is the plan: we send the uuid
and it uses that uuid
. Copy the name of this method and let's make sure it fails:
symfony php bin/phpunit --filter=testCreateUserWithUuid
It does. It completely ignores the UUID that we send and generates its own.
Making the uuid Field Settable
So how can we make the UUID field settable? Well, it's really no different than any other field: we need to put the property in the correct group and make sure it's settable either through the constructor or via a setter method.
Let's think: we only want this field to be settable on create: we don't want to allow anyone to modify it later. So we could add a setUuid()
method, but then we would need to be careful to configure and add the correct groups so that it can be set on create but not edit.
But... there's a simpler solution: avoid the setter and instead add $uuid
as an argument to the constructor! Then, by the rules of object-oriented coding, it will be settable on create but immutable after.
Let's do that: add a UuidInterface $uuid
argument and default it to null. Then $this->uuid = $uuid ?: Uuid::uuid4()
:
// ... lines 1 - 13 | |
use Ramsey\Uuid\UuidInterface; | |
// ... lines 15 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 122 | |
public function __construct(UuidInterface $uuid = null) | |
{ | |
// ... line 125 | |
$this->uuid = $uuid ?: Uuid::uuid4(); | |
} | |
// ... lines 128 - 305 | |
} |
So if a $uuid
argument is passed, we'll use that. If not, we generate a new one. Oh, and we also need to make sure the UUID is actually writeable in the API. Above the $uuid
property, add @Groups()
with user:write
:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 54 | |
/** | |
// ... lines 56 - 57 | |
* @Groups({"user:write"}) | |
*/ | |
private $uuid; | |
// ... lines 61 - 305 | |
} |
Ok, let's try the test again!
symfony php bin/phpunit --filter=testCreateUserWithUuid
This time... woh! It works. That's awesome. And the documentation for this instantly looks perfect. Refresh the API homepage, open up the POST operation for users, hit "Try it out" and... yep! It already shows a UUID example and it understands that it is available for us to set.
UUID String Transformed to an Object?
But wait a second. How did that work? Think about it, if you look at our test, we're sending a string in the JSON:
// ... lines 1 - 9 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
// ... lines 12 - 33 | |
public function testCreateUserWithUuid() | |
{ | |
// ... lines 36 - 37 | |
$uuid = Uuid::uuid4(); | |
$client->request('POST', '/api/users', [ | |
'json' => [ | |
'uuid' => $uuid, | |
// ... lines 42 - 44 | |
] | |
]); | |
// ... lines 47 - 50 | |
} | |
// ... lines 52 - 108 | |
} |
But ultimately, on our User
object, the constructor argument accepts a UuidInterface
object, not a string:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 122 | |
public function __construct(UuidInterface $uuid = null) | |
{ | |
// ... lines 125 - 126 | |
} | |
// ... lines 128 - 305 | |
} |
How did that string become an object?
Remember: API platform - well really, Symfony's serializer - is really good at reading your types. It notices that the type for $uuid
is UuidInterface
and uses that to try to find a denormalizer that understands this type. And fortunately, API Platform comes with a denormalizer that works with ramsey UUID's out of the box. Yep, that denormalizer takes the string and turns it into a UUID object so that it can then be passed to the constructor.
So... yay UUIDs! But, before we finish, there is one tiny quirk with UUID's. Let's see what it is next and learn how to work around it.
Hello!
I am finding that using
UuidInterface
as a constructor argument type is not "magically" resulting in passed strings being converted into UUIDs. When I attempt to use the endpoint (and pass a UUID) I end up with the following error message. This error message makes perfect sense, but is contrary to the behavior this tutorial describes. Instead, I need to use strings.Error message:
Argument #1 ($uuid) must be of type ?Ramsey\\Uuid\\UuidInterface, string given
I tried creating my own normalizer, but I could not get it to fire the way it's "supposed" to. Is there some undocumented setting or something that I'm missing? My understanding is this should work out of the box with no additional service declarations.
Example code: