How can I add a violation to a collection? - symfony-forms

My form looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$factory = $builder->getFormFactory();
$builder->add('name');
$builder->add('description');
$builder->add('manufacturers', null, array(
'required' => false
));
$builder->add('departments', 'collection', array(
'type' => new Department
));
}
I have a class validator on the entity the form represents which calls:
if (!$valid) {
$this->context->addViolationAtSubPath('departments', $constraint->message);
}
Which will only add a 'global' error to the form, not an error at the sub path. I assume this is because departments is a collection embedding another FormType.
If I changed departments to one of the other fields it works fine.
How can I get this error to appear in the right place? I assume it would work fine if my error was on a single entity within the collection, and thus rendered in the child form, but my criteria is that the violation occur if none of the entities in the collection are marked as active, thus it needs to be at the parent level.

By default, forms have the option "error_bubbling" set to true, which causes the behavior you just described. You can turn off this option for individual forms if you want them to keep their errors.
$builder->add('departments', 'collection', array(
'type' => new Department,
'error_bubbling' => false,
));

I have been wrestling with this issue in Symfony 3.3, where I wished to validate an entire collection, but pass the error to the appropriate collection element/field. The collection is added to the form thus:
$form->add('grades', CollectionType::class,
[
'label' => 'student.grades.label',
'allow_add' => true,
'allow_delete' => true,
'entry_type' => StudentGradeType::class,
'attr' => [
'class' => 'gradeList',
'help' => 'student.grades.help',
],
'entry_options' => [
'systemYear' => $form->getConfig()->getOption('systemYear'),
],
'constraints' => [
new Grades(),
],
]
);
The StudentGradeType is:
<?php
namespace Busybee\Management\GradeBundle\Form;
use Busybee\Core\CalendarBundle\Entity\Grade;
use Busybee\Core\SecurityBundle\Form\DataTransformer\EntityToStringTransformer;
use Busybee\Core\TemplateBundle\Type\SettingChoiceType;
use Busybee\Management\GradeBundle\Entity\StudentGrade;
use Busybee\People\StudentBundle\Entity\Student;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StudentGradeType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* StaffType constructor.
*
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('status', SettingChoiceType::class,
[
'setting_name' => 'student.enrolment.status',
'label' => 'grades.label.status',
'placeholder' => 'grades.placeholder.status',
'attr' => [
'help' => 'grades.help.status',
],
]
)
->add('student', HiddenType::class)
->add('grade', EntityType::class,
[
'class' => Grade::class,
'choice_label' => 'gradeYear',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('g')
->orderBy('g.year', 'DESC')
->addOrderBy('g.sequence', 'ASC');
},
'placeholder' => 'grades.placeholder.grade',
'label' => 'grades.label.grade',
'attr' => [
'help' => 'grades.help.grade',
],
]
);
$builder->get('student')->addModelTransformer(new EntityToStringTransformer($this->om, Student::class));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults(
[
'data_class' => StudentGrade::class,
'translation_domain' => 'BusybeeStudentBundle',
'systemYear' => null,
'error_bubbling' => true,
]
);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'grade_by_student';
}
}
and the validator looks like:
namespace Busybee\Management\GradeBundle\Validator\Constraints;
use Busybee\Core\CalendarBundle\Entity\Year;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class GradesValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (empty($value))
return;
$current = 0;
$year = [];
foreach ($value->toArray() as $q=>$grade)
{
if (empty($grade->getStudent()) || empty($grade->getGrade()))
{
$this->context->buildViolation('student.grades.empty')
->addViolation();
return $value;
}
if ($grade->getStatus() === 'Current')
{
$current++;
if ($current > 1)
{
$this->context->buildViolation('student.grades.current')
->atPath('['.strval($q).']') // could do a single atPath with a value of "[".strval($q)."].status"
->atPath('status') // full path = children['grades'].data[1].status
->addViolation();
return $value;
}
}
$gy = $grade->getGradeYear();
if (! is_null($gy))
{
$year[$gy] = empty($year[$gy]) ? 1 : $year[$gy] + 1 ;
if ($year[$gy] > 1)
{
$this->context->buildViolation('student.grades.year')
->atPath('['.strval($q).']')
->atPath('grade')
->addViolation();
return $value;
}
}
}
}
}
This results in the error being added to the field in the element of the collection as per the attach image.
Craig

