UUID as a API Identifier
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 SubscribeWe have a $uuid
property on User
and it is being set:
// ... lines 1 - 43 | |
class User implements UserInterface | |
{ | |
// ... lines 46 - 52 | |
/** | |
* @ORM\Column(type="uuid", unique=true) | |
*/ | |
private $uuid; | |
// ... lines 57 - 296 | |
} |
But it's completely not part of our API yet.
Testing for the UUID
Before we change that, let's write a test that describes the behavior we expect. Open up tests/Functional/UserResourceTest.php
and find testCreateUser()
:
// ... lines 1 - 8 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
public function testCreateUser() | |
{ | |
$client = self::createClient(); | |
$client->request('POST', '/api/users', [ | |
'json' => [ | |
'email' => 'cheeseplease@example.com', | |
'username' => 'cheeseplease', | |
'password' => 'brie' | |
] | |
]); | |
$this->assertResponseStatusCodeSame(201); | |
$this->logIn($client, 'cheeseplease@example.com', 'brie'); | |
} | |
// ... lines 26 - 82 | |
} |
After creating a User
, our API serializes that User
and returns it in the response. Once we've changed to use the UUID, we'll expect the @id
property on that response to be /api/user/{uuid}
instead of the auto-increment ID.
Let's check for that! Start by querying for the User
object that was just created. We can do that with $user = UserFactory::repository()->findOneBy()
and pass it the email that we used up here. Below that, do a sanity check that the user does exist: $this->assertNotNull($user)
:
// ... lines 1 - 8 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
public function testCreateUser() | |
{ | |
// ... lines 13 - 21 | |
$this->assertResponseStatusCodeSame(201); | |
$user = UserFactory::repository()->findOneBy(['email' => 'cheeseplease@example.com']); | |
$this->assertNotNull($user); | |
// ... lines 26 - 27 | |
} | |
// ... lines 29 - 85 | |
} |
In order to check that the @id
is correct, we need to know what random UUID the user was just assigned. Now that we have the User
object from the database, we can say $this->assertJsonContains()
, pass an array and assert that @id
should be /api/users/
and then $user->getUuid()
:
// ... lines 1 - 8 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
public function testCreateUser() | |
{ | |
// ... lines 13 - 23 | |
$user = UserFactory::repository()->findOneBy(['email' => 'cheeseplease@example.com']); | |
$this->assertNotNull($user); | |
$this->assertJsonContains([ | |
'@id' => '/api/users/'.$user->getUuid()->toString() | |
]); | |
// ... lines 29 - 30 | |
} | |
// ... lines 32 - 88 | |
} |
Oh, except we don't have a getUuid()
method yet!
The UUID Object and UuidInterface
No problem - let's add it! Over in User
, down at the bottom, go to "Code"->"Generate" - or Command
+N
on a Mac - and generate the getter:
// ... lines 1 - 13 | |
use Ramsey\Uuid\UuidInterface; | |
// ... lines 15 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 298 | |
public function getUuid(): UuidInterface | |
{ | |
return $this->uuid; | |
} | |
} |
We don't need a setter.
Oh! Apparently this will return a UuidInterface
.... though I'm not sure why it used the long version here. I'll shorten that, re-type the end and auto-complete it so that PhpStorm adds the use
statement on top:
// ... lines 1 - 13 | |
use Ramsey\Uuid\UuidInterface; | |
// ... lines 15 - 304 |
The uuid
property will store in the database as a string in MySQL. But in PHP, this property holds a Uuid
object. That's not too important... just be aware of it. Over in the test, to get the string, we can say ->toString()
:
// ... lines 1 - 8 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
public function testCreateUser() | |
{ | |
// ... lines 13 - 25 | |
$this->assertJsonContains([ | |
'@id' => '/api/users/'.$user->getUuid()->toString() | |
]); | |
// ... lines 29 - 30 | |
} | |
// ... lines 32 - 88 | |
} |
Though, really, that's not needed because the Uuid
object has an __toString()
method.
Let's try this! Copy the method name, find your terminal and run:
symfony php bin/phpunit --filter=testCreateUser
And... excellent. We're looking for the UUID, but it still uses the id.
Using the UUID as the API Identifier
So how do we tell API Platform to use the uuid
property as the "identifier"? It's actually pretty simple! And we talked about it before.
Go to the top of User
. Every API Resource needs an identifier. And when you use Doctrine, API Platform assumes that you want the database id as the identifier. To tell it to use something different, add @ApiProperty()
with identifier=false
:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
/** | |
// ... lines 48 - 50 | |
* @ApiProperty(identifier=false) | |
*/ | |
private $id; | |
// ... lines 54 - 304 | |
} |
That says:
Hey! Please don't use this as the identifier.
Then, above uuid
, add @ApiProperty()
with identifier=true
:
// ... lines 1 - 44 | |
class User implements UserInterface | |
{ | |
// ... lines 47 - 54 | |
/** | |
// ... line 56 | |
* @ApiProperty(identifier=true) | |
*/ | |
private $uuid; | |
// ... lines 60 - 304 | |
} |
That's it! Try the test again:
symfony php bin/phpunit --filter=testCreateUser
And... got it! But if we run all of the tests:
symfony php bin/phpunit
Fixing all the Places we Relied in the Id
Ah... things are not so good. It turns out that we were relying on the id as the identifier in a lot of places. Let's see an example. In testCreateCheeseListing()
, we send the owner
field set to /api/users/1
. But... that is not the IRI of that user anymore! The IRI of every user just changed!
Let's fix some tests! Start inside UserResourceTest
and search for /api/users/
. Yep! To update a user, we won't use the id anymore, we'll use the uuid. In testGetUser()
, it's the same to fetch a user. Change one more spot at the bottom:
// ... lines 1 - 8 | |
class UserResourceTest extends CustomApiTestCase | |
{ | |
// ... lines 11 - 32 | |
public function testUpdateUser() | |
{ | |
// ... lines 35 - 38 | |
$client->request('PUT', '/api/users/'.$user->getUuid(), [ | |
// ... lines 40 - 43 | |
]); | |
// ... lines 45 - 51 | |
} | |
public function testGetUser() | |
{ | |
// ... lines 56 - 63 | |
$client->request('GET', '/api/users/'.$user->getUuid()); | |
// ... lines 65 - 82 | |
$client->request('GET', '/api/users/'.$user->getUuid()); | |
// ... lines 84 - 87 | |
} | |
} |
Over in CheeseListingResourceTest
, search for the same thing: /api/users/
. Then we'll change a few more spots. Like, when we're setting the owner
property, this needs to use the UUID. I'll keep searching and fix a few more spots:
// ... lines 1 - 10 | |
class CheeseListingResourceTest extends CustomApiTestCase | |
{ | |
public function testCreateCheeseListing() | |
{ | |
// ... lines 15 - 35 | |
$client->request('POST', '/api/cheeses', [ | |
'json' => $cheesyData + ['owner' => '/api/users/'.$otherUser->getUuid()], | |
]); | |
// ... lines 39 - 40 | |
$client->request('POST', '/api/cheeses', [ | |
'json' => $cheesyData + ['owner' => '/api/users/'.$authenticatedUser->getUuid()], | |
]); | |
// ... line 44 | |
} | |
public function testUpdateCheeseListing() | |
{ | |
// ... lines 49 - 57 | |
$client->request('PUT', '/api/cheeses/'.$cheeseListing->getId(), [ | |
// ... line 59 | |
'json' => ['title' => 'updated', 'owner' => '/api/users/'.$user2->getUuid()] | |
]); | |
// ... lines 62 - 68 | |
} | |
// ... lines 70 - 146 | |
public function testGetCheeseListingCollection() | |
{ | |
// ... lines 149 - 167 | |
$this->assertJsonContains(['hydra:member' => [ | |
0 => [ | |
// ... lines 170 - 173 | |
'owner' => '/api/users/' . $user->getUuid(), | |
// ... lines 175 - 176 | |
] | |
]]); | |
} | |
public function testGetCheeseListingItem() | |
{ | |
// ... lines 183 - 192 | |
$response = $client->request('GET', '/api/users/'.$otherUser->getUuid()); | |
// ... lines 194 - 195 | |
} | |
} |
Let's see if we found everything! Run the tests now:
symfony php bin/phpunit
And... green! We just switched to UUID's! Woo!
But... part of the point of changing to a UUID was that it would be nice to allow our API clients - like JavaScript - to set the UUID themselves. Let's make that possible next.
Hi All!
I'm creating an API , and want to use UUID's. Everything seems to work, except for PUT requests. I always get this error:
"hydra:description": "Cannot assign Symfony\\Component\\Uid\\UuidV4 to property App\\Entity\\Subscription::$id of type ?int",
The entity is pretty straigt forward:
I'm really losing my mind on this, and google or chatgpt are not my friend :(
Is there any genius that sees what I am missing?
Symfony version: 6.4.2
API Platform version: 3.1.14
Thanks a lot, all help is really appreciated.
EDIT: sorry, I seem to struggle with the code block, I can't manage to put it in one codeblock ...