Submit array of tag names and convert to entities - forms

I want to do such a trick: submit tag names as an array and then convert them to entities. Submitted data:
{"title":"Title","description":"Desc", "tags": ["first", "second"]}
Form:
class ImageType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('imageFile')
->add('tags', CollectionType::class, [
'entry_type' => ImageTagType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Image::class,
'csrf_protection' => false,
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_image';
}
}
Form for image tag:
class ImageTagType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\ImageTag'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_imagetag';
}
}
The entities are connected with many-to-many. One image can have many tags and one tag can have many images. I am building an API and want to simplify tags submission. What should I do?

Use DataTransformer. Example:
....
$builder->get('YOUR_FILED')
->addModelTransformer(new CallbackTransformer(
function ($array) {
// your implementation here
},
function ($array) {
if (!$array) {
return;
}
return new ArrayCollection(array_map(function ($data) {
return $this->someRepository->findOneBy(['FIELD' => $data]);
}, $array));
}
));
....

Related

Embed nested collections form in Symfony 6

I want to embed nested collections on three levels, for the second level it works but not the last one i can't get the prototype on the last level.
I work with Symfony 6 and i use stimulus.
class FormType1 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field1')
->add('formType2', CollectionType::class, [
'entry_type' => FormType2::class,
'entry_options' => ['label' => false],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData1::class,
]);
}
}
class FormType2 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field2')
->add('formType3', CollectionType::class, [
'entry_type' => FormType3::class,
'entry_options' => ['label' => false],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData2::class,
]);
}
}
class FormType3 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field3')
->add('field4');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData3::class,
]);
}
}
Here is the prototype:
<div {{ stimulus_controller('form-collection') }}
data-form-collection-index-value="{{ form.formType3|length > 0 ? form.formType3|last.vars.name + 1 : 0 }}"
data-form-collection-prototype-value="{{ form_widget(form.formType3.vars.prototype)|e('html_attr') }}" <<============
>
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
<button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button>
</div>
In Stimulus Controller form-collection i get the prototype in this way
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
The prototypeValue It return an empty string

Custom Choice type with parameters

I make my custom ChoiceType
class CountryType extends ChoiceType
{
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'choices' => $this->locationService->getCountries(),
]);
}
}
class StateType extends ChoiceType
{
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$choices = isset($countryId)? $this->locationService->getStatesByCountryId($countryId):[]; <---
$resolver->setDefaults([
'choices' => $choices,
]);
}
}
class FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('country', CountryType::class, [
'label' => 'Country',
'required' => false,
])->add('state', StateType::class, [
'label' => 'State',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => FormRequest::class,
'validation_groups' => [],
]);
}
}
But for StateType optionally (when I edit entity) I need $countryId how I can put it? Maybe with options or choice_loader?
I also tried to crank out such an idea, but as I understand it, this is not possible
{{ form_widget(form.state, {
'countryId': form.country.vars.value,
}) }}

How-To: A form with a collection of forms which contains another collection

I have to create a simple system for some polls starting with 3 entities: VotingSession, Question and Answer.
Between VotingSession and Question is a One-To-Many relationship.
Between Question and Answer is a One-To-Many relationship.
What I'm trying to achieve is the next scenario: Answers can have multiple types. For examples, you can have radiobox inputs where you will choose one answer, you can have checkbox inputs where you will choose 1 or more items or text input fields where you will fill whatever you want.
I already pass a VotingSession object to the first form and entire form is displayed perfectly, so there's no problem here. What I discovered is that when I access $builder->getData() inside the buildForm() method on QuestionForm I get value NULL, even form is filled.
My idea is to try to achieve my goal based on passed value, but I can't do that.
Do you have any idea what I'm doing wrong or any advice?
I have the following forms:
VotingSessionForm:
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('questions', CollectionType::class, [
'entry_type' => QuestionForm::class,
'by_reference' => true,
'prototype' => true,
'prototype_name' => '__question__',
'entry_options' => [
'label' => false,
],
'constraints' => [
new Valid(),
],
]);
}
/**
* #inheritDoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'error_bubbling' => false,
'data_class' => VotingSession::class,
]);
}
QuestionForm:
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('answers', CollectionType::class, [
'entry_type' => AnswerForm::class,
'prototype' => true,
'prototype_name' => '__answers__',
'entry_options' => [
'label' => false,
],
'constraints' => [
new Valid(),
],
]);
}
/**
* #inheritDoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Question::class,
]);
}
AnswerForm:
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TextType::class, [
'label' => false,
'disabled' => true,
])
->add('value', NumberType::class, [
'label' => false,
'mapped' => false,
]);
}
/**
* #inheritDoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Answer::class,
]);
}

How to retrive data from non-mapped field in embeded form

I would like to know if it's possible to get the data from a non-mapped field in an embeded form.
Here is the main form :
class PlayedLifeScoreType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//->add('nom')
//->add('prenom')
// NOTE: Use form collection to allow multiple `played` forms per JoueurType
->add('playeds', CollectionType::class, [
'entry_type' => PlayedLifeType::class,
'label' => false,
])
->add('submit', SubmitType::class, [
'attr' => ['class' => 'save'],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Partie::class,
]);
}
}
And the embedded one :
class PlayedLifeType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
/*->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$form->add('joueur', null, array(
'data' => $event->getData() ?: options['joueur']
))*/
//->add('joueur')
->add('joueur', TextType::class, [
'label' => false,
'disabled' => 'true',
'attr' => ['class' => 'tinymce'],
])
->add('max')
->add('score')
->add('round', IntegerType::class,[
'mapped' => false,
'label' => 'Round',
])
;
//});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Played::class,
'joueur' => null
]);
}
}
And want to get the data from "round". I tried like this but doesn't work :
$r = $mainForm->get('playeds')->get("round")->getData();
I get this error :
Child "round" does not exist.
Any idea ?
field "playeds" is a CollectionType.
So, for each entry, there is a 'round' value
To access this, you should do something like:
foreach ($mainForm->get('playeds') as $played) {
//you can access round here with $played->get('round')->getData()
//Or the Played object with $played->getData()
}

Options to subform

How i can give form's options to a subform ?
In the example below, i have the option "special" declared.
I want to access this option "special" in my subform.
My main form :
class DemandeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', TextType::class, []);
$builder->add('service_agent', ServiceType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}
And my subform :
class ServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($options['special']);
$builder->add('service', TextType::class, []);
$builder->add('agent', TextType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
}
I answer my question.
I must declare option "special" in subform.
And this field's option is accessible in the main form.
Like that :
class DemandeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', TextType::class, []);
$builder->add('service_agent', ServiceType::class, [
'mapped' => false,
'special' => $options['special']
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}
And in my subform :
class ServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($options['special']); // It's OK :)
$builder->add('service', TextType::class, []);
$builder->add('agent', TextType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}