Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

User API Resource

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

I want to expose our new User entity as an API resource. And we know how to do that! Add... @ApiResource!

... lines 1 - 4
use ApiPlatform\Core\Annotation\ApiResource;
... lines 6 - 8
* @ApiResource()
... line 11
class User implements UserInterface
... lines 14 - 128

Just like that! Yes! Our API docs show one new resource with five new endpoints, or operations. And at the bottom, here's the new User model.

Hmm, but it's a bit strange: both the hashed password field and roles array are part of the API. Yea, we could create a new user right now and pass whatever roles we think that user should have! That might be ok for an admin user to be able to do, but not anyone. Let's take control of things.


Oh, one thing I want you to notice is that, so far, the primary key is always being used as the "id" in our API. This is something that's flexible in API Platform. In fact, instead of using an auto-increment id, one option is to use a UUID. We're not going to use them in this tutorial, but using a UUID as your identifier is something that's supported by Doctrine and API Platform. UUIDs work with any database, but they are stored more efficiently in PostgreSQL than MySQL, though we use some UUID's in MySQL in some parts of SymfonyCasts.

But... why am I telling you about UUID's? What's wrong with auto-increment ids? Nothing... but.... UUID's may help simplify your JavaScript code. Suppose we write some JavaScript to create a new CheeseListing. With auto-increment ids, the process looks like this: make a POST request to /api/cheeses, wait for the response, then read the @id off of the response and store it somewhere... because you'll usually need to know the id of each cheese listing. With UUID's, the process looks like this: generate a UUID in JavaScript - that's totally legal - send the POST request and... that's it! With UUID's, you don't need to wait for the AJAX call to finish so you can read the id: you created the UUID in JavaScript, so you already know it. That is why UUID's can often be really nice.

To make this all work, you'll need to configure your entity to use a UUID and add a setId() method so that it's possible for API Platform to set it. Or you can create the auto-increment id and add a separate UUID property. API Platform has an annotation to mark a field as the "identifier".

Normalization & Denormalization Groups

Anyways, let's take control of the serialization process so we can remove any weird fields - like having the encoded password be returned. We'll do the exact same thing we did in CheeseListing: add normalization and denormalization groups. Copy the two context lines, open up User and paste. I'm going to remove the swagger_definition_name part - we don't really need that. For normalization, use user:read and for denormalization, user:write.

... lines 1 - 9
* @ApiResource(
* normalizationContext={"groups"={"user:read"}},
* denormalizationContext={"groups"={"user:write"}},
* )
... line 15
class User implements UserInterface
... lines 18 - 135

We're following the same pattern we've been using. Now... let's think: what fields do we need to expose? For $email, add @Groups({}) with "user:read", "user:write": this is a readable and writable field. Copy that, paste above password and make it only user:write.

... lines 1 - 7
use Symfony\Component\Serializer\Annotation\Groups;
... lines 9 - 16
class User implements UserInterface
... lines 19 - 25
... line 27
* @Groups({"user:read", "user:write"})
private $email;
... lines 31 - 36
... lines 38 - 39
* @Groups({"user:write"})
private $password;
... lines 43 - 133

This... doesn't really make sense yet. I mean, it's not readable anymore, which makes perfect sense. But this will eventually store the encoded password, which is not something that an API client will set directly. But... we're going to worry about all of that in our security tutorial. For now, because password is a required field in the database, let's temporarily make it writable so it doesn't get in our way.

Finally, make username readable and writable as well.

... lines 1 - 16
class User implements UserInterface
... lines 19 - 43
... line 45
* @Groups({"user:read", "user:write"})
private $username;
... lines 49 - 133

Let's try it! Refresh the docs. Just like with CheeseListing we now have two models: we can read email and username and we can write email, password and username.

The only other thing we need to make this a fully functional API resource is validation. To start, both $email and $username need to be unique. At the top of the class, add @UniqueEntity() with fields={"username"}, and another @UniqueEntity() with fields={"email"}.

... lines 1 - 6
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
... lines 8 - 11
... lines 13 - 16
* @UniqueEntity(fields={"username"})
* @UniqueEntity(fields={"email"})
... line 19
class User implements UserInterface
... lines 22 - 142

Then, let's see, $email should be @Assert\NotBlank() and @Assert\Email(), and $username needs to be @Assert\NotBlank(). I won't worry about password yet, that needs to be properly fixed anyways in the security tutorial.

... lines 1 - 9
use Symfony\Component\Validator\Constraints as Assert;
... lines 11 - 20
class User implements UserInterface
... lines 23 - 29
... lines 31 - 32
* @Assert\NotBlank()
* @Assert\Email()
private $email;
... lines 37 - 49
... lines 51 - 52
* @Assert\NotBlank()
private $username;
... lines 56 - 140

So, I think we're good! Refresh the documentation and let's start creating users! Click "Try it out". I'll use my real-life personal email address: cheeselover1@example.com. The password doesn't matter... and let's make the username match the email without the domain... so I don't confuse myself. Execute!

Woohoo! 201 success! Let's create one more user... just to have some better data to play with.

Failing Validation

Oh, and what if we send up empty JSON? Try that. Yea! 400 status code.

Ok... we're done! We have 1 new resource, five new operations, control over the input and output fields, validation, pagination and we could easily add filtering. Um... that's amazing! This is the power of API Platform. And as you get better and better at using it, you'll develop even faster.

But ultimately, we created the new User API resource not just because creating users is fun: we did it so we could relate each CheeseListing to the User that "owns" it. In an API, relations are a key concept. And you're going to love how they work in API Platform.

Leave a comment!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9