This documentation is based on the official Symfony Documentation with some API Platform integrations.
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); } }
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