Api Platform conference
Register now

This documentation is based on the official Symfony Documentation with some API Platform integrations.

# Creating the Entity and Repository

You can follow the official Symfony Documentation and add the API Platform attributes (e.g. #[ApiResource]) by your own, or just use the following entity file and modify it to your needs:

<?php // api/src/Entity/User.php  namespace App\Entity;  use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use Doctrine\ORM\Mapping as ORM; use App\Repository\UserRepository; use App\State\UserPasswordHasher; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert;  #[ApiResource(  operations: [  new GetCollection(),  new Post(processor: UserPasswordHasher::class, validationContext: ['groups' => ['Default', 'user:create']]),  new Get(),  new Put(processor: UserPasswordHasher::class),  new Patch(processor: UserPasswordHasher::class),  new Delete(),  ],  normalizationContext: ['groups' => ['user:read']],  denormalizationContext: ['groups' => ['user:create', 'user:update']], )] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] #[UniqueEntity('email')] class User implements UserInterface, PasswordAuthenticatedUserInterface {  #[Groups(['user:read'])]  #[ORM\Id]  #[ORM\Column(type: 'integer')]  #[ORM\GeneratedValue]  private ?int $id = null;   #[Assert\NotBlank]  #[Assert\Email]  #[Groups(['user:read', 'user:create', 'user:update'])]  #[ORM\Column(length: 180, unique: true)]  private ?string $email = null;   #[ORM\Column]  private ?string $password = null;   #[Assert\NotBlank(groups: ['user:create'])]  #[Groups(['user:create', 'user:update'])]  private ?string $plainPassword = null;   #[ORM\Column(type: 'json')]  private array $roles = [];   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;  }   /**  * @see PasswordAuthenticatedUserInterface  */  public function getPassword(): string  {  return $this->password;  }   public function setPassword(string $password): self  {  $this->password = $password;   return $this;  }   public function getPlainPassword(): ?string  {  return $this->plainPassword;  }   public function setPlainPassword(?string $plainPassword): self  {  $this->plainPassword = $plainPassword;   return $this;  }   /**  * @see UserInterface  */  public function getRoles(): array  {  $roles = $this->roles;   $roles[] = 'ROLE_USER';   return array_unique($roles);  }   public function setRoles(array $roles): self  {  $this->roles = $roles;   return $this;  }   /**  * A visual identifier that represents this user.  *  * @see UserInterface  */  public function getUserIdentifier(): string  {  return (string) $this->email;  }   /**  * @see UserInterface  */  public function eraseCredentials(): void  {  $this->plainPassword = null;  } }

The repository is same as generated by Symfony. For completeness:

<?php // api/src/Repository/UserRepository.php  namespace App\Repository;  use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; use App\Entity\User; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;  /**  * @extends ServiceEntityRepository<User>  *  * @method User|null find($id, $lockMode = null, $lockVersion = null)  * @method User|null findOneBy(array $criteria, array $orderBy = null)  * @method User[] findAll()  * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)  */ class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface {  public function __construct(ManagerRegistry $registry)  {  parent::__construct($registry, User::class);  }   public function save(User $entity, bool $flush = false): void  {  $this->getEntityManager()->persist($entity);   if ($flush) {  $this->getEntityManager()->flush();  }  }   public function remove(User $entity, bool $flush = false): void  {  $this->getEntityManager()->remove($entity);   if ($flush) {  $this->getEntityManager()->flush();  }  }   /**  * Used to upgrade (rehash) the user's password automatically over time.  */  public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void  {  if (!$user instanceof User) {  throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));  }   $user->setPassword($newHashedPassword);   $this->save($user, true);  } }

# Creating and Updating User Password

There’s no built-in way for hashing the plain password on POST, PUT or PATCH. Happily you can use the API Platform state processors for auto-hashing plain passwords.

First create a new state processor:

<?php // api/src/State/UserPasswordHasher.php  namespace App\State;  use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;  /**  * @implements ProcessorInterface<User, User|void>  */ final readonly class UserPasswordHasher implements ProcessorInterface {  public function __construct(  private ProcessorInterface $processor,  private UserPasswordHasherInterface $passwordHasher  )  {  }   /**  * @param User $data  */  public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User  {  if (!$data->getPlainPassword()) {  return $this->processor->process($data, $operation, $uriVariables, $context);  }   $hashedPassword = $this->passwordHasher->hashPassword(  $data,  $data->getPlainPassword()  );  $data->setPassword($hashedPassword);  $data->eraseCredentials();   return $this->processor->process($data, $operation, $uriVariables, $context);  } }

Then bind it to the ORM persist processor:

# api/config/services.yaml  services:  # ...   App\State\UserPasswordHasher:  bind:  $processor: '@api_platform.doctrine.orm.state.persist_processor'

You may have wondered about the following lines in our entity file we created before:

 operations: [  ...  new Post(processor: UserPasswordHasher::class),  new Put(processor: UserPasswordHasher::class),  new Patch(processor: UserPasswordHasher::class),  ...  ],

This just means we want to run the new created state processor to these specific operations. So we’re done. Create a new user, change the password and enjoy!

You can also help us improve the documentation of this page.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop