Symfony TypeTestCase How to populate collection of embedded form using mock? - forms

For a simple blog manager where I want to associate some tags to some Post I have create a repository able to create a new entity when it's name can't be found in database:
public function requireByName(string $name): PostTag
{
$result = $this->findOneBy(['name' => [$name]]);
if (!$result instanceof PostTag) {
$result = $this->create($name);
$this->getEntityManager()->flush();
}
return $result;
}
I have managed to create a form TagType populating PostTag entity with the results of the PostTagRepository::requireByName method:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name', TextType::class, [
'setter' => function (PostTag &$tag, string $name) {
$tag = $this->tagRepository->requireByName($name);
},
]);
}
All of this is tested with the instruction provided in Symfony documentation How to unit test your form
public function testItSetsRepositoryResultToInboundModel(): void
{
$formTagName = 'my new tag';
$formData = ['name' => $formTagName];
$createdPostTag = new PostTag($formTagName);
$this->tagRepository->method('requireByName')
->with($formTagName)
->willReturn($createdPostTag);
$this->testedForm->submit($formData);
self::assertTrue($this->testedForm->isSynchronized());
$formEntity = $this->testedForm->getData();
self::assertInstanceOf(PostTag::class, $formEntity);
self::assertSame($createdPostTag, $formEntity);
}
public function testItMapsTagsWithItsNameInFormField(): void
{
$tagName = 'my new tag';
$formData = new PostTag($tagName);
$form = $this->factory->create(TagType::class, $formData);
self::assertTrue($form->has('name'));
self::assertEquals($tagName, $form->get('name')->getData());
}
My next step is to create a TypeTestCase for my PostType form where I can assume that results from PostTagRepository are populated in my Post entity once I submit an array of tag names. Here is the code I produced so far (I load some extensions - Validator, CKEditor - required from the legacy behavior) :
class PostTypeTest extends TypeTestCase
{
use ValidatorExtensionTrait;
private PostTagRepository&MockObject $tagRepository;
protected function setUp(): void
{
$this->tagRepository = $this->createMock(PostTagRepository::class);
parent::setUp(); // TODO: Change the autogenerated stub
}
/** #return FormExtensionInterface[]
*
* #throws Exception
*/
protected function getExtensions(): array
{
$tagType = new TagType($this->tagRepository);
return [
$this->getValidatorExtension(),
$this->getCKEditorExtension(),
new PreloadedExtension([$tagType], []),
];
}
private function getCKEditorExtension(): PreloadedExtension
{
$CKEditorType = new CKEditorType($this->createMock(CKEditorConfigurationInterface::class));
return new PreloadedExtension([$CKEditorType], []);
}
public function testItMapsRepositoryResultToTagCollection(): void
{
$model = new Post();
$form = $this->factory->create(PostType::class, $model);
$requiredTag = new PostTag('poo');
$this->tagRepository->method('requireByName')
->with('poo')
->willReturn($requiredTag);
$form->submit([
'title' => 'a title',
'content' => 'some content',
'isPublic' => true,
'tags' => [
['name' => 'poo'],
],
]);
self::assertTrue($form->isSynchronized());
self::assertNotEmpty($model->getTags());
}
}
And here is the PostType code :
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => 'Titre',
'required' => true,
'constraints' => [new Assert\NotBlank(), new Assert\Length(['min' => 5, 'max' => 255])],
]
)
->add(
'content',
CKEditorType::class,
[
'required' => true,
'label' => 'Contenu',
'config' => [
'filebrowserBrowseRoute' => 'elfinder',
'filebrowserBrowseRouteParameters' => [
'instance' => 'default',
'homeFolder' => '',
],
],
]
)
->add('isPublic', CheckboxType::class, [
'label' => 'Le Post est public',
'required' => false,
])
->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => ['label' => false],
])
->add('submit', SubmitType::class, [
'label' => 'app.actions.validate',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Post::class,
]
);
}
}
All properties are populated with submitted values but the $model->getTags() Collection remains empty.
Help would be really appreciated if someone could tell me what I am doing wrong ?

Related

Functionnals tests with Symfony4 on Forms (EntityType input) didn't work

