TL;DR: Required attribute not set according to set validation rules.
validation_groups are a greate way to define what in a form should be validated (and how). I have this working as expected for the classic "Register" and "Update Profile" Form.
What i dont get to work is a small UI glitch. On all "required" fields, this fields should be marked with an ***** .
According to the Documentation :
The required option can be guessed based on the validation rules (i.e. is the field NotBlank or NotNull) or the Doctrine metadata (i.e. is the field nullable). This is very useful, as your client-side validation will automatically match your validation rules.
This seems not to work, i can of course overrite the required and if i set it to false
it is, as expected, not displayed.
But if i use my validate_group for profile_update the password fields are not in the validation_group - and if empty dont get marked as failed element. But the required
attribute is still set.
So to come to the question - how can the required
flag be based on the @Assert
annotation of the entity?
As you can see on the image, the password fields are marked as "required" but, as intended, are not validated. Again, this is not a validation problem, its just a UI problem with the required attribute.
Dont think it will help much, but here are the relevant (shortend) code parts:
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',
));
}
[...]
}
views\form.html.twig
[...]
{{ form(form, {'style': 'horizontal', 'col_size': 'xs', 'align_with_widget': true, 'attr': {'novalidate': 'novalidate'}}) }}
[...]
So, after not finding out why this is not working, i decided to code the functionality myself.
To make this work a big help was found in this Article and the source of the JsFormValidatorBundle
What i do is: Using an FormType Extension which calls a Service Class to get the constraints of the entity. Once i know which field elements should be required on which are not, i modify the view and set the required variable accordingly.
WARNING This code is not extensible tested, 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();
}
}
}
An Up2Date version can be found on https://gitlab.cwd.at/symfony/cwdgenericbundle/tree/master/Form