Unit testing of forms with collections - symfony

I have almost no experience in terms of unit tests. I read this symfony cookbook chapter to test a form type.
http://symfony.com/doc/current/cookbook/form/unit_testing.html
My form look like this:
public function __construct(SecurityContext $securityContext, \Doctrine\ORM\EntityManager $em)
{
$this->securityContext = $securityContext;
$this->entityManager = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', 'text', array('label' => 'title', 'translation_domain' => 'messages', 'attr' => array('maxlength' => 255)))
->add('comments', 'collection', array(
'type' => new CommentType() ,
'allow_add' => false,
'allow_delete' => false,
'label' => false,
'options' => array(
'label' => false,
)
)
)
->add('translations', 'a2lix_translations', array(
'fields' => array(
'coverLetter' => array(
'label' => 'msg.coverLetter',
'field_type' => 'textarea',
'attr' => array('class' => 'rte')
)
)
));
}
Now i write a class to test my form.
class QuestionnaireControllerTest extends TypeTestCase
{
public function testAddQuestionnaire()
{
$kernel = new \AppKernel('dev', true);
$kernel->boot();
$container = $kernel->getContainer();
$securityContext = $container->get('security.context');
$entityManager = $container->get('doctrine.orm.entity_manager');
$formData = array('title' => 'Exp. title');
$type = new QuestionnaireType($securityContext, $entityManager);
$form = $this->factory->create($type);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}
But I have some questions for my test class.
Is this the correct way to get the kernel?
How can i test the form item "comments (collection)", "translations (a2lix_translations)"?
Unfortunately i do'nt find useful tutorials for these issues.

When it comes to TypeTest's it can become a little weird actually as it seems out of the loop. I faced some similar problems and actually did the following:
1) Regarding the Kernel: Extend the KernelTestCase and include TypeTestCase (and super) logic in your test (or create yourself an abstract). That way you will have a more consistent Kernel init way and TypeTestCase is not that big besides the form initialization. To have that customizable actually might be helpful later on.
2) To get especially the A2lix stuff loaded as extensions to your test forms you have to override
protected function getExtensions()
and return an array
return array(
new PreloadedExtension(..., array())
);
For A2lix ... might look like
$gedmoTranslationsType = new GedmoTranslationsType($this->container->get('a2lix_translation_form.gedmo.listener.translations'), $this->container->get('a2lix_translation_form.gedmo.service.translation'), $this->getLocales(), false);
$gedmoTranslationsLocalesType = new GedmoTranslationsLocalesType();
$translationsFields = new TranslationsFieldsType();
return array(
$gedmoTranslationsType->getName() => $gedmoTranslationsType,
$gedmoTranslationsLocalesType->getName() => $gedmoTranslationsLocalesType,
$translationsFields->getName() => $translationsFields,
);
I already got myself the container and locales in convenient members and functions. You should do so to.
In general the extensions will also become important with e.g. testing Entity types. Therefore, generalizing these things might be helpful.
There is a lot more stuff to Type testing actually that you will figure out over time. I hope this helps anyway.
Best

Related

Expression Validator gives "Unable to get a property on a non-object"

can anybody figure out why this:
$builder
->add('networkLoIp', IntegerType::class, array(
'constraints' => array(
new A\NotBlank(),
new A\Expression(array(
'expression' => 'value <= this.getNetworkHiIp()'
))
)
))
->add('networkHiIp', IntegerType::class, array(
'constraints' => array(
new A\NotBlank()
)
))
->setMethod('post')
;
gives an error like this: "Unable to get a property on a non-object" ?
When I dump data after submit I can see values I've put inside the form.
EDIT
A very similar error I get after moving expression to options, i.e.
public function configureOptions(OptionsResolver $optionsResolver)
{
$optionsResolver->setDefaults(array(
'constraints' => array(
new A\Expression(array(
'expression' => "value['networkLoIp'] <= value['networkHiIp']"
))
)
));
}
"Unable to get an item on a non-array."
For your particular case you can use this:
$builder
->add('networkLoIp', IntegerType::class, array(
'constraints' => array(
new A\NotBlank(),
new A\LessThanOrEqual($this->getNetworkHiIp())
// or if the method is static and defined somewhere else
// new A\LessThanOrEqual(ThatObject::getNetworkHiIp())
)
))
Reference for LessThanOrEqual.
If the field stores an IP address I would use Ip validator as well.
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use AppBundle\Form\CreateLicence
class crateLicenciaonType extends AnstractType{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$entity= new CreateLicence();
$builder
->add('networkLoIp', IntegerType::class, array(
'constraints' => array(
new A\NotBlank(),
new A\LessThanOrEqual($entity->getNetworkHiIp())
// or if the method is static and defined somewhere else
// new A\LessThanOrEqual(ThatObject::getNetworkHiIp())
)
))
}
You can't apply expression contrainst on a non object but you can use an other one for the same purpose or create an object (DTO) link to your form.

