I have a form in Symfony 2.7.10 which definition looks like this:
<?php
// ...
class RecipeType extends AbstractType
{
// ...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('meal_schema', 'choice', [
'label' => 'Mealtime schema',
'choices' => [
'breakfast' => 'Breakfast',
'second_breakfast' => 'Second breakfast',
'lunch' => 'Lunch',
'dinner' => 'Dinner',
'snack' => 'Snack',
'supper' => 'Supper',
],
'multiple' => true,
'expanded' => true,
'label_attr' => ['class' => 'col-md-2'],
'required' => true,
])
}
// ...
}
This is how validation looks in validation.yml file:
My\Real\Namespace\Entity\Recipe:
properties:
name:
- NotBlank: ~
mealSchema:
# Look below
Name field validation works, but mealSchema validation doesn't.
Settings which I've already tried without success:
# This one works but assigns error to form instead of field so it's displayed on top of the page
- NotBlank:
message: Mealtime schema should not be blank
# This doesn't work
- Choice:
callback: [RecipeMealSchemaChoices, getChoiceKeys] # This method isn't even called
min: 1
minMessage: "Please select meal schema"
# This also doesn't work
- Count:
min: 1
max: 99
minMessage: Mealtime schema should not be blank
maxMessage: Max meal time exceeded
Ok, I solved it.
My mistake was to not provide information about mealSchema field in my entity when asking on stackoverflow. If I'd do that then I would realise that the entity field is in fact a smallint.
My colleague wanted to emulate a MySQL "SET" field type in Doctrine so he used an array as entrypoint values and converted it to bit values in setter method (and the other way around in the getter method).
That's why none of the choice-related validation rules worked. For now, I've made 2 quick solutions:
Change all fields' names in RecipeType from underscore to camelCase because that is how they are named in our entity. This helped for the error being attached to the form instead of the field.
Used a NotNull validator because the default value of mealSchema was null, not an empty array.
Related
My goal is to create chapter of a book and in each chapter, I can have as many chapters as I want as a subchapter. I have an entity which is a Chapter of a book
for each chapter, I can add a subchapter (which is in fact an other chapter ( but with id parent not null))
class Chapter
{
/**
* One champ has One parent champ.
* #ORM\ManyToOne(targetEntity="Chapter", inversedBy="children")
* #ORM\JoinColumn(name="id_parent", referencedColumnName="id")
*/
public $parent;
/**
* One champ has Many champs.
* #ORM\OneToMany(targetEntity="Chapter", mappedBy="parent",cascade={"persist"}))
* #ORM\OrderBy({"ordre" = "ASC"})
*/
public $children;
and I'd like to add/delete/modify with symfony form each chapter, so I have the following formtype
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('libelleLong', textType::class, ['label'=> 'titre']);
$builder->add('children', CollectionType::class,[
'entry_type' => ChapterType::class,
'entry_options' => ['label' => false,'block_name' => 'childrenList'],
'block_name' => 'childrenList',
'label'=> false,
'allow_add'=>true,
'allow_delete'=> true,
'prototype'=> true,
'by_reference' => false,
]
);
}
but this line 'prototype'=> true put everything down...
[19-May-2020 20:57:08 UTC] PHP Fatal error: Allowed memory size of
134217728 bytes exhausted (tried to allocate 32768 bytes) in
vendor\symfony\symfony\src\Symfony\Component\Debug\Exception\OutOfMemoryException.php
on line 1
How can I allow prototype to the children?
I believe, the problem ultimately is recursion. since the child form will also have a children field, where all subforms will also have children fields ... ad infinitum. Hence it's running out of memory just to create this. (This problem only occured by adding prototype because it then has to render one future child, which also has prototype on, and has to do the same - please note, that the form component is not optimally equipped to handle recursion. maybe you can find a different way to technically and visually handle this, for example a list of (pos,string,depth) tuples, that optically improved by form rendering / js, with transformation etc.)
So you have to limit the depth this recursion can go. A generic approach (and tbh I'm not certain this will work) would be to add an option max_depth, which essentially tells the form builder if and how many more levels this can go.
For that, your form type class should include a method configureOptions:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['max_depth']);
$resolver->setDefaults([
'data_class' => Chapter::class, // you probably have this line!
'max_depth' => 3, // default value
]);
}
this will add the configuration option to your form type, now we have to use it in your buildForm method:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('libelleLong', textType::class, ['label'=> 'titre']);
// only add children sub form, if max_depth > 0, to stop infinite recursion
if($options['max_depth'] > 0) {
$builder->add('children', CollectionType::class,[
'entry_type' => ChapterType::class,
'entry_options' => [
'label' => false,
'block_name' => 'childrenList',
// IMPORTANT!!! reduce max_depth by 1
'max_depth' => $options['max_depth'] - 1,
],
'block_name' => 'childrenList',
'label'=> false,
'allow_add' => true,
'allow_delete'=> true,
'prototype'=> true,
'by_reference' => false,
]);
} // end if for max_depth
}
I would advise to keep max_depth small, like ... 2 or 3.
An alternative would be, to create another form type called SubChapterType and maybe SubSubChapterType, that essentially are the same as ChapterType, except their children's entry_type is the next form type, where the last form type has no children field.
I have an older project which still uses Symfony 2. In it there is a form for editing a client profile. In the controller we have this:
$form = $this->createForm(new ClientProfile($remindTimes), $client);
$form->handleRequest($request);
And in the ClientProfile class we have
class ClientProfile extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('client_name', 'text', array('label' => 'Full name'))
->add('client_address', 'text', array('label' => 'Address', 'required' => false))
->add('client_city', 'text', array('label' => 'City', 'required' => false))
->add('client_post_code', 'text', array('label' => 'Postal index', 'required' => false))
->add('client_email', 'email', array('label' => 'E-mail', 'required' => false));
}
}
... and some other fields, but you get the gist. Then there's also a Twig view which renders the HTML. Standard stuff, as far as I can tell.
Now for my requirement. The client object has two special properties. Let's call them FroobleEnabled and FroobleType. If Frooble is disabled, then the type value has no meaning (can be set to 0). In the UI I want a dropdown with the values:
Disabled
Type 1
Type 2
Type 3
If the user selects Disabled, then FroobleEnabled gets set to false and FroobleType gets set to 0. Otherwise FroobleEnabled gets set to true and FroobleType to 1, 2 or 3 respectively.
How do I do this? The thing which makes this special is that it's not a 1:1 mapping anymore. There are two fields in the model object but just one UI control. I think I could achieve this with the a DataMapper, but I also don't want to manually map all the other fields (though I can, if there's no other option). I also cannot find any decent documentation about DataMapper or any other Symfony Forms features that could help me.
One way to accomplish this is to create a field, let's say, frooble.
In the form class create a frooble Choice field with mapped => false and values 0, 1, 2, 3. Set its choices to strings appropriate to the application.
In the controller, after form submission & validation, include code something like:
...
$frooble = $form->get('frooble')->getData();
if (0 === $frooble) {
$client->setFroobleEnabled(false);
$client->setFroobleType(0);
} else {
$client->setFroobleEnabled(true);
$client->setFroobleType($frooble);
}
I have two fields in my entity contractLength and contractEndDate. The aim is to be able to specify EITHER a contractLength OR a contractEndDate.
To accomplish this I have added an unmapped field to the form useBespokeEndDate, of checkbox type.
I want to be able in the form class ContractType to listen for the submit event and then check if the checkbox is ticked, if it is then proceed as normal but if it is not ticked (so we are using contractLength) then set the contractEndDate field to null.
I believe I should be using Event Listeners as detailed here http://symfony.com/doc/master/form/dynamic_form_modification.html#customizing-your-form-based-on-the-underlying-data (under the Dynamic Generation for Submitted Forms heading) to listen for the POST_SUBMIT event and then check the value of useBespokeEndDate and then modify the data as appropriate. However, I'm confused as to how exactly to get the form data for the unmapped field and then to also modify the actual data of the form.
My buildForm method in ContractType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('useBespokeEndDate', CheckboxType::class, array(
'label' => 'Use Bespoke Contract End Date?',
'empty_data' => false,
'mapped' => false
))
->add('contractLength', ChoiceType::class, array(
'choices' => array(12 => 12, 24 => 24, 36 => 36, 48 => 48, 60 => 60),
'label' => 'Contract Length (months)'
))
->add('contractEndDate', DateType::class, array('label' => 'Contract End Date'))
->add('save', SubmitType::class, array('label' => 'Save'));
$builder->addEventListener(FormEvents::POST_SUBMIT,
// Do something here to modify the data
);
}
Had the same problem, and I could access the unmapped values this way :
// in your POST_SUBMIT callback :
$form = $event->getForm();
$contract = $event->getData();
// get the unmapped value :
$useBespokeEndDate = $form->get('useBespokeEndDate')->getData();
// .. continue
I've got a similar situation where I have an option field, it's use (and therefore value) depending on a radio button group.
Rather than making a dynamic form, I instead separated the form from the entity, and actually ended up created a sub-form (Type) that had its own data_class Form configuration option and Data Mappers to convert the value objects to the entities.
There are useful blog posts with more information about Value Objects in forms and Creating a Custom Data Mapper for Symfony Forms.
For me, this alos gives the advantage of moving some quite complex checks into their own smaller class, and making the code more reusable.
I have a ChoiceType::Type field that display some choices, and i want to add, for each choices, an input to add a price on it. I did it like this :
->add('product_price', ChoiceType::class, array(
'choices' => array(
"Product 1",
"Product 2",
),
)
The JS that add the input for each choices :
var productBoxes = $("[id^=product_]");
// Listen the checkbox to display or hide the prices inputs
productBoxes.each(function (index) {
var priceField = '<label class="control-label required" for="product_price_' + index + '">Capacité</label>' +
'<input type="text" id="product_price_' + index + '" name="product[price][]" class="form-control">';
$(this).click(function () {
if ($(this).is(':checked')) {
$(this).parent().append(priceField);
}
})
})
The javascript works, it append fields next to each choices. Now I want to send my data in an array, like this :
["Product 1" => "value of the attached field"]
But I don't know how to fetch that extra data and save it to the database.
Did someone knows how to do it ?
EDIT 1
I tried to do it with CollectionType, but don't find out how to render each CollectionType element as checkbox. Is there a way to do it this way ?
Thanks for your help !
I think better way is to create custom type and set additional field there.
For example:
You main form type:
$builder->add('product_price', CollectionType::class, array(
'label' => 'Prices',
'entry_type' => ProductPriceType::class
));
And ProductPriceType:
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', TextType::class, array())
->add('price', NumericType::class, array());
}
/**
* #param OptionsResolver #resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\SomeEntity'
));
}
I think you get product data from base.
In this case you get array that contains 2 values - product and price
In your example, the easiest way would be to add two more fields to your form. Keep them hidden with CSS (display: none), and just show them with JS (toggle class "hidden" when the choice gets un/selected)
->add('product_one_price', NumberType::class, array(
'attr' => array('class' => 'hidden')
))
->add('product_two_price', NumberType::class, array(
'attr' => array('class' => 'hidden')
))
An alternative would be to have nested forms, or to build the form dynamically which may or may not be overkill, depending on what you're actually doing
Maybe I'm wrong but I think you should dig in the ChoiceType class.
When using the basic ChoiceType, Symfony docs says :
The choices option is an array, where the array key is the item's label and the array value is the item's value
If I correctly understand your case, you want something very specific like this kind of choices :
$choices = [
'item_label' => [
'value' => 'item_value',
'price' => 'item_price'
],
'item_label2' => [
'value' => 'item_value2',
'price' => 'item_price2'
],
...
]
I can't tell you precisely which class to override but your best bet will be to take a look at :
ChoiceListFactory class
ChoiceList class (the ChoiceType uses the child class SimpleChoiceList)
ChoiceToValuesTransformer
I have two questions :
What is the data model to store the label and the price ?
What is this list for ? If it's too complicated for you to dig in the Form components, maybe you should split your process in two steps :
One form to define your prices for your product
One form to select the products you are interested in
I'm trying to validate a form in a symfony 2.3 project,
So I have a 'Customer' field :
$builder
->add('customer',
'entity',
array('property'=> 'item',
'multiple' => true,
'expanded' => true,
'class' => 'OrdersBundle:Customer',
'required' => true, 'empty_value' => '',
'query_builder' => function(\Ella\OrdersBundle\Repository\CustomerRepository $er) {
return $er->createQueryBuilder('q')->andWhere("q.is_delete = 0")->orderBy('q.item', 'asc');
}));
I'm trying to return an error when user didn't select anything, so i do this :
properties:
customer:
- Choice: { min: 1, minMessage: 'message' }
Or
properties:
customer:
- NotBlank:
message: message
and other things, but nothing works , an idea of what I'm doing wrong ??
In the doc they say we could use an array, but this doesn't work either ...
Actually Symfony return :
Either "choices" or "callback" must be specified on constraint Choice
For the Choice validator you either need to specify an array with the available allowed choices or a callback function, from the docs:
This constraint is used to ensure that the given value is one of a given set of valid choices. It can also be used to validate that each item in an array of items is one of those valid choices.
What you could use may be the Count validator:
customer:
- Count:
min: 1
max: 99
minMessage: "Min message"
maxMessage: "You cannot specify more than {{ limit }}"