Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Recurso API de usuario

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

Quiero exponer nuestra nueva entidad User como un recurso API. ¡Y ya sabemos cómo hacerlo! Añade... @ApiResource!

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

¡Así de fácil! ¡Sí! Nuestra documentación de la API muestra un nuevo recurso con cinco nuevas rutas, u operaciones. Y en la parte inferior, está el nuevo modelo User.

Hmm, pero es un poco extraño: tanto el campo password con hash como el array roles forman parte de la API. Sí, ¡podríamos crear un nuevo usuario ahora mismo y pasarle los roles que creamos que debe tener! Eso podría estar bien para un usuario administrador, pero no para cualquiera. Tomemos el control de las cosas.

¿Usuarios?

Una cosa que quiero que notes es que, hasta ahora, la clave primaria siempre se utiliza como "id" en nuestra API. Esto es algo que es flexible en la Plataforma API. De hecho, en lugar de utilizar un id autoincrementado, una opción es utilizar un UUID. No vamos a utilizarlos en este tutorial, pero utilizar un UUID como identificador es algo que admiten Doctrine y la Plataforma API. Los UUIDs funcionan con cualquier base de datos, pero se almacenan de forma más eficiente en PostgreSQL que en MySQL, aunque utilizamos algunos UUIDs en MySQL en algunas partes de SymfonyCasts.

Pero... ¿por qué te hablo de UUID's? ¿Qué hay de malo en autoincrementar los ids? Nada... pero.... Los UUID's pueden ayudar a simplificar tu código JavaScript. Supongamos que escribimos un JavaScript para crear un nuevo CheeseListing. Con los ids autoincrementados, el proceso se parece a esto: hacer una petición POST a /api/cheeses, esperar la respuesta, luego leer el @id de la respuesta y almacenarlo en algún sitio... porque normalmente necesitarás saber el id de cada lista de quesos. Con los UUID, el proceso es así: genera un UUID en JavaScript -eso es totalmente legal-, envía la petición POST y... ¡ya está! Con los UUID's, no necesitas esperar a que termine la llamada AJAX para poder leer el id: has creado el UUID en JavaScript, así que ya lo conoces. Por eso los UUID a menudo pueden ser muy útiles.

Para que todo esto funcione, tendrás que configurar tu entidad para que utilice un UUID y añadir un método setId() para que sea posible que la Plataforma API lo establezca. O puedes crear el id de autoincremento y añadir una propiedad UUID independiente. La Plataforma API tiene una anotación para marcar un campo como "identificador".

Grupos de normalización y desnormalización

De todos modos, vamos a tomar el control del proceso de serialización para poder eliminar cualquier campo extraño, como que se devuelva la contraseña codificada. Haremos exactamente lo mismo que hicimos en CheeseListing: añadir grupos de normalización y desnormalización. Copia las dos líneas de contexto, abre User y pégalas. Voy a eliminar la parte deswagger_definition_name: realmente no la necesitamos. Para la normalización, utilizauser:read y para la desnormalización, user:write.

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

Seguimos el mismo patrón que hemos estado utilizando. Ahora... pensemos: ¿qué campos necesitamos exponer? Para $email, añade @Groups({}) con "user:read", "user:write": es un campo legible y escribible. Cópialo, pégalo encima de password y hazlo sólo con 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
}

Esto... no tiene mucho sentido todavía. Es decir, ya no es legible, lo que tiene mucho sentido. Pero esto acabará almacenando la contraseña codificada, que no es algo que un cliente de la API vaya a establecer directamente. Pero... nos preocuparemos de todo eso en nuestro tutorial de seguridad. Por ahora, como la contraseña es un campo obligatorio en la base de datos, vamos a hacerla temporalmente escribible para que no nos estorbe.

Por último, haz que username sea legible y también escribible.

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

¡Vamos a probarlo! Actualiza los documentos. Al igual que con CheeseListing, ahora tenemos dos modelos: podemos leer email y username y podemos escribir email, passwordy username.

Lo único que nos falta para que sea un recurso de la API totalmente funcional es la validación. Para empezar, tanto $email como $username deben ser únicos. En la parte superior de la clase, añade @UniqueEntity() con fields={"username"}, y otro@UniqueEntity() con 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

Entonces, veamos, $email debe ser @Assert\NotBlank() y @Assert\Email(), y $username necesita ser @Assert\NotBlank(). No me preocuparé todavía de la contraseña, eso hay que arreglarlo bien de todos modos en el tutorial de seguridad.

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