I have a case very similar. I have a CollectionType with a Custom Form (with DataTransformers inside, etc...), i need check one by one the elements and mark what of them is wrong and print it on the view.
I make that solution at the ConstraintValidator (my custom validator):
The validator must target to CLASS_CONSTRAINT to work or the propertyPath doesnt work.
public function validate($value, Constraint $constraint) {
/** #var Form $form */
$form = $this->context->getRoot();
$studentsForm = $form->get("students"); //CollectionType's name in the root Type
$rootPath = $studentsForm->getPropertyPath()->getElement(0);
/** #var Form $studentForm */
foreach($studentsForm as $studentForm){
//Iterate over the items in the collection type
$studentPath = $studentForm->getPropertyPath()->getElement(0);
//Get the data typed on the item (in my case, it use an DataTransformer and i can get an User object from the child TextType)
/** #var User $user */
$user = $studentForm->getData();
//Validate your data
$email = $user->getEmail();
$user = $userRepository->findByEmailAndCentro($email, $centro);
if(!$user){
//If your data is wrong build the violation from the propertyPath getted from the item Type
$this->context->buildViolation($constraint->message)
->atPath($rootPath)
->atPath(sprintf("[%s]", $studentPath))
->atPath("email") //That last is the name property on the item Type
->addViolation();
}
}
}
Just i validate agains the form elements in the collection and build the violation using the propertyPath from the item in the collection that is wrong.

Related

Symfony form builder default select by EntityType

I try to create form with html select element using EntityType. I must get values by some condition and by this condition necessary default value not select from database. So i get all options values, without one, that must be a default value. So i try to find a way to put this value to select. What i tried...
Set value in form:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $growing, ['user_id' => $user_id]);
$tableForm->get('garden')->setData($garden);
$form = $tableForm->createView()
Then i tried to set data in entity:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$growing->setGarden($garden);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $growing, ['user_id' => $user_id]);
$form = $tableForm->createView()
Then i tried to set default select value in form_builder using 'data' attribute:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $grow, [
'user_id' => $user_id,
'selected_choice' => $garden
]);
$form = $tableForm->createView();
Form_builder code:
class GrowingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', HiddenType::class)
->add('garden', EntityType::class , [
'class' => 'FarmBundle\Entity\Garden',
'query_builder' => function (GardenRepository $gr) use ($options) {
return $gr->queryFreeGardens($options['user_id']);
},
'attr' => [
'data-type' => 'text',
'class' => 'table-select',
'disabled' => true
],
'required' => false,
'data' => $options['selected_choice']
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FarmBundle\Entity\Growing',
'selected_choice' => null,
'user_id' => null
));
}
}
And code of query for query builder:
class GardenRepository extends \Doctrine\ORM\EntityRepository
{
public function queryFreeGardens($user_id)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.growing', 'grow')
->where('grow.plantDate is NULL')
->orWhere('grow.endDate is not NULL')
->andWhere('g.user = :user_id')
->orderBy('g.name')
->setParameter('user_id', $user_id);
return $qb;
}
}
And all of this 3 methods not works. Result is one, if entity not get for query in query builder, i cant set this entity. If i will set entity as default value, that was in query builder all will works fine.
How can i solve this problem?
try this
in controller:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $grow, [
'user_id' => $user_id,
'additional_id' => 7
]);
$form = $tableForm->createView();
in form:
class GrowingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', HiddenType::class)
->add('garden', EntityType::class , [
'class' => 'FarmBundle\Entity\Garden',
'query_builder' => function (GardenRepository $gr) use ($options) {
return $gr->queryFreeGardens($options['user_id'], $options['additional_id');
},
'attr' => [
'data-type' => 'text',
'class' => 'table-select',
'disabled' => true
],
'required' => false
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FarmBundle\Entity\Growing',
'additional_id' => null,
'user_id' => null
));
}
}
in repository:
class GardenRepository extends \Doctrine\ORM\EntityRepository
{
public function queryFreeGardens($user_id, $additional_id)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.growing', 'grow')
->where('grow.plantDate is NULL')
->andWhere('g.id = :additional_id')
->orWhere('grow.endDate is not NULL')
->andWhere('g.user = :user_id')
->andWhere('g.id = :additional_id')
->orderBy('g.name')
->setParameter('user_id', $user_id)->setParameter('additional_id', $additional_id);
return $qb;
}
}
maybe you will need to adjust your repository method to retrieve values in right way. There are or clause, you should add this additional id to both branches of your or clause. The main idea is to retrieve you selected object too.

