The User Entity with Symfony Security
The User Entity with Symfony Security
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
So far, the ENTIRE API is public – FROM HERE, the security block begins: EXACTLY as in the Symfony course (chapter 34), EVERYTHING starts with a User entity implementing UserInterface.
Generating the User entity
docker compose exec php bin/console make:userThe INTERACTIVE wizard asks FOR the entity name (User), the unique field (email), and WHETHER a password is needed (yes) – EXACTLY the same questions as in the Symfony course.
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']]
)]
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['user:read'])]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
#[Assert\NotBlank]
#[Assert\Email]
#[Groups(['user:read', 'user:write'])]
private string $email = '';
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
private string $password = '';
#[Groups(['user:write'])]
#[Assert\NotBlank]
private ?string $plainPassword = null;
public function getId(): ?int
{
return $this->id;
}
public function getUserIdentifier(): string
{
return $this->email;
}
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
public function setPlainPassword(?string $plainPassword): static
{
$this->plainPassword = $plainPassword;
return $this;
}
public function eraseCredentials(): void
{
$this->plainPassword = null;
}
}Achtung: plainPassword DELIBERATELY belongs ONLY to user:write, NEVER to user:read – the plaintext password must NEVER, under ANY circumstances, appear in an API response. password (the HASHED password) has NO #[Groups] at all, keeping it COMPLETELY invisible.
Hashing the password
EXACTLY as in the Symfony course (chapter 35), an EventSubscriber or state processor (chapter 61 concept, brought forward HERE) handles automatically hashing plainPassword into password BEFORE saving – details on this in chapter 48.
Creating a migration
docker compose exec php bin/console make:migration
docker compose exec php bin/console doctrine:migrations:migrate --no-interactionTipp: make:user AUTOMATICALLY adjusts config/packages/security.yaml (the providers section with App\Entity\User) – EXACTLY this file gets EXTENDED for JWT in chapter 48.