Is it possible to apply validation constraints to a Symfony login form?
To me it looks like that won't work. I use Symfony 5.2
I have created a Symfony login form as described on the documentation page "https://symfony.com/doc/current/security/form_login_setup.html".
Now I would like to validate the form and have created the following constraints for this in "validation.yaml".
App \ Entity \ User:
properties:
username:
- NotBlank:
message: 'form.user.username.not_blank'
password:
- NotBlank:
message: 'form.user.password.not_blank'
Unfortunately the constraints are ignored.
If I leave the username and password fields blank, I get a message that the login failed. In such a case, I would like to receive the constraint message that the username and password cannot be empty.
I didn't get any further during my research on the Internet.
Could it be that no validation constraints can be used in a Symfony login form?
Has anyone of you successfully set up validation constraints in a Symfony 5 login form and can you give me a tip on what to look out for?
I stumbled upon a similar issue - and used the following solution:
Since the authentification happens before the regular form validation I implemented a custom validation in the 'authenticate' method of my LoginFormAuthenticator:
public function authenticate(Request $request): PassportInterface
{
$credentials = $request->get('login');
<snip>
$errors = $this->validateCredentials($credentials);
if (0 !== $errors->count()) {
throw new AuthenticationException();
}
return new Passport(
new UserBadge($credentials['email']),
new PasswordCredentials($credentials['password']),
[
new CsrfTokenBadge('login_token', $credentials['_csrf_token']),
new RememberMeBadge(),
]
);
}
The validateCredentials method which stores the $error-object in the session:
public function validateCredentials($credentials) {
$constraints = new Assert\Collection([
'fields' => [
'email' =>
new Assert\Sequentially([
new Assert\NotBlank([
'message' => 'login.email.not_blank'
]),
new Assert\Email([
'message' => 'login.email'
])
]),
<snip>
],
'allowExtraFields' => true
]);
$errors = $this->validator->validate(
$credentials,
$constraints
);
if (0 !== $errors->count()) {
$this->session->set('login-errors', $errors);
} else {
$this->session->remove('login-errors');
}
return $errors;
}
The SecurityController fetches the $error-object from the session and adds the respective errors to the login form:
$loginForm = $this->createForm(LoginType::class, $formData);
$loginErrors = $request->getSession()->get('login-errors');
if ($loginErrors) {
foreach ($loginErrors as $error) {
$propertyPath = trim($error->getPropertyPath(), '[]');
$errorMessage = $error->getMessage();
$loginForm->get($propertyPath)->addError(new FormError($errorMessage));
}
}
Most likely not the best approach - but it does the job reasonably well and it's only the login form that makes this extra validation step necessary.
With model
You have to add these constraints to the entitiy (if you are using it).
I'd suggest to use the annotations:
https://symfony.com/doc/current/validation.html#configuration
Just add those to your entity.
In Your Model (Entity):
use Symfony\Component\Validator\Constraints as Assert;
class MyClass
{
/**
* #Assert\NotBlank
*/
private $myVarToBeAsserted;
}
Without model
If you are using a data class (no model behind the form), you can use annotations as well. But in this case you have to add those to your form itself:
https://symfony.com/doc/current/form/without_class.html
MyFormType:
$builder->add(
'birth',
DateType::class,
[
'required' => true,
'constraints' =>
[
new NotBlank(),
new Date()
],
'label' => $labelBirth,
'widget' => 'single_text',
'html5' => true,
'attr' =>
[
],
]
);
... and in controller:
$form = $this->createForm(
MyFormType::class,
[
'data_class' => true
],
);
What you did btw: You defined the message for those possible assertions. Which would be shown if your assertion NotBlank() would be triggered.
thank you for your answer to my question.
I have just tested it with annotations, but unfortunately it doesn't work for me even with that. When I submit the "empty" login form, I get the message "Invalid credentials.".
I don't understand why Symfony is checking the login data here even though the form fields are empty.
Before the access data can be validated, it must first be checked whether the form has been filled out correctly.
Here is some sample code to illustrate what I'm trying to do.
User.php
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
/**
* #return string|null
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* #param string|null $username
* #return $this
*/
public function setUsername(?string $username): self
{
$this->username = $username;
return $this;
}
/**
* #return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* #param string|null $password
* #return $this
*/
public function setPassword(?string $password): self
{
$this->password = $password;
return $this;
}
/**
* #return array
*/
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* #param array $roles
* #return $this
*/
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #return string|null
*/
public function getSalt() :?string
{
return null;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
}
}
LoginType.php
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\User;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LoginType extends AbstractType
{
public const CAPTCHA_REQUIRED = 'captcha_required';
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username', TextType::class, ['label' => 'login_form.username'])
->add('password', PasswordType::class, ['label' => 'login_form.password'])
->add('submit', SubmitType::class, ['label' => 'button.login']);
if ($options[self::CAPTCHA_REQUIRED]) {
$builder
->add('captcha', CaptchaType::class, ['label' => 'login_form.captcha']);
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
self::CAPTCHA_REQUIRED => false,
]);
}
}
validator.yaml
framework:
validation:
enabled: true
email_validation_mode: html5
validation.yaml
App\Entity\User:
properties:
username:
- NotBlank: ~
password:
- NotBlank: ~
Incidentally, I have no problems with any of the other forms.
Only especially with my login form does the validation of the constraints not work.
I suspect because Symfony first validates the access data instead of checking the constraints first.
Have you ever successfully used constraints in a Syfmony login form?
thank you for your proposed solution.
Your suggestion helped me a lot.
I have now implemented the validation in my login form in this way.
In my LoginFormAuthenticator class I added a new method "validate". This method validates the login form and saves the errors in the session.
private function validate(array $credentials)
{
$user = new User();
$user->setUsername($credentials['username'])
->setPassword($credentials['password']);
$errors = $this->validator->validate($user);
if (0 !== $errors->count()) {
$this->session->set(SessionKey::LOGIN_VALIDATION_ERRORS, $errors);
} else {
$this->session->remove(SessionKey::LOGIN_VALIDATION_ERRORS);
}
}
In my SecurityController class, I check whether validation errors are stored in the session. If there are any, I will add them to the login form as you have already described in your post.
$loginErrors = $request->getSession()->get(SessionKey::LOGIN_VALIDATION_ERRORS);
if ($loginErrors) {
foreach ($loginErrors as $error) {
$propertyPath = trim($error->getPropertyPath(), '[]');
$errorMessage = $error->getMessage();
$form->get($propertyPath)->addError(new FormError($errorMessage));
}
}
For me this is a workable solution. Maybe not nice but it works.
This is how I did Symfony 4.4 login form server-side validation. It must be done before Symfony performs its built-in authentication checks: in my AppCustomAuthenticator I added a local validator in the getCredentials() method:
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
// I need to validate the login form fields before the actual authentication
// do NOT validate with entity annotations, but with local validator (otherwise, the User entity validations would trigger errors like it would for create/update User)
$dataConstraints = new Assert\Collection([
'fields' => [
'username' => [
new Assert\NotBlank([
'message' => 'Username cannot be empty'
]),
new Assert\Length([
'min' => 4,
'max' => 100,
'minMessage' => 'Username is too short. It should have 4 characters or more.',
'maxMessage' => 'Username is too long. It should have 100 characters or less.'
])
],
'password' => [
new Assert\NotBlank([
'message' => 'Password cannot be empty'
]),
new Assert\Length([
'min' => 4,
'max' => 100,
'minMessage' => 'Password is too short. It should have 4 characters or more.',
'maxMessage' => 'Password is too long. It should have 100 characters or less.'
])
]
],
// to allow more fields in the first parameter of $this->validator->validate() [which is the array of fields to validate], but which have no constraint in the second parameter (in $dataConstraints)
'allowExtraFields' => true
]);
$errors = $this->validator->validate(
$credentials,
$dataConstraints
);
if (0 !== $errors->count()) {
$arrErrors = [];
foreach($errors as $error){
$arrErrors[] = $error->getMessage();
}
throw new CustomUserMessageAuthenticationException('Login failed.',$arrErrors);
}
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
So, do not use User entity validator for the login form, because entity Assert constraints are for create/update forms (register user/edit user). Then, in my controller, login route:
/**
* #Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
// if already loggedin, redirect to another page
// if ($this->getUser()) {
// return $this->redirectToRoute('target_path');
// }
// here we prepare the errors for the view, if any
// I made it two-dimensional array, you can do whatever
$retError = [];
// get the login error(s)
$errors = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
// prepare the Login form errors (including the Symfony authentication error; $errors includes any of them, from the exceptions in AppCustomAuthenticator), if any
if($errors){
$retError[] = [
'error_group' => $errors->getMessageKey(),
'error_items' => $errors->getMessageData()
];
}
return $this->render('site/login.html.php', [
'last_username' => $lastUsername,
'retError' => $retError
]);
}
In my login.html.php view (PHP templating):
<?php if ($retError) { ?>
<div class="alert alert-danger">
<ul>
<?php foreach($retError as $error){ ?>
<li>
<?=$error['error_group'];?>
<?php
if(is_array($error['error_items']) && count($error['error_items']) > 0){
?>
<ul>
<?php foreach($error['error_items'] as $error_item){ ?>
<li><?=$error_item;?></li>
<?php } ?>
</ul>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
Building Symfony 4.1 app. In my ProfileController ...
I have a booking_new method with form to create a new booking:
/**
* #Route("/profile/booking/new", name="profile_booking_new")
*/
public function booking_new(EntityManagerInterface $em, Request $request)
{
$form = $this->createForm(BookingFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var #var Booking $booking */
$booking = $form->getData();
$booking->setUser($this->getUser());
$em->persist($booking);
$em->flush();
return $this->redirectToRoute('profile_booking_show');
}
return $this->render('profile/bookings/booking_new.html.twig',[
'bookingForm' => $form->createView()
]);
}
Then I have a booking_show method to render single booking pages with route set to booking id:
/**
* #Route("/profile/booking/{id}", name="profile_booking_show")
*/
public function booking_show(BookingRepository $bookingRepo, $id)
{
/** #var Booking $booking */
$booking = $bookingRepo->findOneBy(['id' => $id]);
if (!$booking) {
throw $this->createNotFoundException(sprintf('There is no booking for id "%s"', $id));
}
return $this->render('profile/bookings/booking_show.html.twig', [
'booking' => $booking,
]);
}
After the booking is created I want to redirect user to the show booking view with the correct id.
Run server and get this error ...
ERROR: Some mandatory parameters are missing ("id") to generate a URL for route "profile_booking_show".
I understand the error but how do I fix it? How do I set the id of the booking just created without needing to query for the id?
Once the new entity is persited & flushed, you can use it like:
$em->persist($booking);
$em->flush();
return $this->redirectToRoute('profile_booking_show', ['id' => $bookig->getId()]);
As stated in the docs, you have to add an array of parameters as second argument
return $this->redirectToRoute('profile_booking_show', ['id'=>$id]);
On my Symfony 3 project I have made the following contoller:
/**
* #Route("/person_in_need/{id}",name="person_in_need_view")
*/
public function viewPersonInNeed(Request $request,$id)
{
}
And I want to redirect in it from another controller eg:
/**
* #Route("/person_in_need/add",name="add_person_in_need")
*/
public function addPersonInNeed(Request $request)
{
$form=$this->createForm(PersonInNeedType::class,new PersonInNeed());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* #var PersonInNeedBusinessLogicModel $dataInsert
*/
$dataInsert=$this->get('app.person_in_need_business_model');
$name=$form->get(PersonInNeedConstats::PERSON_IN_NEED_NAME)->getData();
$surname=$form->get(PersonInNeedConstats::PERSON_IN_NEED_SURNAME)->getData();
$reason=$form->get(PersonInNeedConstats::PERSON_IN_NEED_REASON)->getData();
$dataInsert->registerPersonInNeed($name,$surname,$reason);
//Pass id there
// return $this->redirectToRoute('person_in_need_view');
}
return $this->render('social_worker/add_person_in_need.html.twig',array(
'form'=>$form->createView()
));
}
How I will redirect and pass the id parameter using the route name?
Actualy you're almost done ;) TL;DR just use
return $this->redirectToRoute('person_in_need_view', ['id' => $yourEntity->getId()]);
I'm a bit curious why you get data via $form->get('<FIELD_NAME>)->normData() but there're posibly a reson for that...
I hope you've configured your PersonInNeedType for your PersonInNeed Entity. If so, just call
if ($form->isSubmitted() && $form->isValid()) {
/** #var PersonInNeed personInNeed **/
$personInNeed = $form->getData();
//at the end...after persist and flush so your entity is stored in DB and you have an ID
return $this->redirectToRoute('person_in_need_view', ['id' => $personInNeed->getId()]);
}
See Link:
This redirects using a 301 return code to the client:
$this->redirect(
$this->generateUrl(
'person_in_need_view',
array('id' => $id)
)
);
This redirects internally:
$this->forward(
'person_in_need_view',
array('id' => $id)
)
I'm building a web application with Symfony 3 and I need a REST API for mobiles applications. I use FosUserBundle and FosRestBundle.
Here's a simple controller:
/**
* #Route("/titles/add", name="_addTitle");
* #param Request $request
* #return Response
*/
public function addAction(Request $request)
{
$title = new Title();
$title->setUserId($this->getUser()->getId());
$form = $this->createForm(NewTitle::class, $title);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->get('title.manager')->saveTitle($title);
return $this->redirect('/contacts/add');
}
return $this->render('title/add.html.twig', array(
'form' => $form->createView()
));
}
/**
* #Post("/api/titles")
*/
public function postAction(Request $request)
{
$title = new Title();
$title->setUserId(1);
$form = $this->createForm(NewTitle::class, $title);
$form->submit($request->request->all());
if ($form->isValid()) {
$this->get('title.manager')->saveTitle($title);
return View::create($title, Codes::HTTP_CREATED);
}
return View::create($form, Codes::HTTP_BAD_REQUEST);
}
I have one action for the web application and one action for REST API. Business logic is in a "manager" service.
NewTitle Form Type :
class NewTitle extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TextType::class)
->add('save', SubmitType::class, array('label' => 'Add title'));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false
));
}
}
My goal is to keep the code clean and avoid duplication. I have some questions :
Do you see a better way ?
in the postAction, when I have form errors it displays the "submit" field in my JSON response. How can I use same form class but "submit" element only for HTML form ?
I want to add Notifications system in my symfony (2.8) project, i thought that Sonata Notification Bundle could help, but turns out that i do not know how to use it, i install it very well, but i do not know how to use it in my project.
i need some help about this bundle, some tutorial or so.
or
is there another way to use notification system, please tell me,
thank you in advance
That the controller that i want to use notification bundle
namespace LocationBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use LocationBundle\Entity\Agence;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Agence controller.
*
*/
class AgenceController extends Controller
{
/**
* Lists all Agence entities.
*
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$agences = $em->getRepository('LocationBundle:Agence')->findAll();
return $this->render('agence/index.html.twig', array(
'agences' => $agences,
));
}
/**
* Creates a new Agence entity.
*
*/
public function newAction(Request $request)
{
$agence = new Agence();
$form = $this->createForm('LocationBundle\Form\AgenceType', $agence);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($agence);
$em->flush();
return $this->redirectToRoute('agence_show', array('id' => $agence->getId()));
}
return $this->render('agence/new.html.twig', array(
'agence' => $agence,
'form' => $form->createView(),
));
}
/**
* Finds and displays a Agence entity.
*
*/
public function showAction(Agence $agence)
{
$deleteForm = $this->createDeleteForm($agence);
return $this->render('agence/show.html.twig', array(
'agence' => $agence,
'delete_form' => $deleteForm->createView(),
));
}
/**
* Displays a form to edit an existing Agence entity.
*
*/
public function editAction(Request $request, Agence $agence)
{
$deleteForm = $this->createDeleteForm($agence);
$editForm = $this->createForm('LocationBundle\Form\AgenceType', $agence);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($agence);
$em->flush();
return $this->redirectToRoute('agence_edit', array('id' => $agence->getId()));
}
return $this->render('agence/edit.html.twig', array(
'agence' => $agence,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
/**
* Deletes a Agence entity.
*
*/
public function deleteAction(Request $request, Agence $agence)
{
$form = $this->createDeleteForm($agence);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->remove($agence);
$em->flush();
}
return $this->redirectToRoute('agence_index');
}
/**
* Creates a form to delete a Agence entity.
*
* #param Agence $agence The Agence entity
*
* #return \Symfony\Component\Form\Form The form
*/
private function createDeleteForm(Agence $agence)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('agence_delete', array('id' => $agence->getId())))
->setMethod('DELETE')
->getForm()
;
}
I am pretty sure the Sonata Notification Bundle is not what you are searching. The Word "Notification" in the title is in your case a bit misleading. The Bundle is used to postpone actions/events using a queue system like RabbitMQ.
For what you are searching: Take a look at the Symfony's own "Flash Messages": http://symfony.com/doc/current/book/controller.html#flash-messages
It's very easy to implement and you don't need an additional bundle.