ZF2 + Doctrine2 Annotation Form Required & AllowEmpty

I use Annotation in a Doctrine Entity Class. Annotation for a field are :
/**
* #var integer
*
* #ORM\Column(name="duree", type="integer", nullable=true)
*
* #Form\Type("Zend\Form\Element\Number")
* #Form\Attributes({"required":false, "placeholder":"Durée", "min":"1", "max":"20"})
* #Form\Required(false)
* #Form\AllowEmpty()
* #Form\Options({"label":"Durée :"})
* #Form\Filter({"name": "Int"})
* #Form\Validator({"name":"IsInt"})
*/
private $duree;
So the DB column can be Empty (nullable), and in the form i wan't the same (ie user can leave input empty). I've both annotation Required(false) and allowEmpty, but the form never valid (always got isEmpty for this field).
If i set #Form\Type to "Text", it is working fine (form is valid event if input is empty). But with the class Number, it'is not the same.
I've the same pb with a Select element (correspondinf to a relationship). Annotations are :
/**
* #var \Application\Entity\CcCategorie
*
* #ORM\ManyToOne(targetEntity="Application\Entity\CcCategorie")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="categorie", referencedColumnName="id", nullable=true, onDelete="SET NULL")
* })
*
* #Form\Type("DoctrineModule\Form\Element\ObjectSelect")
* #Form\Attributes({"type":"select", "required":false})
* #Form\Options({"label":"Catégorie :"})
* #Form\Required(false)
* #Form\AllowEmpty()
* #Form\Options({
* "label":"Catégorie :",
* "empty_option": "---",
* "target_class": "Application\Entity\CcCategorie",
* "property": "label"
* })
*/
private $categorie;
But, with this the field has error (isEmpty) if the Select is set to empty option when validating Form.
The only workaround i've found is to set the annotation
* #Form\Type("\Application\Form\Entity\CcRepriseFieldset")
at the top of the entity class. The class CcRepriseFieldset extend Fieldset, and implement InputFilterProviderInterface. Then i specify the function in this class :
public function getInputFilterSpecification()
{
return array(
array(
"name" => "duree",
'required' => false,
'allow_empty' => true,
),
array(
"name" => "categorie",
'required' => false,
'allow_empty' => true,
),
);
}
With this it works... But it's not annotations.
I don't understand why annotation not work
thanks
Ok i found what is the probleme.
Annotation Builder made a array with all fields spec, like this :
array (size=7)
'name' => string 'reprise' (length=7)
'attributes' => array (size=0)
'elements' =>
array (size=8)
1 =>array (size=2)
...
'fieldsets' => array (size=0) empty
'type' => string '\Application\Form\Entity\CcRepriseFieldset'
(length=42)
'input_filter' =>
array (size=8)
'name' =>
array (size=4)
'name' => string 'name' (length=4)
'required' => boolean true
'filters' =>
array (size=3)
...
'validators' =>
array (size=2)
...
But this array is the Array for the FieldSet attached to the Entity.
So the Zend\Form\Factory never parse this because 'input_filter' are only parse for ZendForm Element not for fieldset (of course because Fieldset doesn't have SetInputFilter method)...
Ok i've a workaround but i'm not very statisfied of it .
First i've create a new FormFactory :
namespace Application\Form;
use Zend\Form\FieldsetInterface;
Class EntityFormFactory extends \Zend\Form\Factory {
public function configureFieldset(FieldsetInterface $fieldset, $spec)
{
$fieldset=parent::configureFieldset($fieldset, $spec);
$fieldset->input_filter_factory= $spec['input_filter'];
return $fieldset;
}
}
So this factory add the "input_filter" to a input_filter_factory variable of the fieldset.
Then in the fieldset class is :
class CcRepriseFieldset extends Fieldset implements \Zend\InputFilter\InputFilterProviderInterface
{
public function getInputFilterSpecification()
{
if (isset($this->input_filter_factory)){
return $this->input_filter_factory;
}
}
}
And finally, when i use annotationbuilder i change the formfactory :
$builder = new AnnotationBuilder($this->_em);
$builder->setFormFactory(new EntityFormFactory());
With this all is working fine... I'm not sur it's the best way to do it.

Declaring Symfony Form options (Sym 2.8/3.0)

I am looking for the best method for creating/adding dynamic options in a form. By options, I mean things like choice value pairs, or maybe even default values. I can see at least three options:
1) add the options to the $options array when adding the form type. For this, it appears that I must first declare a default value and then add them in the add method and in the controller:
controller:
$choices = [];
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$options = ['pages' => $choices];
$form = $this->createForm('MyBundle\Form\Type\PageType', $data, $options);
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'pages' => []
]);
}
}
2) If the values are not dependent on controller values, it seems I could create them in the OptionsResolver (assuming access to the source data)
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$resolver->setDefaults([
'pages' => $choices
]);
}
3) Finally, I can also add in the buildForm method (again assuming access to source data):
FormType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $choices,
'label' => __('Page')
]);
}
Obviously, there is the most flexibility in the first option, but If I do not require that flexibility, or do not want to manage the options in the controller for some reason, does it make more sense to do the work in the buildForm or configureOptions methods?
If you require flexibility you can't use solution 3. But if you want to avoid flexibility, solution 3 is the best.
Solution 1 and 2 are OK, it really depend of what you need :
If you use your form in several actions with different choices: use solution 1, but add a requirement on this option to prevent the form to be called without choices
If your choices are often the same, but you want to override them only sometimes: chose solution 2
Personally I prefer the solution 1, because it's always better if your form relies on the less possible external objects ($this->pages in your example).
Regards
If you work with Doctrine Entities, you should use this:
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// ...
$builder->add('pid', EntityType::class, array(
'class' => 'AppBundle:Page',
'choice_label' => 'title',
));
For working with another type of objects this one:
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use AppBundle\Entity\Page;
// ...
$builder->add('pid', ChoiceType::class, [
'choices' => [
new Page('Page 1'),
new Page('Page 2'),
new Page('Page 3'),
new Page('Page 4'),
],
'choices_as_values' => true,
'choice_label' => function($page, $key, $index) {
/** #var Page $page */
return $page->getTitle();
}
]);
More information you can read in blog post here.