Así que, ¡creo que estamos bien! Actualiza la documentación y empecemos a crear usuarios! Haz clic en "Probar". Utilizaré mi dirección de correo electrónico personal de la vida real:cheeselover1@example.com. La contraseña no importa... y hagamos que el nombre de usuario coincida con el correo electrónico sin el dominio... para no confundirme. ¡Ejecuta!

¡Woohoo! ¡201 éxito! Vamos a crear un usuario más... para tener mejores datos con los que jugar.

Validación fallida

¿Y si enviamos un JSON vacío? Pruébalo. ¡Sí! código de estado 400.

Bien... ¡hemos terminado! Tenemos 1 nuevo recurso, cinco nuevas operaciones, control sobre los campos de entrada y salida, validación, paginación y podríamos añadir fácilmente el filtrado... ¡es increíble! Este es el poder de la Plataforma API. Y a medida que vayas mejorando en su uso, desarrollarás aún más rápido.

Pero en última instancia, creamos el nuevo recurso API User no sólo porque crear usuarios es divertido: lo hicimos para poder relacionar cada CheeseListing con el User que lo "posee". En una API, las relaciones son un concepto clave. Y te va a encantar cómo funcionan en la Plataforma API.

Leave a comment!

32
Login or Register to join the conversation
bquarta Avatar

Hello everyone,
a quick "heads up" when you're using PostgreSQL. Since Ryan is talking about some benefits of using UUIDs you should mention that in Postgres "user" is a reserved word ;) ... If you don't want to run into an Error 500 when trying to create a User please add the following line to your Entity:
@Orm\Table("`user`")

This will escape the word in your SQL-Statements and you should be fine :)

best regards
Ben

3 Reply
erop Avatar

... or just @Orm\Table(name=“users”). I mostly like name table with plurals.

Reply
bquarta Avatar

Yeah, that would be even easier :D

Reply
Roozbeh S. Avatar
Roozbeh S. Avatar Roozbeh S. | posted hace 2 años

Hi Ryan,

I am a beginner and I have a question which may not be very relevant to this Tutorial, any help will be appreciated.

I have created a React Native application that has a login page!
A symfony project with a controller and a router to login page and a Database!
Also, an API platform(REST) with User and Person and Token Entities!
(In additions, the "User" Entity has a person_id and Token Entity has a user_id).

In the mobile page, If the User enters the correct email address and the password, the App will send a "POST" request to my Router defined inside the Controller and then the User will be navigated to the main page of the mobile application!
In the main page, when the user clicks on the "profile screen", I need to fetch the data from my API side!

So, Here is what becomes unclear to me!
1. Should my method be "GET" or "POST"?

2-How can I retrieve the User data by submitting a Bearer Authorization Token instead of adding an id to the Url.
Does API Platform support this?

With Regards
Roozbeh

Reply

Hi Roozbeh S. !

Nice to chat with you :). I do need to tell you that I don't have any practical experience with React Native apps, but I will do my best to answer your questions!

> 1. Should my method be "GET" or "POST"?

GET - because you are "fetching" user data.

> 2-How can I retrieve the User data by submitting a Bearer Authorization Token instead of adding an id to the Url.
> Does API Platform support this?

Good question. You have two options for this:

A) The more "pure" option would say that, when you log in, you should receive back some "user data"... most importantly the current user IRI. Then, you can store that in your app and whenever you need to fetch the user's data, you will know what URL to use (e.g. /api/users/5). You will, of course, still need to send the Authorization token. But the point is that the API won't use the token to *identify* the user (you are already telling it which user you want)... it will just be using it to make sure that this Bearer token has *access* to read that user's information.

B) It's also fairly common to make an endpoint like /api/me which you can use to request the "users information" that is related to the sent Token. In API Platform, you would probably implement this as a custom controller on your User resource. I think... I have not actually done that before... which is why I'm slightly guessing on the implementation :).

Cheers!

Reply
Roozbeh S. Avatar

Yes! I have the IRI now!

You've explained it very clearly Ryan!
Thank you!

Reply
JavierMendezK Avatar
JavierMendezK Avatar JavierMendezK | posted hace 2 años

My schema shows correct on the User-user.read and User-user.write but on the POST operation, it is not showing "password" and it will not work unless I add the user:read group for password. Any ideas?
Using symfony 5.2

Reply

Hey JavierMendezK

That's a bit odd. I'd expect the password field to be present in the POST operation. Do you have a setter method for the password field? I think I'd need to see your entity code

Cheers!

