Symfony manually $form->submit(); with multidimensional array - symfony

Im struggling since 10 hours with Symfony 5.1.7 and $form->submit();
My target is a JSON API that converts data to a similiar array. I already debugged and found following part.
Can someone please help me what I am doing wrong here?
To test it, i have created a manually PHP array to submit it.
My Code in Controller
$form = $this->createForm(AddCommentFormType::class);
$test = [
'content' => 'Test',
'media' => [
[
'path' => '1.png',
],
[
'path' => '2.png',
],
],
'_token' => '3bF4qkiUPjKNuGnbY-ySdO6B2sCLzKcS4ar7auX3Dek',
];
$form->submit($test);
AddCommentFormType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('content', TextareaType::class, [
'constraints' => [
new NotBlank(),
new Length([
'max' => 10000,
]),
],
])
->add('media', CollectionType::class, [
'entry_type' => MediaFormType::class,
'constraints' => [
new Count([
'min' => 1,
'max' => 5,
]),
],
])
->add('_token', HiddenType::class, [
'mapped' => false,
'constraints' => [
new NotBlank(),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
MediaFormType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('path', TextType::class, [
'constraints' => [
new NotBlank(),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Media::class,
]);
}
Validator Result
children[media].data
This collection should contain 1 element or more.
[]
children[media]
This form should not contain extra fields.
[▼
[▼
"path" => "1.png"
]
[▼
"path" => "2.png"
]
]

your form has no default data, since you create it with
$form = $this->createForm(AddCommentFormType::class);
createForm can take an additional parameter for default data. This alone is not necessarily a problem, the default is an array of the form (or something very similar, maybe empty strings instead of null)
[
'content' => null,
'media' => [],
'_token' => null,
]
However, the CollectionType will not allow adding or removing elements by default. Setting it's options allow_add (and optionally allow_remove, if you ever set default values) will change that.
So the minimal change would be:
->add('media', CollectionType::class, [
'allow_add' => true, // <-- this is new
'entry_type' => MediaFormType::class,
'constraints' => [
new Count([
'min' => 1,
'max' => 5,
]),
],
])

If your type is AddCommentFormType the form expects by default the data to be in add_comment_form keys like:
$test = [
‘add_comment_form’ => [
'content' => 'Test',
'media' => [
[
'path' => '1.png',
],
[
'path' => '2.png',
],
],
'_token' => '3bF4qkiUPjKNuGnbY-ySdO6B2sCLzKcS4ar7auX3Dek',
]
];

Related

Symfony 5 - display an required input field after a specify dropdown select

I would like to build a form with Symfony 5 that should display a required input field for a certain dropdown selection.
After selecting "sonstiges" I need a required input field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('salutation', ChoiceType::class, [
'label' => 'salutation',
'required' => true,
'constraints' => [
new NotBlank()
],
'placeholder' => 'Bitte wählen',
'label_attr' => [
'class' => 'visually-hidden'
],
'choices' => [
'Herr' => 'herr',
'Frau' => 'frau',
'Diverse' => 'diverse',
],
])
->add('firstname', TextType::class, [
'label' => false,
'required' => true,
'constraints' => [
new NotBlank()
],
'attr' => [
'placeholder' => "firstname",
],
])
->add('afterWork', ChoiceType::class, [
'label' => 'afterWork',
'required' => true,
'constraints' => [
new NotBlank()
],
'placeholder' => 'Bitte wählen',
'label_attr' => [
'class' => 'visually-hidden'
],
'choices' => [
'Studium' => 'studium',
'weitere Schule' => 'weiterSchule',
'Ausbildung' => 'ausbildung',
'Sonstiges' => 'sonstiges',
],
])
}
I added a $builder->addEventListener, unfortunately it didn't give me the result I need, I also added the input field as not required, hidden it and displayed it with a javascript. Unfortunately, it will not be validated because it is not required in the $builder.
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
// this would be your entity, i.e. SportMeetup
$data = $event->getData();
if (!is_null($data) && $data['afterWork'] == "sonstiges") {
$form->add('afterWorkText', TextType::class, [
'label' => "afterWorkText",
'required' => false,
'constraints' => array(
new NotBlank(),
),
'attr' => [
'placeholder' => "afterWorkText"
],
'label_attr' => [
'class' => 'visually-hidden'
],
]);
}
}
Is there a way to insert this field with "display:none" attribute and activate it with a javascript? In addition, the field should then be set to required.
Or can someone help me to find the right solution here?

Symfony Security component register form ChoiceType field insert into bdd as an array(for the roles a user as)

When i register with my form(1) the roles the user select isn't entered in the bdd instead it enter just [] i couldn't get it enter either ROLE_USER or ROLE_ADVERTISER or both in array in the bdd, roles is a longtext in,
Here is my Form builder:
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
])
->add('roles', ChoiceType::class, [
'required' => true,
'multiple' => false,
'expanded' => false,
'choices' => [
'ROLE_USER' => 'ROLE_USER',
'ROLE_ADVERTISER' => 'ROLE_ADVERTISER',
],
]);
$builder->get('roles')
->addModelTransformer(new CallbackTransformer(
function ($rolesArray) {
// transform the array to a string
return count($rolesArray)? $rolesArray[0]: null;
},
function ($rolesString) {
// transform the string back to an array
return [$rolesString];
}
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

Symfony 3.4 Collection Embedding With Default Value

I have an issue with passing the default value to a form. It just doesn't appear in the form.
I've tried to follow the official documentation, and seems to be configured correctly.
The $facets_landing_page is a doctrine entity from ->find($id) with a one to many relationship. facetsLandingPage is the name of the collection (containing many) inside the $facets_landing_page object.
If I pass $facets_landing_page as a 'data' option directly into the ->add function, it shows in the form but then has issues on save submit.
Form creation:
$formBuilder = $this->createFormBuilder($facets_landing_page)
->add('facetsLandingPage', FacetsLandingPageType::class);
Then $form->createView() etc.
The custom type:
class FacetsLandingPageType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
'facetsLandingPage', CollectionType::class, [
'entry_type' => FacetsLandingPageDescriptionType::class,
'entry_options' => [
'label' => false,
],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'label' => false,
]
);
}
public function getBlockPrefix() {
return 'flpwrapper';
}
}
The child type:
class FacetsLandingPageDescriptionType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('language', LanguageSelectType::class);
$builder->add('fec', FecSelectType::class, ['required' => false]);
$builder->add('title', TextType::class);
$builder->add('meta_title', TextType::class);
$builder->add('meta_description', TextType::class);
$builder->add('markdown', MarkdownType::class);
}
public function getBlockPrefix() {
return 'flp';
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(
[
'data_class' => FacetsLandingPageDescription::class,
'required' => false,
'attr' => [
'class' => 'collection_item',
],
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options) {
$view->vars['tab_title'] = 'New';
if (!empty($form->getData())) {
$view->vars['tab_title'] = $form->getData()->getTabTitle();
}
parent::buildView($view, $form, $options);
}
}
You can use the prototype_data attribute in your collection declaration. You can adapt something like the following.
->add(
'collectionItems',
CollectionType::class,
[
'entry_type' => CollectionItemType::class,
'prototype_data' => new CollectionItemType()
]
)
seems like FacetsLandingPageType wasn't getting any default value from the form. So I got rid of it, and I passed the item directly into the main form
->add(
'facetsLandingPage', CollectionType::class, [
'entry_type' => FacetsLandingPageDescriptionType::class,
'entry_options' => [
'label' => false,
],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
]
)

Use Expression constrainst on non object

I have a form like it:
class FeatureDynamicSequenceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('upstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
],
])
->add('downstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
],
])
->add('showUtr', CheckboxType::class,[
'data' => true,
'label' => 'Show UTR',
'required' => false,
])
->add('showIntron', CheckboxType::class,[
'data' => true,
'required' => false,
])
;
}
}
In this form, I would like add a Constrainst that check:
If showUtr or ShowIntron are not checked, then upstream and downstreal can't be > to 0.
Then I want something like it:
->add('upstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
new Expression([
'expression' => 'value > 0 && (this.showUtr || this.showIntron)',
'message' => 'You cannot set upstream if you do not display UTRs and introns.',
]),
],
])
But I can't use it, because it's not an object, value give me the value of the upstream field (it's ok), but I can't access to the showUtr or showIntron value...
EDIT: try with Callback closure
->add('upstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
new Callback([
'callback' => function($data, ExecutionContextInterface $executionContectInterface) {
dump($data);
$executionContectInterface->addViolation('You cannot set upstream if you do not display UTRs and introns.');
},
])
],
])
I have the same problem, $data just contain the field value.
I don't really want to create an Entity, because I don't persist it... And I can't believe there is not a solution to check it whithout creating an Entity.
I answered in a previous question here
I solved it by using:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new Callback([
'callback' => function($data, ExecutionContextInterface $executionContectInterface) {
if ($data['upstream'] > 0 && (!$data['showUtr'] || !$data['showIntron'])) {
$executionContectInterface->buildViolation('You cannot set upstream if you do not display UTRs and introns.')
->atPath('[upstream]')
->addViolation()
;
}
},
]),
],
]);
}
The full code is:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('upstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
],
])
->add('downstream', IntegerType::class, [
'data' => 0,
'constraints' => [
new LessThan([
'value' => 1000,
]),
],
])
->add('showUtr', CheckboxType::class, [
'data' => true,
'label' => 'Show UTR',
'required' => false,
])
->add('showIntron', CheckboxType::class, [
'data' => true,
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new Callback([
'callback' => function($data, ExecutionContextInterface $executionContectInterface) {
if ($data['upstream'] > 0 && (!$data['showUtr'] || !$data['showIntron'])) {
$executionContectInterface->buildViolation('You cannot set upstream if you do not display UTRs and introns.')
->atPath('[upstream]')
->addViolation()
;
}
},
]),
],
]);
}

