Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:user

The 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.

api/src/Entity/User.php
<?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-interaction

Tipp: 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.