In a form event, setting a field 'attr' => array('readonly' => 'readonly') is rendered as "disabled" = "1". This is not the desired effect. A disabled select field persists a null value on submit. A readonly field should retain and persist the displayed value. Or so I thought. So how to get the value to remain unchanged and unchangeable?
Edit;
A hidden field does not do the trick. choice_attr does not help either.
I'm voting to close this question. I've not discovered any method for displaying a disabled entity field and also retain the value. If you've got any idea on how that's done...
An example (in Symfony 2.8.3):
The Household entity has six attributes, each of which is an entity in a OneToMany relationship to Household. (The application has other entities which have similar attributes.) The Housing entity/attribute of Household has two properties: housing and enabled. The application's client can set a property to enabled = no if they no longer intend to track that property.
If a property is set to enabled = no its availability in a new or edit Household form is readily eliminated by including a where clause in the entity field's query builder, e.g., ->where("h.enabled=1"). However, doing so causes the disabled property to be set to null. Thus the need for retaining the value somehow.
The ideal solution would be a service for these attribute entity fields that would both display values and retain if enabled is no.
I have tried using an event listener, a hidden field, choice_attr, modifying the form template and the form theme all to no avail. For example, a hidden field is text when an entity field is required. This doesn't mean it can't be done, only that I haven't stumbled on the proper method.
The eventual solution: a service using Doctrine metadata to get disabled entity fields, form class modifications, and, for ManyToMany relationships, some jquery and invisible template entry.
Service functions:
/**
* Create array of disabled fields of an entity object
*
* #param type $object
* #return array
*/
public function getDisabledOptions($object) {
$values = [];
$className = get_class($object);
$metaData = $this->em->getClassMetadata($className);
foreach ($metaData->associationMappings as $field => $mapping) {
if (8 > $mapping['type']) {
$fieldName = ucfirst($field);
$method = 'get' . $fieldName;
if (method_exists($object->$method(), 'getEnabled') && false === $object->$method()->getEnabled()) {
$values[] = $fieldName;
}
}
}
$manyToMany = json_decode($this->getMetaData($object), true);
foreach(array_keys($manyToMany) as $key) {
$values[] = $key;
}
return $values;
}
/**
* Get array of disabled ManyToMany options
*
* #param Object $object
* #return array
*/
public function getMetaData($object) {
$data = array();
$className = get_class($object);
$metaData = $this->em->getClassMetadata($className);
foreach ($metaData->associationMappings as $field => $mapping) {
if (8 === $mapping['type']) {
$data[$field] = $this->extractOptions($object, $field);
}
}
return json_encode($data);
}
Controller use of service:
$searches = $this->get('mana.searches');
$disabledOptions = $searches->getDisabledOptions($household);
$metadata = $searches->getMetadata($household);
...
$form = $this->createForm(HouseholdType::class, $household, $formOptions);
...
return $this->render('Household/edit.html.twig',
array(
'form' => $form->createView(),
....
'metadata' => $metadata,
));
Example of form class field:
->add('housing', EntityType::class,
array(
'class' => 'TruckeeProjectmanaBundle:Housing',
'choice_label' => 'housing',
'placeholder' => '',
'attr' => (in_array('Housing', $options['disabledOptions']) ? ['disabled' => 'disabled'] : []),
'label' => 'Housing: ',
'query_builder' => function (EntityRepository $er) use ($options) {
if (false === in_array('Housing', $options['disabledOptions'])) {
return $er->createQueryBuilder('alias')
->orderBy('alias.housing', 'ASC')
->where('alias.enabled=1');
} else {
return $er->createQueryBuilder('alias')
->orderBy('alias.housing', 'ASC');
}
},
))
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Truckee\ProjectmanaBundle\Entity\Household',
'required' => false,
'disabledOptions' => [],
));
}
Jquery to remove disabled attribute:
$("input[type=Submit]").click(function () {
$("input").removeAttr("disabled");
$("select").removeAttr("disabled");
});
Example of template & jquery for ManyToMany relationship:
<div id="household_options" style="display:none;">{{ metadata }}</div>
jquery:
if (0 < $("#household_options").length) {
var house_options = JSON.parse($("#household_options").text());
$.each(house_options, function (index, item) {
$.each(item, function (k, v) {
var formAttr = 'household_' + index + '_' + v.id;
$("#" + formAttr).attr('disabled', 'disabled');
});
});
}
Related
I have a symfony 4.2 form via AJAX with embedded sub-form that is used as a custom group.
The problem is that my imgLinks which is a part of the embedded form group_pictures does not return its error as a part of the group_picture bu as a lone error without any group.
The field is being validated and the error is returned as expected EXCEPT not assigned to the correct group.
In the symfony profiler Form tab, I can see the error related to the field but it does not display the error as part of the group like shown in the screenshot below
This is a very odd behaviour as all my other fields return errors in their respective groups.
In the profiler post parameter we can see that it is submitted with the correct group:
So here is the entity field:
/**
* #var string|null
*
* #ORM\Column(name="img_links", type="simple_array")
*
* #AcmeAssert\ImageLinks
*/
private $imgLinks;
Changing my custom Assert for another bundled with symfony results in the same behaviour so my custom constraint is not the culprit
The Form Type
$builder->add(
$builder->create('group_pictures', FormType::class, array('inherit_data' => true))
->add('imgLinks', CollectionType::class, array(
'label' => false,
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
))
);
My controller function to collect errors and return them to my AJAX
private function getErrorMessages($form)
{
$errors = array();
foreach ($form->getErrors() as $key => $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $child) {
if (!$child->isValid()) {
$errors[$child->getName()] = $this->getErrorMessages($child);
}
}
return $errors;
}
in the error collector above if I insert code to dump the group picture child like this:
if( $child->getName() == 'group_pictures'){
dump($child);
}
I see that there is no error assigned to it -errors: []
Then the errors returned to the browser (console screenshot) as you can see the error is assigned the key 0 instead of group_picture
getErrorMessages
Method first assign the error as ordered /index => value/ and then for every child form the error is assigned as unordered /key => value/
This part for me is a bit odd
this is what i'm using
public function
function getErrors(FormInterface $form)
{
$this->childErrors($form);
foreach ($form->all() as $key => $child) {
if ($child instanceof FormInterface) {
$this->childErrors($child, true);
}
}
return $this->errors;
}
And this is the childErrors method
private function childErrors(FormInterface $form, $recursive = false)
{
foreach ($form->getErrors() as $error) {
$message = $this->translator->trans($error->getMessage(), [], 'validators');
$this->errors[] = $message;
}
if ($form->count() && $recursive) {
foreach ($form->all() as $child) {
$this->childErrors($child, $recursive);
}
}
}
UPDATE:
yes, it is the error_bubbling options.
The collection type entries should be set error_bubbling to true /it is by default false and the collection type itself is set to true, so it will need to be disabled otherwise the error will bubble up to the main form.
I'm using the select2 plugin with ajax to have a dynamic field on my form, but when i submit the it return me an error "This value is not valid", which is normal cause i use the ChoiceType with an empty array() in the choices options on creation. According to this part of the symfony doc, the form event is my savior, so trying to use it but it look like something wrong with my code and can't really see what.
So My Question Is :
HOW to pass the choices possibility to the field, for the form to be valid.
My form Type
class ArticleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//My other field
//My functions to add the field with the possible choices
$formModifier = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$listImages = array();
}
//die(var_dump($listImages)); //Array of Image
$form->add('image', ChoiceType::class, array(
'attr' => array(
'id' => 'image'),
'expanded' => false,
'multiple' => false,
'choices' => $listImages));
};
$formModifierSubmit = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$form->get('image')->addError(new FormError(
'Veuillez choisir une image s.v.p.'
));
}
//die(var_dump($listImages)); //Array of Image object
$config = $form->get('image')->getConfig();
$opts = $config->getOptions();
$chcs = array('choices' => $listImages);
//die(var_dump($chcs)); //output an array with a 'choices' keys with array value
array_replace($opts, $chcs); //not work
//array_merge($opts, $chcs); //not work
//die(var_dump($opts)); //replacements/merge are not made
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be the entity Article
$data = $event->getData();
$formModifier($event->getForm(), $data->getImage());
}
);
//$builder->get('image')->addEventListener( //give error cause the field image don't exist
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifierSubmit) {
$imageVal = $event->getData();
//die(var_dump($imageVal)); //return all the submitted data field in an array
//But when change this event to Submit it return the Article model populated by the submitted data, EXCEPT the image field which have null as value
$formModifierSubmit($event->getForm(), $imageVal['image']);
}
);
}
public function getChoiceValue($imageValue, $options)
{
$listImages = $options['em']->getRepository('AlmotivAppBundle:Image')->findBy(array(
'id' => $imageValue
));
return $listImages; //array of Image object
}
[...]
}
For Info
My image field is not depending on any other field like the doc example, so i need to populate the choices options on PRE_SUBMIT event to give the possible choice.
And also image have a ManyToOne relation in my Article entity
class Article implements HighlightableModelInterface
{
//some properties
/**
* #ORM\ManyToOne(targetEntity="Image\Entity\Path", cascade={"persist"})
* #Assert\Valid()
*/
private $image;
}
If i'm in the bad way let me know cause i'm out of idea now, i try much thing, like
array_replace with the options in the configuration of the field but didn't wrong.
make an ajax request to the url of the form action url : $form.attr('action'), i think it will load the choices option with the possible of <option> but my select is still returned with none <option>.
and much more (can't remmenber).
And also i'm using the v3.1 of the framework with the v4.0.3 of the select2 plugin, if need more info just ask and thx for reading and trying help.
Edit
Just add some info to be more clear
You making things way too complicated. In your documentation example they add eventListener for already existing form field ('sport') and you are adding it to only later added field which does not exist (your 'image' field and 'position' field from the documentation example).
You should use EntityType and if you need (which I'm not if sure you are) filter your images using query_builder option, for validation add constraints (example with controller).
class ArticleType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder
// My other field
$imageFieldFunction = $this->getImageFieldFunction();
$builder->addEventListener(FormEvents::PRE_SET_DATA, $imageFieldFunction);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $imageFieldFunction);
}
private function getImageFieldFunction()
{
return function(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
//when your data_class is Article
$image = $data->getImage();//depending on your Article class
/*if you are using data_class => null
$image = $data['image'];
*/
$imageId = $image ? $image->getId() : 0;
$builder->add('image', EntityType::class , array(
'class' => 'AlmotivAppBundle:Image',
'attr' => array(
'id' => 'image'
) ,
'expanded' => false,
'multiple' => false,
'constraints' => new NotBlank(),
'query_builder' => function (EntityRepository $er) use ($imageId) {
return $er->createQueryBuilder('i')
->where('i.id = :image_id')
->setParameter('image_id', $imageId);
}
));
}
}
}
In Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
It works, and this is what profiler displays after form submit: http://i.imgur.com/54iXbZy.png
Actors' amount grown up and I wanted to load them with Ajax autocompleter on Select2. I changed form to ChoiceType:
->add('actors', ChoiceType::class, array(
'multiple' => true,
'attr' => array(
'class' => "select2-ajax",
'data-entity' => "actor",
),
))
//...
$builder->get('actors')
->addModelTransformer(new ActorToNumberModelTransformer($this->manager));
I made DataTransformer:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberModelTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
$actorsArray = $actors->toArray();
foreach($actorsArray as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return new ArrayCollection();
$actors = new ArrayCollection();
$actorIdArray = $actorIds->toArray();
foreach($actorIdArray as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors->add($actor);
}
return $actors;
}
}
And registered form:
common.form.type.movie:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
But seems like the reverseTransform() is never called. I even put die() at the beginning of it - nothing happened. This is, what profiler displays after form submit: http://i.imgur.com/qkjLLot.png
I tried to add also ViewTransformer (code here: pastebin -> 52LizvhF - I don't want to paste more and I can't post more than 2 links), with the same result, except that reverseTransform() is being called and returns what it should return.
I know that this is an old question, but I was having a very similar problem. It turned out that I had to explicitly set the compound option to false.
That is to say, for the third parameter to the add() method, you need to add 'compound => false'.
I try to create a customWidget with a special tablemethod to only display the pre selected choices of the user, this is the form :
$this->widgetSchema['Books_list'] = new MyWidgetFormThematicSelector(array(
'multiple' => true,
'model' => 'Books',
'table_method' => array('method' => 'getOnlySelected', 'parameters' => array($this->getObject()->getId())),
'expanded' => true,
));
this is the method getOnlySelected:
$q = Doctrine::getTable('BooksAuthors')
->createQuery('ba')
->select('ba.position,ba.name')
->leftJoin('ba.Books b')
->where('ba.BooksAuthors_id = ?', $id);
echo count($q); //return 4
return $q;
this method return 4 elements which is normal then if i try to echo the values of the getChoices method from the widget I get only 1 in return !?
class MyWidgetFormThematicSelector extends sfWidgetFormDoctrineChoiceWithParams {
public function configure($options = array(), $attributes = array())
{
parent::configure($options, $attributes);
}
public function getChoices() {
$choices = parent::getChoices();
echo count($choices); // return 1
return $choices;
}
public function render($name, $value = null, $attributes = array(), $errors = array()) {
return parent::render($name, $value, $attributes, $errors);
}
}
What's going on here ?
I create a similar widget in the same form where the probleme does not occurs, and it s quite the same code...
thx
I solve this problem by setting the attribute 'key_method' => 'myUniqueId', in the form where the widget is called...
Cause Ive got two primary keys in my table and the sfWidgetFormDoctrineChoiceWithParams widget use the one which was identic for all the results as the key for the array choices, so the size of the array was always one...By setting the other primary key as the main key of the getChoices method I get the correct result.
I have an array of options for a select list.
$options = array( 1=>'Option1', 2=>... );
But if I only have one option, I rather want either:
A hidden <input type="hidden" name="opt" value="2"/> with a validator requiring the posted value to be 2
No output. The value will only be stored in the form_element/form until requested by $form->getValues()
This code is a non-working example of what I want: ($this is a Zend_Form object)
$first_val = reset(array_keys($options));
if( count($options) > 1 )
$this->addElement('select', 'opt', array(
'multiOptions' => $options,
'label' => 'Options',
'value' => $first_val,
'required' => true ));
else
$this->addElement('hidden', 'opt', array(
'required' => true,
'value' => $first_val ));
However, the value will not validate to $first_val. Anyone may change the hidden value, allowing them to inject invalid values. This is not acceptable.
Help?
your code is missing a validator, e.g. Zend_Validate_Identical
I created a custom Zend_Form_Element that does exactly what I want. Maybe someone else might find it useful:
<?php
require_once 'Zend/Form/Element.php';
/**
* Class that will automatically validate against currently set value.
*/
class myApp_Element_Stored extends Zend_Form_Element
{
/**
* Use formHidden view helper by default
* #var string
*/
public $helper = 'formHidden';
/**
* Locks the current value for validation
*/
public function lockValue()
{
$this->addValidator('Identical', true, (string)$this->getValue());
return $this;
}
public function isValid($value, $context = null)
{
$this->lockValue();
return parent::isValid($value, $context);
}
}
?>