I've created a custom form validator that does not seem to be working. It gets called during form submission and returns false when it should fail validation. But it doesn't seem to tell the form that validation has failed.
Here is the form validator: (code box scrolls)
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RequiredIfPlainPasswordSetValidator extends ConstraintValidator
{
/**
* RequestStack instance.
*
* @var Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* dependency injection.
*
* @param Symfony\Component\HttpFoundation\RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* if the plain password has been set, the current password must not be
* empty.
*
* @param string $currentPassword
* @param Symfony\Component\Validator\Constraint $constraint
*
* @return bool
*/
public function validate($currentPassword, Constraint $constraint)
{
$plainPassword = $this->requestStack->getCurrentRequest()->request
->get('security_settings')['plainPassword']['first'];
if ($plainPassword && !$currentPassword) {
return false;
}
return true;
}
}
And here is the constraint that goes with it:
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class RequiredIfPlainPasswordSet extends Constraint
{
/**
* error message
*
* @var string
*/
protected $message = 'Please enter your current password.';
/**
* the alias for the related validator in services.yml
*
* @return string
*/
public function validatedBy()
{
return 'required_if_plain_password_set';
}
}
and the relevant part of app/config/services.yml:
services:
redacted.appbundle.required_if_plain_password_set:
class: Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSetValidator
arguments: ["@request_stack"]
tags:
- { name: validator.constraint_validator, alias: required_if_plain_password_set }
I've created a custom form type: (code box scrolls)
<?php
namespace Redacted\AppBundle\Form;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
class SecuritySettingsFormType extends AbstractType
{
/**
* the name of the form type
*
* @return string
*/
public function getName()
{
return 'security_settings';
}
/**
* add fields to form
*
* @param Symfony\Component\Form\FormBuilderInterface $formBuilder
* @param array $options
*/
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$formBuilder
->add('email')
->add('currentPassword', 'password')
->add('plainPassword', 'repeated', [
'type' => 'password',
'invalid_message' => 'Passwords must match.',
]);
}
/**
* configureOptions.
*
* @param Symfony\Component\OptionsResolver\OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$options = [
'data_class' => 'AppBundle\Entity\User',
'validation_groups' => ['security_settings'],
];
$resolver->setDefaults($options);
}
}
which validates against the User entity due to the data_type. Here is the relevant part of the User entity:
<?php
namespace Redacted\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSet;
class User implements UserInterface, \Serializable
{
// ...
/**
* @var string (default: null)
*
* @Assert\NotBlank(message="user.email_required", groups={"security_settings"})
* @Assert\Email(message="security_settings.valid_email" groups={"security_settings"})
* @ORM\Column(name="email", type="string", length=255, nullable=true, unique=true)
*/
private $email = null;
/**
* current password - used for validation only
*
* @RequiredIfPlainPasswordSet(
* message="security_settings.current_password_required",
* groups={"security_settings"}
* )
* @var string
*/
protected $currentPassword;
// ... setters and getters for above, etc.
}
And finally, here's the controller method I'm using to check. I'll leave out the view because it's probably irrelevant. (I'm defining my controller as a service so no redirectToRoute(), etc.)
// in the controller class...
/**
* Security settings page - email, password, etc.
*
* @Route("/security-settings", name="security_settings")
* @Template()
*
* @param Symfony\Component\HttpFoundation\Request $request
*
* @return array|Symfony\Component\HttpFoundation\RedirectResponse
*/
public function securitySettingsAction(Request $request)
{
$loggedInUser = $this->tokenStorage->getToken()->getUser();
$form = $this->formFactory
->create(new SecuritySettingsFormType(), $loggedInUser);
$form->handleRequest($request);
if ($form->isValid()) {
$user = $form->getData();
// persist the user
$this->saveUserSettings($user);
// set a success message
$this->setSecuritySettingsFlashSuccess($request);
// redirect back
$route = $this->router->generate('security_settings');
return new RedirectResponse($route);
}
return ['form' => $form->createView()];
}
The idea is that the current password should only be required if the new password is entered. Although that validate() function is getting called and returning false when it should, the form's isValid() is returning true and it's saving. If I add a @Assert\NotBlank(groups={"security_settings"}) assertion to the User::currentPassword field, it does fire and fail successfully, so it is looking for validation annotations on that field.
What am I missing?