Skip to content

Commit

Permalink
docs: add user-entity guide
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Jan 9, 2023
1 parent 5869d00 commit 1cd2b4d
Showing 1 changed file with 267 additions and 0 deletions.
267 changes: 267 additions & 0 deletions docs/guide/099-user.php
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!

0 comments on commit 1cd2b4d

Please sign in to comment.