2

Rozdział 2 z 21

Fundament — User, bezpieczeństwo i użytkownicy

Opublikowany

Zanim zbudujemy blog, potrzebujemy użytkowników — artykuł ma autora, komentarz ma autora, a panel admina wymaga logowania. Ten rozdział buduje fundament: encję User (logowanie e-mailem, role, token API), konfigurację security.yaml oraz UserFixtures z referencjami. Zakłada działające środowisko z Rozdziału 1.

Czego się nauczysz

  • Encja User (UserInterface, e-mail jako identyfikator, role, apiToken)
  • security.yaml — hasła, provider, firewalle, access_control
  • UserFixtures z addReference() (wymagane przez BlogFixtures)
  • Migracja tabeli "user"

Rozdział 2: Fundament — User, bezpieczeństwo i użytkownicy

Zanim zbudujemy blog, potrzebujemy użytkowników. Artykuł ma autora, komentarz ma autora, a panel admina wymaga logowania. Ten rozdział stawia wspólny fundament: encję User, konfigurację bezpieczeństwa i dane testowe użytkowników. To on domyka łańcuch, którego wymaga BlogFixtures z Rozdziału 3.

Kolejność: ten rozdział zakłada działające środowisko z Rozdziału 1 (Docker, make start). Jeśli masz już encję User w projekcie, potraktuj ten rozdział jako referencję. Budując od zera, wykonaj go przed encjami bloga (Rozdział 3).


Encja User

User implementuje dwa interfejsy Symfony Security: UserInterface (tożsamość i role) oraz PasswordAuthenticatedUserInterface (hasło). Logujemy e-mailem, a nie loginem:

// src/Entity/User.php
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180)]
    private ?string $email = null;

    /** @var list<string> The user roles */
    #[ORM\Column]
    private array $roles = [];

    /** @var string The hashed password */
    #[ORM\Column]
    private ?string $password = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $createdAt = null;

    #[ORM\Column(length: 64, unique: true, nullable: true)]
    private ?string $apiToken = null;
}

Trzy metody wymagane przez interfejsy (poza getterami/setterami):

public function getUserIdentifier(): string
{
    return (string) $this->email;   // e-mail jednoznacznie identyfikuje użytkownika
}

public function getRoles(): array
{
    $roles = $this->roles;
    $roles[] = 'ROLE_USER';          // każdy zalogowany ma co najmniej ROLE_USER
    return array_unique($roles);
}

public function eraseCredentials(): void
{
    // wyczyść dane wrażliwe trzymane tymczasowo (tu: brak)
}

Warto zauważyć:

  • #[ORM\Table(name: 'user')] — user to słowo zarezerwowane w PostgreSQL, dlatego nazwa tabeli jest w backtickach.
  • Unikalny email (UniqueConstraint) — to identyfikator logowania.
  • roles jako array (JSON w bazie) — prosta autoryzacja bez osobnej tabeli ról.
  • apiToken (unikalny, nullable) — przyda się dopiero w Rozdziale 17 (uwierzytelnianie API tokenem); tu tylko deklarujemy pole.

Bez FOSUserBundle. Symfony 8 ma natywne bezpieczeństwo — encję generuje make:user (docker compose exec app php bin/console make:user), a hasła hashuje wbudowany hasher. Zewnętrzny bundle nie jest potrzebny.


Konfiguracja bezpieczeństwa (security.yaml)

config/packages/security.yaml spina wszystko: hashowanie haseł, źródło użytkowników (provider), firewalle i reguły dostępu:

security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email          # logowanie po e-mailu

    firewalls:
        dev:
            pattern: ^/(_profiler|_wdt|assets|build)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                default_target_path: app_dashboard
                enable_csrf: true
            logout:
                path: app_logout
                target: app_start

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/dashboard, roles: ROLE_USER }

Co tu jest ważne dla bloga:

  • password_hashers: 'auto' — Symfony sam wybiera najlepszy algorytm (bcrypt/argon) i sam weryfikuje hasła.
  • provider z encji po email — po zalogowaniu Symfony wczyta User z bazy po adresie e-mail.
  • form_login — logowanie przez formularz (/login), z ochroną CSRF.
  • access_control na ^/admin — cały panel admina bloga (Rozdział 8) wymaga ROLE_ADMIN.

Firewall api (dla ^/api) dokładamy dopiero w Rozdziale 17, razem z uwierzytelnianiem tokenem. Do Części I–IV wystarcza firewall main.


Użytkownicy testowi (UserFixtures)

BlogFixtures (Rozdział 3) pobiera autora artykułu przez getReference('user_...'). Te referencje rejestruje UserFixtures. Hasła hashujemy wstrzykniętym UserPasswordHasherInterface:

// src/DataFixtures/UserFixtures.php
namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserFixtures extends Fixture
{
    public function __construct(
        private UserPasswordHasherInterface $hasher,
    ) {}

    public function load(ObjectManager $manager): void
    {
        $users = [
            ['name' => 'Admin Główny', 'email' => 'admin@example.com',
             'password' => 'Admin1234!', 'roles' => ['ROLE_ADMIN'],
             'apiToken' => 'dev-admin-api-token'],
            ['name' => 'Jan Kowalski', 'email' => 'jan.kowalski@example.com',
             'password' => 'User1234!', 'roles' => []],
            ['name' => 'Piotr Wiśniewski', 'email' => 'piotr.wisniewski@example.com',
             'password' => 'User1234!', 'roles' => ['ROLE_MODERATOR']],
        ];

        foreach ($users as $data) {
            $user = new User();
            $user->setName($data['name']);
            $user->setEmail($data['email']);
            $user->setRoles($data['roles']);
            $user->setPassword($this->hasher->hashPassword($user, $data['password']));
            $user->setCreatedAt(new \DateTimeImmutable());
            $user->setApiToken($data['apiToken'] ?? null);

            $manager->persist($user);
            // KLUCZOWE: referencja, po którą sięga BlogFixtures
            $this->addReference('user_' . $data['email'], $user);
        }

        $manager->flush();
    }
}

addReference('user_admin@example.com', $user) to most między fixturami: BlogFixtures implementuje DependentFixtureInterface (zależy od UserFixtures) i pobiera admina przez getReference('user_admin@example.com', User::class) — bez tworzenia go na nowo.

Konta, których użyjesz w tutorialu:

E-mail Hasło Rola
admin@example.com Admin1234! ROLE_ADMIN
jan.kowalski@example.com User1234! ROLE_USER
piotr.wisniewski@example.com User1234! ROLE_MODERATOR

Migracja tabeli „user”

Encja gotowa — generujemy i uruchamiamy migrację, a potem ładujemy użytkowników:

docker compose exec app php bin/console make:migration
docker compose exec app php bin/console doctrine:migrations:migrate
docker compose exec app php bin/console doctrine:fixtures:load

Powstanie CREATE TABLE "user" (kolumny id, email, roles JSON, password, name, created_at, api_token) z unikalnym indeksem na email. Sprawdź:

docker compose exec database psql -U app -d app -c '\d "user"'

Podsumowanie

Fundament pod blog stoi:

  • ✅ encja User (natywne interfejsy Security, logowanie e-mailem, roles, apiToken) — bez FOSUserBundle,
  • ✅ security.yaml: password_hashers: auto, provider z encji po email, firewall main (form_login + logout), access_control na ^/admin,
  • ✅ UserFixtures z addReference() — most do BlogFixtures,
  • ✅ migracja tabeli "user" i załadowani użytkownicy testowi.

W Rozdziale 3 zbudujemy encje bloga — BlogArticle, BlogCategory, BlogTag i BlogComment — których autorzy i komentujący to właśnie utworzeni tu użytkownicy.

Narzędzia / paczki

Security UserInterface DoctrineFixturesBundle

Spis treści