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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

We 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.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.7
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.4
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.4.5", // v2.7.3
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.4
        "ramsey/uuid-doctrine": "^1.6", // 1.6.0
        "symfony/asset": "5.1.*", // v5.1.5
        "symfony/console": "5.1.*", // v5.1.5
        "symfony/debug-bundle": "5.1.*", // v5.1.5
        "symfony/dotenv": "5.1.*", // v5.1.5
        "symfony/expression-language": "5.1.*", // v5.1.5
        "symfony/flex": "^1.1", // v1.9.10
        "symfony/framework-bundle": "5.1.*", // v5.1.5
        "symfony/http-client": "5.1.*", // v5.1.5
        "symfony/monolog-bundle": "^3.4", // v3.5.0
        "symfony/security-bundle": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/validator": "5.1.*", // v5.1.5
        "symfony/webpack-encore-bundle": "^1.6", // v1.7.3
        "symfony/yaml": "5.1.*" // v5.1.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
        "symfony/browser-kit": "5.1.*", // v5.1.5
        "symfony/css-selector": "5.1.*", // v5.1.5
        "symfony/maker-bundle": "^1.11", // v1.21.1
        "symfony/phpunit-bridge": "5.1.*", // v5.1.5
        "symfony/stopwatch": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.5
        "zenstruck/foundry": "^1.1" // v1.1.2
    }
}