Symfony dependent dropdown with 3 entities

So I'm about to create a form with three dropdowns which are interdependent.
When one or several channel1s are selected, the choices for channel 3 should change, according to what is selected for channel1. And dependent on that, the last dropdown "agencies" should change its choices.
I've already tried different solutions but none of them has worked so far. Right now I'm stuck on the solution provided by Symfony's documentation, which provides code for two entities but even with that one, my second dropdown doesn't have any values, so it's not working.
Here is my Form Type:
class SelectionType extends AbstractType {
protected $tokenStorage;
// private $manager;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//solution Symfony Documentation
$channel1s = new Channel1();
$currentuser = $this->tokenStorage->getToken()->getUser();
$builder
->add('channel1s', EntityType::class, array(
'class' => 'AppBundle:Channel1',
'property' => 'name',
'label' => 'label.channel1s',
'empty_value' => 'label.select_channel1s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'multiple' => true,
'required' => false,
));
$formModifier = function (FormInterface $form, Channel1 $channel1s = null) {
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3();
$form->add('channel3s', EntityType::class, array(
'class' => 'AppBundle:Channel3',
'property' => 'name',
'label' => 'label.channel3s',
'empty_value' => 'label.select_channel3s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'choices' => $channel3s,
'multiple' => true,
'choices_as_values' => true,
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getChannel1s());
}
);
$builder->get('channel1s')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$channel1s = $event->getForm()->getData();
$formModifier($event->getForm()->getparent(), $channel1s);
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'DocumentBundle\Entity\UploadProfile'
));
}
public function getName()
{
return 'uploadprofile';
}
}
I've also tried a solution with Subscribers from that page: http://showmethecode.es/php/symfony/symfony2-4-dependent-forms/ but it didn't work out either..
I think my problem is somewhere around that line:
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3();
but that's just a guess..
I also added that ajax function:
var $channel1s = $('#uploadprofile_channel1s');
$channel1s.change(function() {
var $form = $(this).closest('form');
var data = {};
data[$channel1s.attr('name')] = $channel1s.val();
// data[channel3s.attr('name')] = channel3s.val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(html) {
$('#uploadprofile_channel3s').replaceWith(
$(html).find('#uploadprofile_channel3s')
);
}
});
});
My 3 entities have ManytoMany or OneToMany relationships and I should have all the right getters and setters, but if anyone needs them to varify, let me know and I will upload them!
I've been stuck on this for quite a while now, so I would be happy about any kind of help or advise!
NOTE: there's still a third entity (agency) to be added but since not even the first one's are working, I decided to upload only the first two..
ADDED:
or maybe somebody can explain to my that line:
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3s();
might be, that this is my problem?

Symfony 3: How to use two choices/dropdowns from two tables in one form

Symfony version 3.1.3
I have created a dropdown using the entity called Classes and you can see the Controller below,
public function studentAddClassAction( $id, Request $request )
{
// get the student from the student table
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('PIE10Bundle:Users')->find($id);
// new class object and create the form
$classes= $em->getRepository('PIE10Bundle:Classes')->findAll();
$form = $this->createForm(ClassType::class, $classes);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid() )
{
// form submit operations
}
return $this->render(
'PIE10Bundle:student:layout_student_addclass.html.twig',
array(
'user' => $user,
'title' => 'Add Class',
'tables'=> 1,
'form' => $form->createView()
)
);
}
and the ClassType is below
class ClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('classes',
EntityType::class,
array('class' => 'PIE10Bundle:Classes',
'expanded' => false,
'multiple' => false,));
$builder->add('Add Class',
SubmitType::class,
array('attr' => array('class' => 'btn btn-primary',
'style' => 'margin:15px 0;')) );
}
}
And this works fine and it gives all the classes from the database. Also I have another entity called Users and it has a column called roles (DC2Type:array) and it has a role called ROLE_PARENT and I can retrieve all the parents using the following query
$query = $this->getDoctrine()->getEntityManager()
->createQuery('SELECT u FROM PIE10Bundle:Users u WHERE u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%' );
$users = $query->getResult();
My Question is how to add these parents list as a choice list into same above form in the studentAddClassAction Controller.
Please let me know in any other information is needed for this.
To have a custom set of entities as a choice list you need to use a query_builder option
So it will look like
$builder->add('parent',
EntityType::class,
array('class' => 'PIE10Bundle:Users',
'expanded' => false,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%');
},
'multiple' => false
));

