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

We have a User entity... but it is not yet part of our API. How do we make it part of the API? Ah, we already know! Go above the class and add the ApiResource attribute.

... lines 1 - 4
use ApiPlatform\Metadata\ApiResource;
... lines 6 - 12
#[ApiResource]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 16 - 116
}

Refresh the docs. Look at that! Six fresh new endpoints for the User class! And thanks to our fixtures, we should be able to see data immediately. Let's try the collection endpoint. Execute and... it's alive.

Though... it is a little weird that fields like roles and password show up. Ah, we'll worry about that in a minute.

API Platform & UUIDs

Before we keep rolling forward, I want to mention one quick thing about UUIDs. As you can see, we're using auto-increment IDs for of our API - it's always /api/users/ then the entity id. But you can totally use a UUID instead. And that's something we'll do in a future tutorial.

But... why would you use UUIDs? Well, sometimes it can make life easier in JavaScript when working with frontend frameworks. You can actually generate the UUID in JavaScript and then send that to your API when creating a new resource. This can help because your JavaScript knows the ID of the resource immediately and can update the state... instead of waiting for the Ajax request to finish to get the new auto-increment id.

Anyways, my point is: API Platform does support UUIDs. You could add a new UUID column, then tell API Platform that it should be your identifier. Oh, but keep in mind that some database engines - like MySQL - can have poor performance if you make the UUID the primary key. In that case, just keep id as the primary key, and add an extra UUID column.

Adding the Serialization Groups

Anyways, back to our User resource! Right now, it's returning way too many fields. Fortunately, we know how to fix that. Up on ApiResource, add a normalizationContext key with groups set to user:read to follow the same pattern that we used in DragonTreasure. Also add denormalizationContext set to user:write.

... lines 1 - 13
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 20 - 123
}

Now we can just decorate the fields that we want in the API. We don't need id... since we always have @id, which is more useful. But we do want email. So add the #Groups() attribute, hit tab to add that use statement and pass both user:read and user:write.

... lines 1 - 9
use Symfony\Component\Serializer\Annotation\Groups;
... lines 11 - 13
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 20 - 25
#[Groups(['user:read', 'user:write'])]
private ?string $email = null;
... lines 28 - 123
}

Copy that... and go down to password. We do need the password to be writeable but not readable. So add user:write.

... lines 1 - 9
use Symfony\Component\Serializer\Annotation\Groups;
... lines 11 - 13
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 20 - 25
#[Groups(['user:read', 'user:write'])]
private ?string $email = null;
... lines 28 - 35
#[Groups(['user:write'])]
private ?string $password = null;
... lines 38 - 123
}

Now this still isn't quite correct. The password field should hold the hashed password. But our users will, of course, send the plaintext passwords via the API when creating a user or updating their password. Then we will hash it. That's something we're going to solve in a future tutorial when we talk more about security. But this will be good enough for now.

Oh, and above username, also add user:read and user:write.

... lines 1 - 9
use Symfony\Component\Serializer\Annotation\Groups;
... lines 11 - 13
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 20 - 25
#[Groups(['user:read', 'user:write'])]
private ?string $email = null;
... lines 28 - 35
#[Groups(['user:write'])]
private ?string $password = null;
... lines 38 - 39
#[Groups(['user:read', 'user:write'])]
private ?string $username = null;
... lines 42 - 123
}

Cool! Refresh the docs... and open up the collections endpoint to give it a go. The result... exactly what we wanted! Only email and username come back.

And if we were to create a new user... yup! The writable fields are email, username, and password.

Adding Validation

Ok, what else are we missing? How about validation? If we try the POST endpoint with empty data... we get that nasty 500 error. Fixing time!

Back over in the file, start above the class to make sure that both email and username are unique. Add UniqueEntity passing fields set to email... and we can even include a message. Repeat that same thing... but change email to username.

... lines 1 - 10
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
... lines 12 - 18
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
#[UniqueEntity(fields: ['username'], message: 'It looks like another dragon took your username. ROAR!')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 23 - 129
}

Next, down in email, add NotBlank... then I'll add the Assert in front... and tweak the use statement so it works just like last time.

... lines 1 - 10
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
... lines 13 - 18
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
#[UniqueEntity(fields: ['username'], message: 'It looks like another dragon took your username. ROAR!')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 23 - 29
#[Assert\NotBlank]
... line 31
private ?string $email = null;
... lines 33 - 129
}

Nice. email needs one more - Assert\Email - and above username, add NotBlank.

... lines 1 - 10
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
... lines 13 - 18
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
#[UniqueEntity(fields: ['username'], message: 'It looks like another dragon took your username. ROAR!')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 23 - 29
#[Assert\NotBlank]
#[Assert\Email]
private ?string $email = null;
... lines 33 - 45
#[Assert\NotBlank]
private ?string $username = null;
... lines 48 - 129
}

I'm not too worried about password right now... because it's already a bit weird.

Let's try this! Scroll up and just send a password field. And... yes! The nice 422 status code with validation errors. Try valid data now: pass an email and username... though I'm not sure this guy's actually a dragon... we might need a captcha.

Hit Execute. That's it! 201 status code with email and username returned!

Our resource has validation, pagination and contains great information! And we could even easily add filtering. In other words, we're crushing it!

And now we get to the really interesting part. We need to "relate" our two resources so that each treasure is owned by a user. What does that look like in API Platform? It's super interesting, and it's next.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice