📖 Symfony Framework Toàn tập - Forms, Validation và Security
80 phút

Forms, Validation và Security trong Symfony

Form Types

<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Length;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => 'Full Name',
                'attr' => ['placeholder' => 'Enter your name'],
                'constraints' => [
                    new Length(['min' => 2, 'max' => 100]),
                ],
            ])
            ->add('email', EmailType::class, [
                'label' => 'Email Address',
            ])
            ->add('password', RepeatedType::class, [
                'type' => PasswordType::class,
                'first_options' => ['label' => 'Password'],
                'second_options' => ['label' => 'Confirm Password'],
                'invalid_message' => 'The passwords must match.',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

Form trong Controller

#[Route('/register', name: 'app_register')]
public function register(
    Request $request,
    UserService $userService
): Response {
    $user = new User();
    $form = $this->createForm(UserType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $userService->create($user);

        $this->addFlash('success', 'Registration successful!');
        return $this->redirectToRoute('app_login');
    }

    return $this->render('user/register.html.twig', [
        'form' => $form,
    ]);
}

Template cho Form

{# templates/user/register.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <div class="container">
        <h1>Register</h1>

        {{ form_start(form) }}
            {{ form_row(form.name) }}
            {{ form_row(form.email) }}
            {{ form_row(form.password) }}

            <button type="submit" class="btn btn-primary">Register</button>
        {{ form_end(form) }}
    </div>
{% endblock %}

Custom Validator

<?php

namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[Attribute]
class UniqueEmail extends Constraint
{
    public string $message = 'This email is already registered: {{ email }}';

    public function validatedBy(): string
    {
        return UniqueEmailValidator::class;
    }

    public function getTargets(): string
    {
        return self::CLASS_CONSTRAINT;
    }
}
<?php

namespace App\Validator;

use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class UniqueEmailValidator extends ConstraintValidator
{
    public function __construct(
        private UserRepository $repository
    ) {}

    public function validate(mixed $value, Constraint $constraint): void
    {
        if (!$value instanceof User) return;

        if ($this->repository->findByEmail($value->getEmail())) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ email }}', $value->getEmail())
                ->atPath('email')
                ->addViolation();
        }
    }
}

Security Configuration

config/packages/security.yaml

security:
    password_hashers:
        App\Entity\User:
            algorithm: bcrypt
            cost: 12

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            jwt: ~

        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
                default_target_path: app_dashboard
            logout:
                path: app_logout
                target: app_home

    access_control:
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/register, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/api, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

Custom Security Voter

<?php

namespace App\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;

class PostVoter extends Voter
{
    public const VIEW = 'POST_VIEW';
    public const EDIT = 'POST_EDIT';
    public const DELETE = 'POST_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) return false;

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::VIEW => true,
            self::EDIT, self::DELETE => $this->isOwner($user, $post),
            default => false,
        };
    }

    private function isOwner(User $user, Post $post): bool
    {
        return $user === $post->getAuthor() || in_array('ROLE_ADMIN', $user->getRoles());
    }
}

Login Controller

#[Route('/login', name: 'app_login')]
class SecurityController extends AbstractController
{
    #[Route('', name: 'login', methods: ['GET', 'POST'])]
    public function login(AuthenticationUtils $authUtils): Response
    {
        if ($this->getUser()) {
            return $this->redirectToRoute('app_dashboard');
        }

        $error = $authUtils->getLastAuthenticationError();
        $lastUsername = $authUtils->getLastUsername();

        return $this->render('security/login.html.twig', [
            'last_username' => $lastUsername,
            'error' => $error,
        ]);
    }

    #[Route('/logout', name: 'logout', methods: ['GET'])]
    public function logout(): void
    {
        throw new \LogicException('This method can be blank');
    }
}

Template Login

{% extends 'base.html.twig' %}

{% block body %}
    <form method="post">
        {% if error %}
            <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
        {% endif %}

        {% if app.user %}
            <div>You are logged in as {{ app.user.userIdentifier }}</div>
        {% endif %}

        <h1>Please sign in</h1>
        <label for="username">Email</label>
        <input type="email" value="{{ last_username }}" name="_username" id="username" required autofocus>

        <label for="password">Password</label>
        <input type="password" name="_password" id="password" required>

        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

        <button type="submit">Sign in</button>
    </form>
{% endblock %}

Bài tập thực hành

Hãy implement registration và login hoàn chỉnh!

📝 Bài tập (1)

  1. Implement authentication system