I have a form for Product Entity
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['edit'] === false) {
$builder
->add('code', TextType::class, [
'label' => 'Code',
])
->add('name', TextType::class, [
'label' => 'Nom',
]);
}
$builder
->add('competence', EntityType::class, [
'label' => 'Compétence',
'choice_label' => 'name',
'class' => Competence::class,
'required' => false,
]);
}
It's works but when I want to write functionnal test it didn't work. While my unit tests are working.
For testing, i've a database AND database_test. For this I use the .env.local and .env.test.local with the right DATABASE_URL (main and test).
public function testAdd()
{
// Instance du client HTTP
$client = static::createHttpBrowserClient();
// Construction de la requête GET sur la page product
$crawler = $client->request(
'GET',
getenv('APP_TEST_URL') . '/product/new'
);
$form = $crawler->selectButton('Ajouter')->form([
'product[name]' => 'Product Test',
'product[code]' => 'ABC_DEF',
'product[competence]' => $this->entityManager->getRepository(\App\Entity\Competence::class)->findOneBy(['name' => 'BVB'])->getId(),
]);
$client->submit($form);
$crawler = $client->followRedirect();
$this->assertSame(1, $crawler->filter('div.alert.alert-success')->count());
}
How I setup this :
/**
* #var \Doctrine\ORM\EntityManager
*/
private $entityManager;
public function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function tearDown(): void
{
$client = static::createHttpBrowserClient();
$client->restart();
}
My problem is about the EntityType Competence. I've to put the Id that I want but for the same Competence in Database and Database_URL, ids are not the same.
So I get this error :
App\Tests\E2e\Controller\ProductControllerTest::testAdd
InvalidArgumentException: Input "product[competence]" cannot take "3" as a value (possible values: "", "258", "259", "260", "261", "262", "263", "264", "265", "266", "267", "268", "269", "270", "271", "272", "273", "274", "275", "276", "277", "278", "279").
Even if I enter "258", for example, I've this error :
"LogicException: The request was not redirected." on the followRedirect().
I don't know if I'm on the right way, but I'm lost right now. I tried a lot of things with doctrine.yaml and .env.*

symfony validation error on nested form for dynamically modified field

I have an embedded forms with a entity field Activity in a sub form dynamically populated with data from a field Module whose data are set via ajax request.
But when i submit the form i have a validation error ("this value is not valid") and the activity field is blank.
The principle :
the main form (Session) has an entity field module and contains a collection (Timeslot).
The Timeslot subform has a field activity whose content depends on the Module value.
My forms :
class SessionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('module', EntityType::class, [
'label' => 'module.name',
'required' => true,
'class' => Module::class,
'placeholder' => 'choose',
])
->add('timeslots', CollectionType::class, [
'required' => true,
'constraints' => new Valid(),
'prototype_name' => '__timeslot_prot__',
'entry_type' => TimeslotType::class,
'entry_options' => ['module' => isset($module) ? $module->getId() : 0],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Session::class,
'module' => null,
]);
}
}
And the Timeslot
class TimeslotType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$module = $options['module'];
$builder
->add('activity', EntityType::class, [
'class' => Activity::class,
'multiple' => false,
'required' => true,
'constraints' => new NotBlank(),
'query_builder' => function (ActivityRepository $activity) use ($module) {
if (null !== $module) {
$qb = $activity->createQueryBuilder('a')
->join('a.activityGroups', 'ag')
->join('ag.module', 'm')
->where('m.id = :mid')
->setParameter('mid', $module)
->orderBy('a.name', 'ASC');
return $qb;
} else {
$qb = $activity->createQueryBuilder('a')
->where('a.id = 0');
return $qb;
}
},
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Timeslot::class,
'module' => null,
]);
}
}
I also have a javascript script which fills the Activity select options with a query through an ajax call.
I also tried to modify my TimeslotType following this symfony doc
but i stumble upon the problem that in my case, the Module field is not in the same formBuilder than Activity, so the POST_SUBMIT eventlistener can't be applied in my case.
class TimeslotType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$module = $options['module'];
$formModifier = function (FormInterface $form, Module $module = null) {
$activities = [];
if (null !== $module){
foreach($module->getActivityGroups() as $ag){
foreach($ag->getActivities() as $activity){
$activities[] = $activity;
}
}
}
$form->add('activity', EntityType::class, [
'label' => 'timeslot.activity',
'label_attr' => ['class' => 'mandatory', 'data-extrainfo' => 'panel_timeslot'],
'attr' => [
'class' => 'activitieslist',
],
'class' => Activity::class,
'multiple' => false,
'required' => true,
'constraints' => new NotBlank(),
'choices' => $activities,
'choices_as_values' => true,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// the entity : Timeslot
$data = $event->getData();
$formModifier($event->getForm(), $module);
}
);
//$builder->get('module') ... Makes no sense here since the module attribute is not a Timeslot, but belongs to Session
/*
$builder->get('module')->addEventListener(FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$module = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $module);
}
);
*/
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Timeslot::class,
'module' => null,
]);
}
}
How can i resolve my problem and validate my form ?
===
Edit
The ajax request is called when the module field is changed
function updateActivities(block) {
var module = $('#session_module').val();
if (module != ''){
$.ajax({
url: Routing.generate('project_module_activities_ajax', {}),
data: { 'module': module },
method: 'POST',
success: function (activities) {
var sel = '';
for (var i = 0; i < activities.length; i++) {
sel += '<option value="' + activities[i].id + '">' + activities[i].name + '</option>'
}
$('#litimeslot'+block).find('.activitieslist').each(function () {
$(this).html(sel);
})
}
});
}
}
And the route project_module_activities_ajax simply returns an array of activities for the module from a query
something like :
[
['id' : 1, 'name' : 'activity 1'],
['id' : 2, 'name' : 'activity 2'],
}
Edit 2
if i add this eventlistener in the TimeslotType :
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($moduleId) {
$form = $event->getForm();
$data = $event->getData();
echo '<pre>form AFTER=';var_dump($form->getData());echo '</pre>';
echo '<pre>data AFTER=';var_dump($data);echo '</pre>';
echo '<pre>module AFTER';var_dump($moduleId);echo '</pre>';
}
);
I have
form AFTER= NULL
data AFTER= array(
["activity"]=>
string(3) "573" <= which is the value i selected
...
)
module AFTER int(0)