Symfony2 FormBuilder with Entity class

I have a form that works well, there is just one issue with it and I'm hoping that I'll get an answer on how to do what I need to do.
<?php
namespace ADS\UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Core\SecurityContext;
class UserType extends AbstractType {
private $type;
public function __construct($type) {
$this->type = $type;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('firstName', 'text', array('required' => true))
->add('lastName', 'text', array('required' => true));
$builder->add('email', 'email', array('required' => true));
$builder->add('parentCompany', 'entity', array(
'class' => 'ADSUserBundle:Company',
'expanded' => false,
'empty_value' => 'CHOOSE ONE',
'required' => false,
'property' => 'companyName'
))
->add('enabled', 'choice', array('choices' => array('1' => 'Enabled', '0' => 'Disabled')))
->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\UserBundle\Entity\User'));
}
public function getName() { return 'ads_userbundle_user'; }
}
I have this form, the portion I am looking at is the 'roles' portion... Right now it created a multiple select box ( as I expect it to ), though the value is sequentially ie: 0,1,2,3,4...
What I really need is to figure out how to take this entity, and make the property to be the displayName ( as it is now ) and get the value to be the corresponding internalName This way it'll give me an array like:
array('ROLE_EXAMPLE' => 'EXAMPLE', 'ROLE_EXAMPLE1' => 'EXAMPLE1')
Any ideas how to accomplish this is greatly appreciated.
Kamil Adryjanek is correct, it is going to be much easier if you change it from an entity to a choice field. I've done some testing, both with FOSUserBundle and without the bundle - in both cases I hit some interesting road blocks.
First, I tried to run it through QueryBuilder in a repository, that didn't work out as it should have. The reason being, the fact that you wanted to be returning an array instead of a ORM object causes an error.
So next, I started looking at creating the choice field. All the guides, say to use the fieldname role instead of roles so I tried that, but I then had to duplicate the UserInterface from FOSUserBundle - I didn't want to do that -- so here I am stressed, and trying to figure it out.
Here is what I ended up doing, which works well.
private $normalRoles = array();
then in the __construct I add: $this->normalRoles = $roles;
Here is the builder:
$builder
->add('roles', 'choice', array(
'multiple' => true,
'choices' => $this->normalRoles
))
;
Originally, I left the multiple part out, figuring that it'd at least let me see an option box. I ended up getting an Array to String conversion error. So, adding the 'multiple' => true in, fixes that error.
Then, in my repository I created a function called normalizeRoles
public function normalizeRoles() {
$data = array();
$qb = $this->getEntityManager();
$query = $qb->createQuery(
"SELECT r.internalName, r.displayName FROM AcmeUserBundle:Roles r"
)->getArrayResult();
foreach ($query as $k => $v) {
$data[$v['internalName']] = $v['displayName'];
}
return $data;
}
From here, we just have to make some small edits in the DefaultController of the UserBundle in the newAction and editAction ( both are the same changes )
So, first off is to put into your Controller use Acme/UserBundle/Entity/Roles in order to avoid any errors and be able to get that repository.
Next, right before you create the form you run the normalizeRoles() function
$roles = $em->getRepository('AcmeUserBundle:Roles')->normalizeRoles()
Then, you pass it through the construct via: new UserType($roles)
full line for that would look like this:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.edit', array(
'id' => $id)
)
));
or for new:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.new')
)
));
At this point -- You'll have a working system that will allow you to dynamically add roles into a database table, and then associate those with a new or current user.
You can try do it via query_builder attribute:
$builder->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
'query_builder' => function (RolesRepository $queryBuilder) {
return $queryBuilder->someMethod() // some method in this repository that return correct query to db.
},
));
In this case it would be better to use choice field Type (http://symfony.com/doc/current/reference/forms/types/choice.html) instead of entity and pass some role choices as option to form because entity field Type get entity id as key for choices:
public function buildForm(FormBuilderInterface $builder, array $options) {
...
$builder->add('roles', 'choice', array(
'choices' => $options['role_choices']
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ADS\UserBundle\Entity\User',
'role_choices' => array()
));
}
Notice: it's recommended to pass variables to form through options parameter, not in constructor.
if I understand your question correctly, you need a data transformers. They help you to show data in form as you want.
Documentation: http://symfony.com/doc/current/cookbook/form/data_transformers.html

Validating dynamically loaded choices in Symfony 2

I have a choice field type named *sub_choice* in my form whose choices will be dynamically loaded through AJAX depending on the selected value of the parent choice field, named *parent_choice*. Loading the choices works perfectly but I'm encountering a problem when validating the value of the sub_choice upon submission. It gives a "This value is not valid" validation error since the submitted value is not in the choices of the sub_choice field when it was built. So is there a way I can properly validate the submitted value of the sub_choice field? Below is the code for building my form. I'm using Symfony 2.1.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parent_choice', 'entity', array(
'label' => 'Parent Choice',
'class' => 'Acme\TestBundle\Entity\ParentChoice'
));
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => array(),
'virtual' => true
));
}
To do the trick you need to overwrite the sub_choice field before submitting the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$parentChoice = $event->getData();
$subChoices = $this->getValidChoicesFor($parentChoice);
$event->getForm()->add('sub_choice', 'choice', [
'label' => 'Sub Choice',
'choices' => $subChoices,
]);
});
}
this accept any value
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if(is_array($data['tags']))$data=array_flip($data['tags']);
else $data = array();
$event->getForm()->add('tags', 'tag', [
'label' => 'Sub Choice',
'choices' => $data,
'mapped'=>false,
'required'=>false,
'multiple'=>true,
]);
});
Adding an alternate approach for future readers since I had to do a lot of investigation to get my form working. Here is the breakdown:
Adding a "New" option to a dropdown via jquery
If "New" is selected display new form field "Custom Option"
Submit Form
Validate data
Save to database
jquery code for twig:
$(function(){
$(document).ready(function() {
$("[name*='[custom_option]']").parent().parent().hide(); // hide on load
$("[name*='[options]']").append('<option value="new">New</option>'); // add "New" option
$("[name*='[options]']").trigger("chosen:updated");
});
$("[name*='[options]']").change(function() {
var companyGroup = $("[name*='[options]']").val();
if (companyGroup == 'new') { // when new option is selected display text box to enter custom option
$("[name*='[custom_option]']").parent().parent().show();
} else {
$("[name*='[custom_option]']").parent().parent().hide();
}
});
});
// Here's my Symfony 2.6 form code:
->add('options', 'entity', [
'class' => 'Acme\TestBundle\Entity\Options',
'property' => 'display',
'empty_value' => 'Select an Option',
'mapped' => true,
'property_path' => 'options.optionGroup',
'required' => true,
])
->add('custom_option', 'text', [
'required' => false,
'mapped' => false,
])
To handle the form data we need to use the PRE_SUBMIT form event.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (isset($data['options']) && $data['options'] === 'new') {
$customOption = $data['custom_option'];
// todo: handle this better on your own
if (empty($customOption)) {
$form->addError(new FormError('Please provide a custom option'));
return;
}
// Check for a duplicate option
$matches = $this->doctrine->getRepository('Acme\TestBundle\Entity\Options')->matchByName([$customOption]);
if (count($matches) > 0) {
$form->addError(new FormError('Duplicate option found'));
return;
}
// More validation can be added here
// Creates new option in DB
$newOption = $this->optionsService->createOption($customOption); // return object after persist and flush in service
$data['options'] = $newOption->getOptionId();
$event->setData($data);
}
});
Let me know if ya'll have any questions or concerns. I know this might not be the best solution but it works. Thanks!
you cannot not build the sub_choice validation because during you config its validator you don't know which values are valid (values depend on value of parent_choice).
What you can do is to resolve parent_choice into entity before you make new YourFormType() in your controller.
Then you can get all the possible values for sub_choice and provide them over the form constructor - new YourFormType($subChoice).
In YourFormType you have to add __construct method like this one:
/**
* #var array
*/
protected $subChoice = array();
public function __construct(array $subChoice)
{
$this->subChoice = $subChoice;
}
and use provided values in form add:
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => $this->subChoice,
'virtual' => true
));
Suppose for sub choices you have id's right ?
Create and empty array with a certain number of values and give it as a choice
$indexedArray = [];
for ($i=0; $i<999; $i++){
$indexedArray[$i]= '';
}
then 'choices' => $indexedArray, :)

Resources