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.

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

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

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
    }
}