Zend - Dynamic form dropdown

I start with zend framework and I'm having trouble implementing a dynamic drop-down list.
I need to create a simple dropdown list of events select from the database.
This is my Module class :
public function getFormElementConfig()
{
return array(
"factories" => [
'participant_form' => function (ServiceManager $serviceManager) {
/** #var EntityManager $entityManager */
$entityManager = $serviceManager->get("doctrine.entitymanager.orm_default");
$events = $entityManager->getRepository('Application\Entity\Event')->findAll();
$eventForSelect = array();
foreach ($events as $event) {
$eventForSelect[$event->getId()] = $event->getName();
}
/** #var \Zend\Form\Form $form */
$form = new ParticipantForm();
$form->setHydrator(new DoctrineHydrator($entityManager));
$form->setObject(new Participant());
$form->setOption('event_for_select', $eventForSelect);
return $form;
},
]
);
}
but I do not know how to get the option 'event_for_select' in my form :
class ParticipantForm extends Form
{
public function __construct($name = null)
{
parent::__construct('user');
$this->setAttribute('class', 'form-horizontal');
$this->add([
'name' => 'id',
'type' => 'Hidden',
]);
$this->add([
'name' => 'firstname',
'type' => 'Text',
'options' => [
'label' => 'Prénom',
],
]);
$this->add([
'name' => 'event',
'type' => 'Select',
'options' => [
'label' => 'Event',
'value_options' => // ?? $event_for_select
],
]);
Thanks for your help !
Add field in your form class:
class ParticipantForm extends Form
{
protected $eventForSelect;
public function setEventForSelect($eventForSelect)
{
$this->eventForSelect = $eventForSelect;
// update field value options
$this->get('event')
->setValueOptions($this->eventForSelect);
return $this;
}
// rest of form class code
}
Then use setEventForSelect() method in your factory:
/** #var \Zend\Form\Form $form */
$form = new ParticipantForm();
$form->setHydrator(new DoctrineHydrator($entityManager));
$form->setObject(new Participant());
$form->setEventForSelect($eventForSelect);
return $form;
And then in form:
$this->add([
'name' => 'event',
'type' => 'Select',
'options' => [
'label' => 'Event',
'value_options' => $this->eventForSelect,
],
]);

Render radio button form from a collectionType [Symfony]

I'm trying to create a quiz for my application with Symfony.
At this point I have 3 class, Qcm, QcmQuestion and QcmAnswer.
I have multiple questions which contains multiple answers, and I want to display answers as radio button.
I only achieve to display them as input. How can I display them as radio button ?
BaseController.php
$em = $this->getDoctrine()->getManager()>getRepository('QcmBundle:QcmQuestion');
$qcmQuestions = $em->findBy(array('qcm' => $id));
$formBuilderQuestionnaire = $this->createFormBuilder();
$i = 0;
foreach ($qcmQuestions as $qcmQuestion) {
$formBuilder = $this->get('form.factory')->createNamedBuilder($i, FormType::class, $qcmQuestion);
$formBuilder
->add('question')
->add('qcmAnswers', CollectionType::class, [
'entry_type' => QcmAnswerType::class
])
;
$formBuilderQuestionnaire->add($formBuilder);
$i++;
}
$form = $formBuilderQuestionnaire->getForm();
$form->add('save', SubmitType::class, array('label' => 'Envoyer',
"attr" => array("class" => "btn btn-primary")));
return $this->render('QcmBundle:qcm:qcmQuestions.html.twig', ["qcmQuestions" => $qcmQuestions, "form" => $form->createView()]);
QcmAnswerType
class QcmAnswerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('response');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'QcmBundle\Entity\QcmAnswer'
));
}
public function getBlockPrefix()
{
return 'qcmbundle_qcmanswer';
}
You do not want CollectionType + QcmAnswerType but you want just EntityType with options multiple: false and expanded: true.
So instead of
->add('qcmAnswers', CollectionType::class, [
'entry_type' => QcmAnswerType::class
])
try with something like:
->add('qcmAnswers', EntityType::class, [
'multiple' => false,
'expanded' => true,
])
More info about EnttiyType can be found in Symfony Docs.
I managed to solved my problems, I used EntityType with a query inside it.
Entities passed to the choice field must be managed. Maybe persist them in the entity manager?"
I had this error because I needed to pass to createNamedBuilder the entire array of questions like this :
$formBuilder = $this->get('form.factory')->createNamedBuilder($i, FormType::class, $qcmQuestions);
The final result :
$em = $this->getDoctrine()->getManager()->getRepository('QcmBundle:QcmQuestion');
$qcmQuestions = $em->findBy(array('qcm' => $id));
$formBuilderQuestionnaire = $this->createFormBuilder();
$i = 0;
foreach ($qcmQuestions as $qcmQuestion) {
/* #var $qcmQuestion QcmQuestion */
$formBuilder = $this->get('form.factory')->createNamedBuilder($i, FormType::class, $qcmQuestions);
$formBuilder
->add('qcmAnswers', EntityType::class, [
'class' => 'QcmBundle\Entity\QcmAnswer',
'expanded' => true,
'label' => $qcmQuestion->getQuestion(),
'query_builder' => function (EntityRepository $er) use ($qcmQuestion) {
return $er->createQueryBuilder('qcmAnswer')
->join('qcmAnswer.qcmQuestion', 'qcmQuestion')
->where('qcmAnswer.qcmQuestion = :qcmQuestionId')
->setParameter('qcmQuestionId', $qcmQuestion->getId());
},
]);
$formBuilderQuestionnaire->add($formBuilder);
$i++;
}
$form = $formBuilderQuestionnaire->getForm();