Using Vue.js to bind value of input field to a second input field

I have Symfony Form which defined as below (minus non-pertinent fields for brevity):
<?php
namespace AppBundle\Form;
use AppBundle\Entity\Category;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TextType::class, [
'label' => 'label.display_name',
'attr' => [
'placeholder' => 'placeholder.category_name',
'class' => 'label',
'#input' => 'vUpdateSlug'
]
])
->add('slug', TextType::class, [
'label' => 'label.slug',
'attr' => [
'class' => 'slug',
'#input' => 'vUpdateSlug',
':value' => 'slug'
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Category::class,
'category_id' => null
]);
}
}
I am attaching Vue.js directives to both input fields. The idea is that when someone types in the label field, the slug field is automatically updated with the label input value with some minor changes (replacing spaces with hyphens). I still want the user to be able to alter the slug if they wish, but not have the label update.
The v-on:input/#input directive behaviour works, however, I'm just starting with Vue.js and my implementation feels a little clunky (repetitive) - see below:
new Vue({
delimiters: ['[[', ']]'],
el: '#category-form',
data: {
slug: this.slug = $('[name="category[slug]"]').val()
},
ready: function () {
this.slug = $('[name="category[slug]"]').val();
},
methods: {
vUpdateSlug: function (event) {
var str = event.target.value.replace(/[^a-zA-Z0-9 -]/g, '').replace(/\s+/g, '-').toLowerCase();
return this.slug = str;
}
}
});
Is there a better solution to my problem?
After more researching and tinkering, I came up with the following which produces the desired result:
CategoryType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TextType::class, [
'label' => 'label.display_name',
'attr' => [
'placeholder' => 'placeholder.category_name',
'class' => 'label',
'v-model' => 'label'
]
])
->add('slug', TextType::class, [
'label' => 'label.slug',
'attr' => [
'class' => 'slug',
'#input' => 'setSlug',
':value' => 'slug'
]
]);
}
Vue Script
new Vue({
delimiters: ['[[', ']]'],
el: '#form-wrapper',
data: {
label: $('[name="category[label]"]').val(),
slug: $('[name="category[slug]"]').val()
},
watch: {
label: function(newLabel) {
this.slug = this.compileSlug(newLabel)
}
},
methods: {
compileSlug: function(input) {
return input.replace(/[^a-zA-Z0-9 -]/g, '')
.replace(/\s+/g, '-')
.toLowerCase();
},
setSlug: function (input) {
this.slug = this.compileSlug(input.target.value)
}
}
});
JSFiddle functioning example

Resources