event that adds a parameter to routing

Is it possible to hook up (ideally in the controller) to add an additional parameter to routing?
I know that sounds unclear and at first glance it may sounds ridiculous - because to reach the controller we already must have routing. But I want to change only default variables.
I'll try to explain what I want to achieve:
Config:
return [
'router' => [
'routes' => [
'some' => [
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => [
'route' => '/some/:project',
'defaults' => [
'__NAMESPACE__' => 'Some\Controller',
'controller' => 'Some\Controller\Some',
'action' => 'some',
'extra' => 'default-value'
],
],
]
]
]
];
Controller:
class SomeController extends AbstractActionController {
protected $project = null;
public function setEventManager(EventManagerInterface $events)
{
parent::setEventManager($events);
$controller = $this;
$events->attach(
'dispatch', function (\Zend\Mvc\MvcEvent $e) use ($controller) {
$params = $e->getRouteMatch()->getParams();
$this->project = $params['project'] ;
// and there should be something that I want to
// achieve but do not know how (and if it is possible)
if ($this->project == 1) {
// magic action which modify config default param
// "extra" from "default-value" to "changed-value"
}
return;
}, 50
);
}
protected function attachDefaultListeners()
{
parent::attachDefaultListeners();
$eventManager = $this->getEventManager();
$eventManager->attach(
\Zend\Mvc\MvcEvent::EVENT_DISPATCH,
function(\Zend\Mvc\MvcEvent $event) {
$ViewModel = $event->getResult();
if ($ViewModel instanceof \Zend\View\Model\ViewModel) {
$ViewModel->setVariable('project',$this->project);
}
},
-99);
}
public function someAction() {
echo $this->params()->fromRoute("extra"); // return "default-value";
// but i want
echo $this->params()->fromRoute("extra"); // return "changed-value";
return new ViewModel();
}
}
View
<?php
echo "project: ".$this->project;
echo $this->url('some',['project'=>1]); // result: "/some/1"
I know this seems very strange. But for some reason (readable links, seo) is necessary to me.
Are you sure, you want to change the default param?
if ($this->project == 1) {
$e->getRouteMatch()->setParam('extra', 'changed-value');
}
You can set default params globally for assembling:
$serviceLocator->get('router')->setDefaultParam('extra', 'changed-value');
There is no way to change the defaults-Property of Zend\Mvc\Router\Http\Segment
If you really need it you must extend this class (but I would not recommend that, because I think your approach is already wrong)