symfony, cant pass validation on a embedded form + file

Trying to embed a collection or just a simple type containing a file field, but both result in the same problem. On post the UploadedFile is present, but validation fails. "This value is not valid."
Bashing my head in all day, anyone?
Company form:
Simple embed with a file:
$builder->add('logo', new FileType(), [
'label' => 'Logo',
'required' => false,
'attr' => [
'accept' => 'image/*',
]
]);
Collection with files:
$builder->add('images', 'collection', array(
'type' => new FileType(),
'data' => [
new File(),
new File(),
new File(),
],
));
FileType:
class FileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$data = $builder->getData();
$builder->add('file', 'file', [
'label' => 'Bestand',
]);
$builder->add('submit', 'submit', [
'label' => 'Save',
]);
}
public function getName()
{
return 'file';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\File',
));
}
}
The entity has a functioniong one-to-many from Company to File.
Everything works, but when the form is submitted. Both fields have a "This value is not valid." error.
Here is a dump of the clientData preSubmit:
'logo' =>
object(Symfony\Component\HttpFoundation\File\UploadedFile)[13]
private 'test' => boolean false
private 'originalName' => string 'etc.jpg' (length=13)
private 'mimeType' => string 'image/jpeg' (length=10)
private 'size' => int 103843
private 'error' => int 0
'images' =>
array (size=3)
0 =>
object(Symfony\Component\HttpFoundation\File\UploadedFile)[14]
private 'test' => boolean false
private 'originalName' => string 'Screen Shot 2014-07-24 at 17.15.30.png' (length=38)
private 'mimeType' => string 'image/png' (length=9)
private 'size' => int 84102
private 'error' => int 0
1 => null
2 => null
Seems perfectly fine to me!