From 37d068ffd6a46c12cb5470c5e1f35699cc027a3a Mon Sep 17 00:00:00 2001 From: Vincent Chalamon Date: Mon, 9 Jan 2023 15:32:18 +0100 Subject: [PATCH] docs: add user-entity guide --- docs/guide/099-user.php | 268 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 docs/guide/099-user.php diff --git a/docs/guide/099-user.php b/docs/guide/099-user.php new file mode 100644 index 00000000000..10d38d85792 --- /dev/null +++ b/docs/guide/099-user.php @@ -0,0 +1,268 @@ + ['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 $painPassword): self + { + $this->plainPassword = $painPassword; + + 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: + +namespace App\Repository { + use App\Entity\User; + use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; + use Doctrine\Persistence\ManagerRegistry; + use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + + /** + * @extends ServiceEntityRepository + * + * @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](/docs/guide/hook-a-persistence-layer-with-a-processor) for auto-hashing plain +// passwords. +// +// First create a new state processor: + +namespace App\State { + use ApiPlatform\Metadata\Operation; + use ApiPlatform\State\ProcessorInterface; + use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; + + final class UserPasswordHasher implements ProcessorInterface + { + public function __construct(private readonly ProcessorInterface $processor, private readonly UserPasswordHasherInterface $passwordHasher) + { + } + + public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + { + 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: +// ```yaml +// # 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: +// ```php +// 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!