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);
}
}
?>
Related
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');
});
});
}
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 have a form which contains three objects:
$builder
->add('customer', new CustomerType())
->add('shippingAddress', new AddressType())
->add('billingAddress', new AddressType())
->add('sameAsShipping', 'checkbox', ['mapped' => false])
;
Each of the embedded forms has their own validation constraints and they work. In my main form, I have cascade_validation => true so that all of the embedded form validation constraints are applied. This also works.
I am having trouble 'disabling' the validation on the billingAddress form if the sameAsShipping checkbox is enabled. I can't make the validation in the AddressType form conditional because it always needs to be enforced for the shippingAddress form.
I've solved this same problem by using validation groups.
First, this is important: use the validation_groups option in your AddressType to set the validation groups of every constraint of each field in the type:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface;
class AddressType extends \Symfony\Component\Form\AbstractType
{
function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $options['validation_groups'];
$builder->add('firstName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
$builder->add('lastName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
}
}
Then, in the parent form pass different validation groups to the two fields:
<?php
$formBuilder = $this->get('form.factory')
->createNamedBuilder('checkout', 'form', null, [
'cascade_validation' => true,
])
->add('billingAddress', 'address', [
'validation_groups' => 'billingAddress'
])
->add('shippingAddress', 'address', [
'validation_groups' => 'shippingAddress'
]);
Then, determine determine your validation groups by looking at the value of the checkbox.
if ($request->request->get('sameAsShipping')) {
$checkoutValidationGroups = ['Default', 'billingAddress'];
} else {
$checkoutValidationGroups = ['Default', 'billingAddress', 'shippingAddress'];
}
You can then validate only either the billingAddress or the shippingAddress, or both using the validation group mechanism.
I chose to use a button:
$formBuilder->add('submitButton', 'submit', ['validation_groups' => $checkoutValidationGroups]);
Create a form model (I use it in nearly every form, but this code here is not tested):
/**
* #Assert\GroupSequenceProvider()
*/
class YourForm implements GroupSequenceProviderInterface {
/**
* #Assert\Valid()
*/
private $customer;
/**
* #Assert\Valid()
*/
private $shippingAddress;
/**
* #Assert\Valid(groups={'BillingAddressRequired'})
*/
private $billingAddress;
private $billingSameAsShipping;
public function getGroupSequence() {
$groups = ['YourForm'];
if(!$this->billingSameAsShipping) {
$groups[] = 'BillingAddressRequired';
}
return $groups;
}
}
Try to use meaningful names. sameAsShipping is hard to understand. Read the if-condition in getGroupSequence: if not billing (address) same as shipping (address) then billing address required.
That's all, clear code in my opinion.
I have a form with an input type='text' name='article[]' .
I don't know the number of article that can be post because there is a little javascript button where I can add as much I want input name=article[].
For now, I use Zend\InputFilter\InputFilter but the validators never get the value on the array in my $_POST.
My input :
<input name="article[]" class="form-control input-md" type="text" >
My InputFilter :
class ArticleFormFilter extends InputFilter{
public function __construct() {
$this->add(array(
'name' => 'article[]',
'required' => true,
'filters' => array(
array(
'name' => 'Zend\Filter\StripTags',
),
array(
'name' => 'Zend\Filter\StringTrim',
),
),
'validators' => array(
array(
'name' => 'NotEmpty',
),
),
));
}
}
If I do it with only one article, using article instead of article[] and no Javascript, it works of course.
To validate and/or filter arrays of POST data use CollectionInputFilter:
class MagazineInputFilter extends \Zend\InputFilter\InputFilter
{
public function __construct()
{
$this->add(new \Zend\InputFilter\Input('title'));
$this->add(new ArticlesCollectionInputFilter(), 'articles');
}
}
class ArticlesCollectionInputFilter extends \Zend\InputFilter\CollectionInputFilter
{
public function __construct()
{
// input filter used for each article validation.
// see source code of isValid() method of this class
$inputFilter = new \Zend\InputFilter\InputFilter();
/*
add inputs and its validation/filtration chains
*/
$this->setInputFilter($inputFilter);
}
}
Or setup input filter for collection inside main input filter of magazine:
class MagazineInputFilter extends \Zend\InputFilter\InputFilter
{
public function __construct()
{
$articles = new \Zend\InputFilter\CollectionInputFilter();
$articlesInputFilter = new \Zend\InputFilter\InputFilter();
/*
add inputs and its validation/filtration chains
*/
$articles->setInputFilter($articlesInputFilter);
$this->add(new \Zend\InputFilter\Input('title'));
$this->add($articles, 'articles');
}
}
first of all the field name shoud be "article" not "article[]".
When you change it you will find another problem:
Warning: Zend\Filter\StripTags::filter expects parameter to be scalar, "array" given; cannot filter
AFAIK the Zend 2 filters doesn't work with arrays... Some answers are here:
Zend Framework 2 filter / validate array of contents
As mentioned here I'm building a custom hydration strategy to handle my related objects in a select box in a form.
My form looks like this:
$builder = new AnnotationBuilder($entityManager);
$form = $builder->createForm(new MyEntity());
$form->add(new MyFieldSet());
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('my_attribute', new MyHydrationStrategy());
$form->setHydrator($hydrator);
$form->get('my_attribute')->setValueOptions(
$entityManager->getRepository('SecEntity\Entity\SecEntity')->fetchAllAsArray()
);
When I add a new MyEntity via the addAction everything works great.
I wrote fetchAllAsArray() to populate my selectbox. It lives within my SecEntityRepository:
public function fetchAllAsArray() {
$objects = $this->createQueryBuilder('s')
->add('select', 's.id, s.name')
->add('orderBy', 's.name ASC')
->getQuery()
->getResult();
$list = array();
foreach($objects as $obj) {
$list[$obj['id']] = $obj['name'];
}
return $list;
}
But in the edit-case the extract() function doesn't work. I'm not at the point where I see something of hydrate() so I'll leave it out for now.
My hydrator strategy looks like this:
class MyHydrationStrategy extends DefaultStrategy
{
public function extract($value) {
print_r($value);
$result = array();
foreach ($value as $instance) {
print_r($instance);
$result[] = $instance->getId();
}
return $result;
}
public function hydrate($value) {
...
}
The problem is as follows:
Fatal error: Call to a member function getId() on a non-object
The print_r($value) returns loads of stuff beginning with
DoctrineORMModule\Proxy__CG__\SecEntity\Entity\SecEntity Object
following with something about BasicEntityPersister and somewhere in the mess are my referenced entities.
The print_r($instance) prints nothing. It's just empty. Therefore I guess is the error message legit... but why can't I iterate over these objects?
Any ideas?
Edit:
Regarding to #Sam:
My attribute in the entity:
/**
* #ORM\ManyToOne(targetEntity="Path/To/Entity", inversedBy="whatever")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
* #Form\Attributes({"type":"hidden"})
*
*/
protected $attribute;
My new selectbox:
$form->add(array(
'name' => 'attribute',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'MyLabel',
'object_manager' => $entityManager,
'target_class' => 'Path/To/Entity',
'property' => 'name'
)
));
My final hope is that I'm doing something wrong within the controller. Neither my selectbox is preselected nor the value is saved...
...
$obj= $this->getEntityManager()->find('Path/To/Entity', $id);
$builder = new \MyEnity\MyFormBuilder();
$form = $builder->newForm($this->getEntityManager());
$form->setBindOnValidate(false);
$form->bind($obj);
$form->setData($obj->getArrayCopy());
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$form->bindValues();
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('entity');
}
}
I still haven't come around to write the tutorial for that :S
I don't know if this is working with the annotationbuilder though! As the DoctrineModule\Form\Element\ObjectSelect needs the EntityManager to work. The options for the ObjectSelect are as follows:
$this->add(array(
'name' => 'formElementName',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'formElementLabel',
'empty_option' => '--- choose formElementName ---',
'object_manager' => $this->getEntityManager(),
'target_class' => 'Mynamespace\Entity\Entityname',
'property' => 'nameOfEntityPropertyAsSelect'
)
));
In this case i make use of $this->getEntityManager(). I set up this dependency when calling the form from the ServiceManager. Personally i always do this from FactoryClasses. My FormFactory looks like this:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
$form = new ErgebnishaushaltProduktForm('ergebnisform', array(
'entity_manager' => $em
));
$classMethodsHydrator = new ClassMethodsHydrator(false);
// Wir fügen zwei Strategien, um benutzerdefinierte Logik während Extrakt auszuführen
$classMethodsHydrator->addStrategy('produktBereich', new Strategy\ProduktbereichStrategy())
->addStrategy('produktGruppe', new Strategy\ProduktgruppeStrategy());
$hydrator = new DoctrineEntity($em, $classMethodsHydrator);
$form->setHydrator($hydrator)
->setObject(new ErgebnishaushaltProdukt())
->setInputFilter(new ErgebnishaushaltProduktFilter())
->setAttribute('method', 'post');
return $form;
}
And this is where all the magic is happening. Magic, that is also relevant to your other Thread here on SO. First, i grab the EntityManager. Then i create my form, and inject the dependency for the EntityManager. I do this using my own Form, you may write and use a Setter-Function to inject the EntityManager.
Next i create a ClassMethodsHydrator and add two HydrationStrategies to it. Personally i need to apply those strategies for each ObjectSelect-Element. You may not have to do this on your side. Try to see if it is working without it first!
After that, i create the DoctrineEntity-Hydrator, inject the EntityManager as well as my custom ClassMethodsHydrator. This way the Strategies will be added easily.
The rest should be quite self-explanatory (despite the german classnames :D)
Why the need for strategies
Imo, this is something missing from the DoctrineEntity currently, but things are still in an early stage. And once DoctrineModule-Issue#106 will be live, things will change again, probably making it more comfortable.
A Strategy looks like this:
<?php
namespace Haushaltportal\Stdlib\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class ProduktbereichStrategy implements StrategyInterface
{
public function extract($value)
{
if (is_numeric($value) || $value === null) {
return $value;
}
return $value->getId();
}
public function hydrate($value)
{
return $value;
}
}
So whenever the $value is not numeric or null, meaning: it should be an Object, we will call the getId() function. Personally i think it's a good idea to give each Element it's own strategy, but if you are sure you won't be needing to change the strategy at a later point, you could create a global Strategy for several elements like DefaultGetIdStrategy or something.
All this is basically the good work of Michael Gallego aka Bakura! In case you drop by the IRC, just hug him once ;)
Edit An additional resource with a look into the future - updated hydrator-docs for a very likely, soon to be included, pull request