How to add a class to all labels in a ZF2 form

I'm using a jQuery plugin that takes the text from labels associated with form elements and puts them as default text for the fields themselves. (You can find the plugin here.)
Here's the catch: it can only do this if the label has the class "inline". Now, I know I can use the following code to do this:
$this->add(array (
'name' -> 'name',
....
'options' => array (
'label' => 'Name',
'label_attributes' => array (
'class' => 'inline'
)
)
));
This will work fine, and if it has to be done item by item, then so be it. But I was wondering if there's some way I can add the class to ALL labels associated with text and text area form elements without using JavaScript. I'm thinking this would either done by a plugin, or by looping through all the elements in the form, but I don't know how to do either.
You could extend the FormRow view helper.
Here is a little example:
use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\View\Helper\FormRow;
class CustomFormRow extends FormRow
{
public function render(ElementInterface $element) {
...
$label = $element->getLabel();
if (isset($label) && '' !== $label) {
// Translate the label
if (null !== ($translator = $this->getTranslator())) {
$label = $translator->translate(
$label, $this->getTranslatorTextDomain()
);
}
$label->setAttribute('class', 'inline');
}
...
if ($this->partial) {
$vars = array(
'element' => $element,
'label' => $label,
'labelAttributes' => $this->labelAttributes,
'labelPosition' => $this->labelPosition,
'renderErrors' => $this->renderErrors,
);
return $this->view->render($this->partial, $vars);
}
...
}
You could probably leave the rest as it is and you should be good to go once you add some configuration in your Module.php for your view helper.
public function getViewHelperConfig() {
return array(
'factories' => array(
'CustomFormRow' => function($sm) {
return new \Application\View\Helper\CustomFormRow;
},
)
);
}
In your template files you now have to use your viewHelper instead.
<?php echo $this->CustomFormRow($form->get('yourelement')); ?>

Resources