forked from api-platform/core
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5869d00
commit 1cd2b4d
Showing
1 changed file
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
<?php | ||
// --- | ||
// slug: user-entity | ||
// name: User Entity | ||
// position: 10 | ||
// executable: true | ||
// --- | ||
|
||
// This documentation is based on the [official Symfony Documentation](https://symfony.com/doc/current/security.html) | ||
// with some API Platform integrations. | ||
// | ||
// You can follow the [official Symfony Documentation](https://symfony.com/doc/current/security.html) 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: | ||
|
||
namespace App\Entity { | ||
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 App\Repository\UserRepository; | ||
use App\State\UserPasswordHasher; | ||
use Doctrine\ORM\Mapping as ORM; | ||
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), | ||
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 $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<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](https://api-platform.com/docs/core/state-processors/) 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! |