"required" based on the assertion from validation_groups on forms
TL; DR: Required attribute not set according to established validation rules.
validation_groups is a friendly way to define what should be validated on a form (and how). This works for me as expected for the classic "Register" and "Update Profile" forms.
What I am not getting is a small glitch in the UI. In all "required" fields, these fields must be marked with an asterisk *****.
According to Documentation :
The required parameter can be guessed based on validation rules (i.e. it's NotBlank or NotNull field) or Doctrine metadata (i.e. nullable field). This is very useful as your client-side validation will automatically match your validation rules.
It doesn't seem to work, I can of course override the required one, and if I set it to false
, it won't show up as expected.
But if I use my validate_group for profile_update, the password fields are not in the validation_group file, and if empty it will not be flagged as a failed item. But the attribute required
is still set.
So to get to the question: how is the flag required
based on the @Assert
object annotation ?
As you can see in the image, the password fields are marked "required" but are not validated as expected. Again, this is not a validation issue, it is just a UI issue with the required attribute.
Don't think it helps, but here are the relevant (short) parts of the code:
Entity \ User:
class User implements UserInterface
{
use Timestampable;
use Blameable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true, length=200, nullable=false)
* @Assert\NotBlank(groups={"default"})
* @Assert\Email(groups={"default"})
* @Assert\Length(max = "200", groups={"default"})
*/
private $email;
/**
* @ORM\Column(type="string", length=64, nullable=false)
* @Assert\NotBlank(groups={"create"})
* @RollerworksPassword\PasswordStrength(minLength=6, minStrength=2)
*/
private $password;
[....]
}
Forms \ UserType:
class UserType extends AbstractType
{
[...]
/**
* @param FormBuilderInterface $builder
* @param array $options
*
* @return misc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstname', 'text', array('label' => 'Firstname'))
->add('lastname', 'text', array('label' => 'Lastname'))
->add('email', 'email', array('label' => 'EMail'))
->add('password', 'repeated', [
'type' => 'password',
'label' => 'Password',
'invalid_message' => 'Password fields must match',
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Repeat Password']
]
);
[...]
$builder
->add('save', 'submit', array('label' => 'Save'));
}
/**
* @param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function(FormInterface $form) {
$data = $form->getData();
if ($data->getId() == null) {
return array('default', 'create');
}
return array('default');
},
'data_class' => 'Dpanel\Model\Entity\User',
));
}
[...]
}
view \ form.html.twig
[...]
{{ form(form, {'style': 'horizontal', 'col_size': 'xs', 'align_with_widget': true, 'attr': {'novalidate': 'novalidate'}}) }}
[...]
source to share
So, without figuring out why this doesn't work, I decided to code the functionality myself.
To make this work a lot of help was found in this article and the source of the JsFormValidatorBundle
What I am doing: Using a FormType extension that calls the service class to get the constraints on the object. Once I know which field elements should be required and which shouldn't, I change the view and set the required variable accordingly.
Warning This code is not extensible and may not work in your configuration!
Form \ Extension \ AutoRequireExtension.php:
<?php
namespace Cwd\GenericBundle\Form\Extension;
use Cwd\GenericBundle\Form\Subscriber\AutoRequire as AutoRequireSubscriber;
use Cwd\GenericBundle\Form\Service\AutoRequire as AutoRequireService;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
/**
* Class AutoRequireExtension
*
* @package Cwd\GenericBundle\Form\Extension
* @DI\Service("cwd.generic.form.extension.autorequire")
* @DI\Tag("form.type_extension", attributes={ "alias"="form" })
*/
class AutoRequireExtension extends AbstractTypeExtension
{
/**
* @var AutoRequireService
*/
protected $service;
/**
* @var bool
*/
protected $enabled;
/**
* @param AutoRequireService $service
* @param bool $enabled
*
* @DI\InjectParams({
* "service" = @DI\Inject("cwd.generic.form.service.autorequire"),
* "enabled" = @DI\Inject("%cwd.genericbundle.form.extension.autorequire.enabled%")
* })
*/
public function __construct(AutoRequireService $service, $enabled = false)
{
$this->service = $service;
$this->enabled = $enabled;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($this->enabled) {
$builder->addEventSubscriber(new AutoRequireSubscriber($this->service));
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($this->enabled) {
if (isset($this->service->fields[$view->vars['name']])) {
$view->vars['required'] = $this->service->fields[$view->vars['name']];
}
// Password Repeat Fallback
if ($view->vars['name'] == 'first' || $view->vars['name'] == 'second') {
$view->vars['required'] = $this->service->fields['password'];
}
}
}
/**
* Returns the name of the type being extended.
*
* @return string The name of the type being extended
*/
public function getExtendedType()
{
return 'form';
}
}
Form \ Subscriber \ AutoRequire.php:
<?php
namespace Cwd\GenericBundle\Form\Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Cwd\GenericBundle\Form\Service\AutoRequire as AutoRequireService;
/**
* Class AutoRequire
*
* @package Cwd\GenericBundle\Form\Subscriber
*/
class AutoRequire implements EventSubscriberInterface
{
protected $service = null;
/**
* @param AutoRequireService $service
*/
public function __construct(AutoRequireService $service)
{
$this->service = $service;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SUBMIT => array('onFormSetData', -10));
}
/**
* @param FormEvent $event
*/
public function onFormSetData(FormEvent $event)
{
/** @var Form $form */
$form = $event->getForm();
$this->service->process($this->getParent($form));
}
/**
* @param Form|FormInterface $element
*
* @return \Symfony\Component\Form\Form
*/
protected function getParent($element)
{
if (!$element->getParent()) {
return $element;
} else {
return $this->getParent($element->getParent());
}
}
}
Form \ Service \ AutoRequire.php:
namespace Cwd\GenericBundle\Form\Service;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\Form;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Class AutoRequire
*
* @DI\Service("cwd.generic.form.service.autorequire")
*/
class AutoRequire
{
/**
* @var ValidatorInterface
*/
protected $validator;
public $fields = array();
protected $groups = null;
/**
* @param ValidatorInterface $validator
*
* @DI\InjectParams({
* "validator" = @DI\Inject("validator")
* })
*/
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
/**
* Add a new form to processing queue
*
* @param \Symfony\Component\Form\Form $form
*
* @return array
*/
public function process(Form $form)
{
// no need to run for every field
if ($this->groups === null) {
$this->groups = $this->getValidationGroups($form);
}
// no need to run for every field
if (count($this->fields) == 0) {
$this->fields = $this->getValidations($form, $this->groups);
}
}
/**
* Get validation groups for the specified form
*
* @param Form|FormInterface $form
*
* @return array|string
*/
protected function getValidationGroups(Form $form)
{
$result = array('Default');
$groups = $form->getConfig()->getOption('validation_groups');
if (empty($groups)) {
// Try to get groups from a parent
if ($form->getParent()) {
$result = $this->getValidationGroups($form->getParent());
}
} elseif (is_array($groups)) {
// If groups is an array - return groups as is
$result = $groups;
} elseif ($groups instanceof \Closure) {
$result = call_user_func($groups, $form);
}
return $result;
}
private function getValidations(Form $form, $groups)
{
$fields = array();
$parent = $form->getParent();
if ($parent && null !== $parent->getConfig()->getDataClass()) {
$fields += $this->getConstraints($parent->getConfig()->getDataClass(), $groups);
}
if (null !== $form->getConfig()->getDataClass()) {
$fields += $this->getConstraints($form->getConfig()->getDataClass(), $groups);
}
return $fields;
}
protected function getConstraints($obj, $groups)
{
$metadata = $this->validator->getMetadataFor($obj);
$fields = array();
foreach ($metadata->members as $elementName => $d) {
$fields[$elementName] = false;
$data = $d[0];
foreach ($data->constraintsByGroup as $group => $constraints) {
if (in_array($group, $groups) && count($constraints) > 0) {
$fields[$elementName] = true;
break;
}
}
}
return $fields;
}
/**
* Gets metadata from system using the entity class name
*
* @param string $className
*
* @return ClassMetadata
* @codeCoverageIgnore
*/
protected function getMetadataFor($className)
{
return $this->validator->getMetadataFactory()->getMetadataFor($className);
}
/**
* Generate an Id for the element by merging the current element name
* with all the parents names
*
* @param Form $form
*
* @return string
*/
protected function getElementId(Form $form)
{
/** @var Form $parent */
$parent = $form->getParent();
if (null !== $parent) {
return $this->getElementId($parent) . '_' . $form->getName();
} else {
return $form->getName();
}
}
}
The Up2Date version can be found at https://gitlab.cwd.at/symfony/cwdgenericbundle/tree/master/Form
source to share