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!