Reply
JavierMendezK Avatar
JavierMendezK Avatar JavierMendezK | MolloKhan | posted hace 2 años

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ApiResource(
* attributes={"security"="is_granted('ROLE_USER')"},
* collectionOperations={
* "get",
* "post" = { "security" = "is_granted('IS_AUTHENTICATED_ANONYMOUSLY')" }
* },
* itemOperations={
* "get",
* "put" = { "security" = "is_granted('ROLE_USER') and object == user" },
* "delete" = { "security" = "is_granted('ROLE_ADMIN')" }
* },
* normalizationContext={"groups"={"user:read"}},
* denormalizationContext={"groups"={"user:write"}},
* )
* @ApiFilter(PropertyFilter::class)
* @UniqueEntity(fields={"username"})
* @UniqueEntity(fields={"email"})
* @ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=180, unique=true)
* @Groups({"user:read", "user:write"})
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;

/**
* @ORM\Column(type="json")
*/
private $roles = [];

/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;

/**
* @Groups({"user:write"})
*/
private $plainPassword;

/**
* @ORM\Column(type="string", length=255, unique=true)
* @Groups({"user:read", "user:write","computer_listing:item:get", "computer_listing:write"})
* @Assert\NotBlank()
*/
private $username;

// use orphanRemoval=true to remove the computers that have no owner
/**
* @ORM\OneToMany(targetEntity=ComputerListing::class, mappedBy="owner", cascade={"persist"})
* @Groups({"user:read","user:write"})
* @Assert\Valid()
*/
private $computerListings;

public function __construct()
{
$this->computerListings = new ArrayCollection();
}

public function getId(): ?int
{
return $this->id;
}

public function getEmail(): ?string
{
return $this->email;
}

public function setEmail(string $email): self
{
$this->email = $email;

return $this;
}

/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}

/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';

return array_unique($roles);
}

public function setRoles(array $roles): self
{
$this->roles = $roles;

return $this;
}

/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}

public function setPassword(string $password): self
{
$this->password = $password;

return $this;
}

/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}

/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
$this->plainPassword = null;
}

public function setUsername(string $username): self
{
$this->username = $username;

return $this;
}

/**
* @return Collection|ComputerListing[]
*/
public function getComputerListings(): Collection
{
return $this->computerListings;
}

public function addComputerListing(ComputerListing $computerListing): self
{
if (!$this->computerListings->contains($computerListing)) {
$this->computerListings[] = $computerListing;
$computerListing->setOwner($this);
}

return $this;
}

public function removeComputerListing(ComputerListing $computerListing): self
{
if ($this->computerListings->removeElement($computerListing)) {
// set the owning side to null (unless already changed)
if ($computerListing->getOwner() === $this) {
$computerListing->setOwner(null);
}
}

return $this;
}

public function getPlainPassword(): ?string
{
return $this->plainPassword;
}

public function setPlainPassword(string $plainPassword): self
{
$this->plainPassword = $plainPassword;

return $this;
}

}

Reply

Hey Javier, sorry for my slow reply. The field password should not be shown on any operation because you don't have any serialization group for it, but the plainPassword field should be shown on POST operations due to the user:write group

Reply
TristanoMilano Avatar
TristanoMilano Avatar TristanoMilano | MolloKhan | posted hace 2 años | edited

I have got the same problem and my code is exactly the same as the example code as i followed the tutorial step by step.
To verify that i made no errors i then just copied from the codeblock in this chapter. The schemas for read and write are shown as intended in the part of the bottom of the swagger doc page:

ApiUser-user.write{
email*	string($email)
externalDocs: OrderedMap { "url": "http://schema.org/email" }
password	string

The hashed password
username*	string

But: above, under POST and PUT my password fild is missing, my example values are the following:

{
  "email": "user@example.com",
  "username": "string"

When i manually add the "password" : "some string" field and click on "execute", everything works as intended.

Even more interestingy, when i click on "schema" right next to "Example Value", is says:

ApiUser.jsonld-user.read{
email*	string($email)
externalDocs: OrderedMap { "url": "http://schema.org/email" }

Shouldn't it use the write model here?

Reply

Hey Tristan P.!

Hmm. It's interesting that we now have two people with the same issue in a short period of time. And in fact (as you already noticed @Tristano! ) there is a third person with an issue that is suspiciously similar: https://symfonycasts.com/sc...

My gut is that there has been a change in API Platform that is causing this. And if so, I believe it is a bug. If you can open an issue and link me to it, I can help push that along.

Cheers!

Reply
Zaz Avatar

Hi,
I have the same issue using symfony 5.3.1

Reply

Yup, there's an open issue about it - no fix proposed at the moment :/ https://github.com/api-plat...

Reply

I'm having the same problem here, I installed api platform by the official guide with docker, so the version is different the one you used for the tutorial, api platform 2.6 symfony 5.2

Reply

Yea, it looks like there is a bug in 2.6.3 related to this - there are several open issues about it - it looks like you already found the main one ;) https://github.com/api-plat...

Cheers!

Reply
Covi A. Avatar

Hey,
did you find the solution? if yes then please show us because i face exactly same problem.

Reply

Hey Monoranjan,

Unfortunately, no official possible solution as I can see the main issue is still open: https://github.com/api-plat... - you probably may look over the thread to find possible workaround if any.

Cheers!

Reply
Thomas C. Avatar
Thomas C. Avatar Thomas C. | posted hace 3 años

Hello everyone,

when i post new user return error 500 with message "General error: 1364 Field 'roles' doesn't have a default value" but my code is the same as eample code. What is wrong?

Reply

Hey Thomas C.

That's odd. Can you double-check that your roles property is initially set to an empty array?


// User.php

private $roles = [];

Cheers!

Reply
Thomas C. Avatar

Yes i have check this. To solve i have modify this in the db ALTER TABLE `user` CHANGE `roles` `roles` TEXT NULL.

My code was :
/*
*
* @ORM\Column(type="json")
*/
private $roles = [];

Reply

Hmm, that's odd. If you persist an User with an empty array in its roles property, it should store a json like this <code[]` and that's not considered as empty for a Database

Reply
Ioan adrian S. Avatar
Ioan adrian S. Avatar Ioan adrian S. | posted hace 3 años | edited

Hello,
It is possible to make a post request providing the primary key value too?
e.g.


{
  id: 1,
  name: "Szabo"
}

I ask because when I try to make the post I get
{
"code": 500,
"message": "Update is not allowed for this operation."
}

Reply

Hey @Szabo!

This *should* be possible, but it would be a PUT request. It's an HTTP "spec" thing - if you're creating an object but *providing* the primary key, then the proper HTTP method is PUT. Try PUT and let me know if it works.

Cheers!

Reply
Joel D. Avatar

Hi everyone!
I thought everyting was undercontrole util I saw in my Mysql database two things:
- the password hasen't been hashed
- no role has been set too

And one question: is it normal that the id inserted has no change compare to an normal autoIncrement, what the difference of having the UUID orientation ? '

I post my new user thanks to the api_doc via the POST tab

thank you for any return which can light this up

Reply

Hey man,

Good observation. Hashing the password and adding roles is a job you have to do and has nothing to do with ApiPlatform. Ryan talks more about that in the next course: https://symfonycasts.com/sc...

About the id thing. How's your configuration? If you are using UUID's then it should be totally different than an auto-incremented id

Cheers!

Reply
Adam D. Avatar

I am sure I am doing something stupid, but I am getting this error when adding the groups annotation to the email property in the user entity...

[Semantical Error] The annotation "@Groups" in property App\Entity\User::$email was never imported. Did you maybe forget to add a "use" statement for this annotation?

Solution:

as per
https://api-platform.com/do...

# api/config/packages/framework.yaml
framework:
serializer: { enable_annotations: true }

#in your user entity
use Symfony\Component\Serializer\Annotation\Groups;

#and you will need this later...
use Symfony\Component\Validator\Constraints as Assert;

This had me stumped for awhile as it is not evident in the tutorial video... maybe a good idea to update this considering it is paid for (well I paid for it). Or it could be that I missed some kind of importation at the start of things...

Reply

Hey Adam D.!

Good job finding the solution! Like all errors, it's a tough error the first time you see it - hopefully it won't be next time! Our code *does* have this use statement, but you're right that you don't see it in the video. The reason is that we *often* allow classes (or annotations) to auto-complete in PhpStorm. When we do this, it automatically adds the use statement needed on top (you need the php-annotations plugin installed for this to work with annotations). I try to mention that this is happening from time-to-time... but if I mention it *every* time, it gets laborious. It's a tricky balance :/. But now that you know it, I think you should be good. If you ever have some mysterious problem, you can also expand the full code block below the video and see *exactly* what our code looks like.

Cheers!

Reply
None N. Avatar

Hey, I get the error below when selecting Argon2i. How can I get around it? Running a mac with brew installed php.

Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.

Reply

Hey @Codsworth

You can try pecl install libsodium just be sure that brew installed php is fully configured

Cheers

Reply
None N. Avatar

Thanks Vladimir,

I'll give that a try!

Reply
Cat in space

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

Este tutorial funciona muy bien para Symfony 5 y la Plataforma API 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
    }
}
userVoice