I'm trying to make my firsts unit tests and functionnal tests with Symfony3 / PHPunit and I don't find solutions to my problem.
To be simple, I'm trying to Unit Test a form created to manage Users and Groups (provided by FOSUserBundle)
I followed the steps of the Symfony documentation to test my form, and I already solved some issues concerning getExtensions(), etc... to be able to run my test without any error
Here is my UserType.php :
<?php
...
class UserType extends BaseCrudForm
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('username', null, array('label' => 'label.username'))
->add('email', EmailType::class, array(
'label' => 'label.email',
'constraints' => array(
new Email()
)
))
->add('enabled', CheckboxType::class, array('label' => 'label.enabled', 'required' => false))
->add('groups', EntityType::class, array(
'class' => 'Acme\AdminBundle\Entity\Group',
'multiple' => true,
'required' => false,
))
->add('avatarImageFile', FileType::class, array('label' => 'label.avatar', 'required' => false))
;
}
...
}
and my UserTypeTest.php (I left my tests into it) :
<?php
...
class UserTypeTest extends BaseTypeTestCase
{
public function testSubmitValidData()
{
$formData = array(
'username' => 'nathalie_' . time(),
'email' => 'nathalie_' . time() . '#portman.fr',
'enabled' => true,
'plainPassword' => 'pa$$word',
///// SOLUTION 1
'groups' => array(
$this->em->getRepository('AcmeAdminBundle:Group')->find(1)
),
///// SOLUTION 2
'groups' => array(1),
);
$form = $this->factory->create(UserType::class);
$user = User::fromArray($formData);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
// dump($user, $form->getData());
$this->assertEquals($user, $form->getData()); //Fails
$view = $form->createView();
$children = $view->children;
foreach(array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}
Problem :
The assert $this->assertEquals($user, $form->getData()); fails, because of the collection User::groups that is not equal
Solution 1 is not working (see code above)
The collection of $user is ok, but the form data is empty
Solution 2 is not working neither (see code above)
The form data is ok, but the $user collection is instantiated with a simple array(1) (sees logic)
Question
How could I instanciate correctly my object User and my form at the same time with the same informations (as it's described in the Symfony documentation a linked in top of this topic) ?
Note: as I said I'm a newbie with Unit Tests, so don't hesitate to tell me if I miss the point
Edit
Here is the fromArray function, implemented as a Trait in the User entity :
<?php
namespace Acme\AdminBundle\Traits;
use Doctrine\Common\Util\Inflector;
trait FromArrayTrait
{
/**
* #param array $data
* #return self
*/
public static function fromArray(array $data)
{
$object = new self;
foreach ($data as $key => $value) {
$method = 'set' . Inflector::classify($key);
if (method_exists($object, $method)) {
call_user_func(array($object, $method), $value);
}
}
return $object;
}
}
Related
In a Symfony 2.7 project,
let's say we have a form composed of 2 fields 'date'(date) and 'group'(entity), both have their own EventListener attached to, for FormEvents::SUBMIT events.
After the first submit, I'd like to add a new field 'travels' to the form and populate it with the result of a query using the two previous fields as criterias.
How to prevent 'travels' entity field from fetching all 'travel' in the DB and populate it manually after the raised events ?
I certainly miss some comprehension, I'm new to Symfony.
I know I can pass data directly in 'choices' option when creating 'travels' in each event but it would make useless DB calls.
I might count the number of registered events (of interest) and create 'travels' field when last event happens but it seems kind of weird...
Is there a clean solution for this case ?
(excuse for English, not my native language)
<?php
namespace MyBundle\Form;
// use directives...
class TravelRequestsWorklistType extends AbstractType {
private $em;
private $travelRepository;
private $searchQueryBuilder;
public function __construct(EntityManager $em) {
$this->em = $em;
$this->travelRepository = $this->em->getRepository(Travel::class);
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$formFactory = $builder->getFormFactory();
$builder
->add('requestedDate', 'date', array(
'widget' => 'single_text',
'input' => 'datetime',
'format' => 'dd/MM/yyyy',
'attr' => array('class' => 'date'),
'data' => null,
'mapped' => false
))
->add('selectedGroup', 'entity', array(
'class' => 'MyBundle\Entity\Group',
'placeholder' => '',
'mapped' => false,
'multiple' => false,
))
->add('search', 'submit');
$builder->get('requestedDate')->addEventListener(FormEvents::SUBMIT,
$this->onDateCriteriaEvent($formFactory));
$builder->get('selectedGroup')->addEventListener(FormEvents::SUBMIT,
$this->onGroupCriteriaEvent($formFactory));
}
private function onDateCriteriaEvent(FormFactory $ff) {
return
function(FormEvent $event) use ($ff) {
$root = $event->getForm()->getParent();
$requestedDate = $event->getData();
$qb = $this->getQueryBuilder();
$qb->andWhere('r.requestedDate = :requestedDate')
->setParameter('requestedDate', $requestedDate);
if(!$this->searchHasResult($root)) {
$this->addTravels($ff, $root);
}
};
}
private function onGroupCriteriaEvent(FormFactory $ff) {
return
function(FormEvent $event) use ($ff) {
$root = $event->getForm()->getRoot();
$selectedGroup = $event->getData();
$qb = $this->getQueryBuilder();
$qb->andWhere('r.group = :group')
->setParameter('group', $selectedGroup);
if(!$this->searchHasResult($root)) {
$this->addTravels($ff, $root);
}
};
}
private function addTravels(FormFactory $ff, Form $rootForm) {
$travels = $ff->createNamedBuilder('travels', 'entity', null,
array(
'class' => 'MyBundle\Entity\Travel',
'mapped' => false,
'multiple' => true,
'expanded' => true,
'auto_initialize' => false
));
$submitButton = $ff->createNamedBuilder('validate', 'submit');
$travels->addEventListener(FormEvents::PRE_SUBMIT, $this->onSearchResult());
$form->add($travels->getForm())->add($submitButton->getForm());
}
// The method setData() shows "This form should not contain extra fields"
private function onSearchResult() {
return
function(FormEvent $e) {
$data = $this->searchResultQueryBuilder->getQuery()->getResult();
$e->setData($data);
};
}
private function getQueryBuilder() {
if(null === $this->searchQueryBuilder) {
$this->searchResultQueryBuilder = $this->travelRepository->createQueryBuilder('r');
// add dummy where clause here in order to call "andWhere" indistinctly later
$this->searchResultQueryBuilder->where("1 = 1");
}
return $this->searchQueryBuilder;
}
private function searchHasResult(Form $form) {
return $form->has('travels');
}
}
Finally I found a piece of solution.
On $formField->addEventListener() we can set a priority order as the third argument. So I can know which event will be triggered the last.
There must be another solution (like registering all the events I want to monitor in some member array and check if they have been executed in each callback). For the moment setting hardcoded events priority without checking callbacks execution is OK (for only 2 events).
The field is populated during creation and not after but anyway, it works :).
public function buildForm(FormBuilderInterface $builder, array $options) {
$formFactory = $builder->getFormFactory();
$builder
->add('requestedDate', 'date', array(
'widget' => 'single_text',
'input' => 'datetime',
'format' => 'dd/MM/yyyy',
'attr' => array('class' => 'date'),
'data' => null,
'mapped' => false
))
->add('selectedGroup', 'entity', array(
'class' => 'MyBundle\Entity\Group',
'placeholder' => '',
'mapped' => false,
'multiple' => false,
))
->add('search', 'submit');
// Here we could use a register method which would contain listener informations
$builder->get('requestedDate')
->addEventListener(FormEvents::SUBMIT,
$this->onDateCriteriaEvent($formFactory),
0); //Will be triggered first
$builder->get('selectedGroup')
->addEventListener(FormEvents::SUBMIT,
$this->onGroupCriteriaEvent($formFactory),
1); //Will be triggered after 1st
}
private function onDateCriteriaEvent(FormFactory $ff) {
return
function(FormEvent $event) use ($ff) {
$root = $event->getForm()->getParent();
$qb = $this->getQueryBuilder();
if(null !== $event->getData()) {
$requestedDate = $event->getData();
$qb->andWhere('r.requestedDate = :requestedDate')
->setParameter('requestedDate', $requestedDate);
// Here we could check for registered events not already raised
// and do the appropriate action (if it is the last or something else...)
}
};
}
private function onGroupCriteriaEvent(FormFactory $ff) {
return
function(FormEvent $event) use ($ff) {
$qb = $this->getQueryBuilder();
$root = $event->getForm()->getRoot();
if(null !== $event->getData()) {
// Check for events not already raised....
$selectedGroup = $event->getData();
$qb->andWhere('r.group = :group')
->setParameter('group', $selectedGroup);
}
$travels = $qb->getQuery()->getResult();
if($this->searchHasResult($root) {
// We know this event is the last raised so we can add 'travels' field
$this->addTravels($ff, $root);
}
};
}
private function addTravels(FormFactory $ff, Form $rootForm) {
$travels = $ff->createNamedBuilder('travels', 'entity', null,
array(
'class' => 'MyBundle\Entity\Travel',
'mapped' => false,
'multiple' => true,
'expanded' => true,
'auto_initialize' => false
));
$submitButton = $ff->createNamedBuilder('validate', 'submit');
$travels->addEventListener(FormEvents::PRE_SUBMIT, $this->onSearchResult());
$form->add($travels->getForm())->add($submitButton->getForm());
}
I'm trying to create a web application with Silex.
For my application I have two objects : Project() and Credential().
The Project() one :
protected function buildDomainObject($row)
{
$credential = new Credential();
$credential->setIdCred($row['idCred']);
$credential->setNameCred($row['nameCred']);
$credential->setToken($row['token']);
$project = new Project();
$project->setId($row['id']);
$project->setName($row['name']);
$project->setBranch($row['branch']);
$project->setCredential($credential);
$project->setComment($row['comment']);
$project->setAlive($row['alive']);
$project->setNumberTaskList($row['numberTaskList']);
return $project;
}
And the Credential one :
protected function buildDomainObject($row)
{
$credential = new Credential();
$credential->setIdCred($row['idCred']);
$credential->setNameCred($row['nameCred']);
$credential->setToken($row['token']);
return $credential;
}
As you can see Project() contains Credential() in the value credential.
There are no issues when passing a new Project() object to FormBuilder.
public function addProjectAction(Request $request, Application $app)
{
$credentials = $app['credential_repository']->findAllAsArray();
$project = new Project();
$projectForm = $app['form.factory']->create(new ProjectType(), $project, ['credentialChoices' => $credentials]);
$projectForm->handleRequest($request);
if ($projectForm->isSubmitted() && $projectForm->isValid()) {
$app['project_repository']->save($project);
}
return $app['twig']->render('projectList_form.html.twig', array(
'title' => 'New project',
'legend' => 'New project',
'projectForm' => $projectForm->createView(),
)
);
}
The problem occurs when I try and fetch a Project() from the database and pass it into FormBuilder.
$credentials = $app['credential_repository']->findAllAsArray();
$project = $app['project_repository']->find($id);
$projectForm = $app['form.factory']->create(new ProjectType(), $project, ['credentialChoices' => $credentials]);
I have the following error :
The value of type "object" cannot be converted to a valid array key.
I think my problem comes from the fact that the Project() object has a property containing the Credential() object.
I was able to recreate the exception by using the object property in the buildForm method.
InvalidArgumentException in ArrayKeyChoiceList.php line 71: The value of type "object" cannot be converted to a valid array key.
You can't use credential property with the choice form field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('credential', 'choice', array(
'choices' => $choices,
'multiple' => false,
'expanded' => false
))
->add('submit', 'submit')
;
}
You could try adding a credential id property to Project().
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('credentialId', 'choice', array(
'choices' => $choices,
'multiple' => false,
'expanded' => false
))
->add('submit', 'submit')
;
}
Or you might need to setup the entity field type as explained in this stackoverflow question
Hi i am tying pass array collection (method getProjects() returns it) to form (select input) and fail. This code returns exception - A "__toString()" method was not found on the objects of type "Tasker\WebBundle\Entity\Project" passed to the choice field.
Can anybody help? Is needed transformer? Or what is right way?
Controller:
/**
* #Route("/pridaj", name="web.task.add")
* #Template()
*/
public function addAction(Request $request)
{
$task = new Task;
/** #var User $loggedUser */
$loggedUser = $this->get('security.token_storage')->getToken()->getUser();
$form = $this->createForm(new AddTaskType(), $task, ['user' => $loggedUser]);
if ($form->handleRequest($request) && $form->isValid()) {
// some stuff
}
return [
'form' => $form->createView()
];
}
Form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('project', 'entity', [
'label' => 'Projekt:',
'class' => 'TaskerWebBundle:Project',
'choices' => $options['user']->getProjects(),
'placeholder' => 'Označte projekt',
])
// ....
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array(
'user',
));
$resolver->setDefaults(array(
'user' => null,
));
}
just add __ToString() to your Project class
Tasker\WebBundle\Entity\Project
class Project
{
....
function __toString() {
return $this->getName(); //or whatever string you have
}
}
I wanted to add another answer, because you do not have to add __toString() to your Project class. The Symfony entity field type allows you to specify which property/field to use for displaying. So instead of __toString() you could specify the property in the form configuration like so:
$builder
->add('project', 'entity', [
'label' => 'Projekt:',
'class' => 'TaskerWebBundle:Project',
'choices' => $options['user']->getProjects(),
'placeholder' => 'Označte projekt',
'property' => 'name'
])
If you check this part of the Symfony documentation you will see that __toString() is automatically called only if you do not specify the property.
I am trying to implement a one to one relation. Each User can be affected at a building (Etablissement for me). A building can have many people but each people can be affected at one building at most.
I have this error :
ContextErrorException: Catchable Fatal Error: Argument 1 passed to Intranet\UserBundle\Entity\User::setUserEtab() must be an instance of Intranet\RhBundle\Entity\Etablissement, array given, called in C:\wamp\www\projet\vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php on line 360 and defined in C:\wamp\www\projet\src\Intranet\UserBundle\Entity\User.php line 322
The line 322 of User.php is :
public function setEtablissement(\Intranet\RhBundle\Entity\Etablissement $etablissement = null)
It occurs at the line $form->handleRequest($request); of my controller. This is my controller :
public function editerAction(Request $request, User $user){
$form = $this->createForm(new EditerFormType, $user);
if ($request->getMethod() == 'POST') {
$form->handleRequest($request);
die("ici");
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$this->get('session')->getFlashBag()->add('success', "L'utilisateur ". $user->getNom() ." ". $user->getPrenom() . " a été édité avec succès !");
$em->flush();
return $this->redirect($this->generateUrl('intranet_rh_homepage'));
}else
$this->get('session')->getFlashBag()->add('danger', "Erreur de formulaire !");
}
return $this->render('IntranetRhBundle:User:editer.html.twig',array('user' => $user, 'form' => $form->createView()));
}
The die doesn't work. Before adding the form, it was working.
This is the EditForm :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', 'email', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
->add('nom', 'text')
->add('prenom', 'text')
->add('naissance','date',array(
'widget' => 'single_text',
'format' => 'dd/MM/yyyy',
'attr' => array('class' => 'date', 'readonly' => 'readonly')
))
->add('sexe', 'choice', array(
'choices' => array('Homme' => 'Homme', 'Femme' => 'Femme'),
'multiple' => false
))
->add('etablissement', new UserEtabType())
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Intranet\Userbundle\Entity\User'));
}
This is my UserEtab form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('etablissement', 'entity', array('class' => 'IntranetRhBundle:Etablissement', 'property' => 'nom', 'empty_value' => 'Aucun', 'empty_data' => -1, 'required' => false))
;
}
And my relation on User entity :
/**
* #ORM\OneToOne(targetEntity="Intranet\RhBundle\Entity\Etablissement", cascade={"persist", "remove"})
**/
private $etablissement;
The form's view is okay, there is a select list with all the building and an empty value. But when I post, I have this error I can't understand and solve.
I have the setter and getter if User entity :
/**
* Set Etablissement
*
* #param \Intranet\RhBundle\EntityEtablissement $etablissement
* #return User
*/
public function setEtablissement(\Intranet\RhBundle\Entity\Etablissement $etablissement = null)
{
$this->etablissement = $etablissement;
return $this;
}
/**
* Get Etablissement
*
* #return \Intranet\RhBundle\Entity\Etablissement
*/
public function getEtablissement()
{
return $this->etablissement;
}
But when I var_dump $request->get('user')->get('etablissement') it doesn't work :
Error: Call to undefined method Intranet\UserBundle\Entity\User::get() in C:\wamp\www\projet\src\Intranet\RhBundle\Controller\UserController.php line 69
And the building doesn't appear in var_dump of $request->get('user').
EDIT :
I don't have a setDefaultOptions method for my UserEtabType because I don't know what I have to do. I tried to implements it but I have this kind of error :
The form's view data is expected to be an instance of class Intranet\UserBundle\Entity\User, but is an instance of class Proxies__CG__\Intranet\RhBundle\Entity\Etablissement. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms an instance of class Proxies__CG__\Intranet\RhBundle\Entity\Etablissement to an instance of Intranet\UserBundle\Entity\User.
For the new UserEtabType :
namespace Intranet\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserEtabType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('etablissement', 'entity', array('class' => 'IntranetRhBundle:Etablissement', 'property' => 'nom', 'empty_value' => 'Aucun', 'empty_data' => -1, 'required' => false))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Intranet\UserBundle\Entity\User'));
}
public function getName()
{
return 'intranet_userbundle_useretablissementtype';
}
}
Can you show the new UserEtabType() class ?
EDIT 1 : Try putting this line :
->add('etablissement', 'entity', array('class' => 'IntranetRhBundle:Etablissement', 'property' => 'nom', 'empty_value' => 'Aucun', 'empty_data' => -1, 'required' => false));
in the main EditForm
EDIT 2:
plus be careful :
$resolver->setDefaults(array('data_class' => 'Intranet\UserBundle\Entity\User'));
must be
$resolver->setDefaults(array('data_class' => 'Intranet\UserBundle\Entity\Etablissement'));
in your UserEtabType() class
Are you getting this error when you try to insert a record ? Or before that. For your information, setUserEtab() in the User entity expects an object of Etablissement, because of the one-to-one relationship. Passing the value of an array is not sufficient. Try passing the object of Etablissement it self (the selected object after performing dql).
Hope this helps someone.
Cheers!
I'm using entity choice list in my form. I want to use only specific entities (in example: only groups that user belongs to)
So, in controller, I'm getting these groups, and trying to pass them into formBuider.
Controller:
/.../
$groups = $em->getRepository('VendorMyBundle:Group')->getUserGroups($user);
$form = $this->createForm(new Message($groups), $message);
/.../
so, what now? how to use it in formBuilder?
how to change this line to use passed array of groups?
->add('group','entity',array('class' => 'Vendor\MyBundle\Entity\Group', 'label'=>'Group:'))
or in the other way:
class MessageType
{
/.../
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('group','entity',
array(
'class' => 'Vendor\MyBundle\Entity\Group',
'property' => 'name',
'query_builder' => function ($repository) {
$qb = $repository->createQueryBuilder('group');
$qb->add('where', 'group.administrator = :user');
$qb->setParameter('user', $user->getId());
return $qb;
},
'label' => 'Group'
)
)
// Continue adding fields
;
}
/.../
}
so how can i get object $user to use in form builder? ($user represent current logged user)
You can give the object you want to use in the __construct() method.
Eg :
$form = $this
->get('form.factory')
->create(new ApplyStepOneFormType($this->company, $this->ad), $applicant);
In your form type :
function __construct(\Your\Bundle\Entity\Company $company, \DYB\ConnectBundle\Entity\Ad $ad) {
$this->company = $company;
$this->ad = $ad;
}
And then in your form type in buildForm method :
$company = $this->company;
$builder->add('ad', 'entity', array(
'class' => '\Your\Bundle\Entity\Ad',
'query_builder' => function(\Your\Bundle\Repository\AdRepository $er) use ($company) {
return $er->getActiveAdsQueryBuilder($company);
},
));
//In controller pass the value which you want to use in builder form in array like
$object = new Question();
$form->create(new QuestionType() , $object , array('sqtname'=>2,'question_type'=>2));
//In Form type class
public function buildForm(FormBuilderInterface $builder , array $options)
{
//for setting data field dynamically
if (array_key_exists('question_type', $options) && $options['question_type'] != '') {
$data = $em->getReference("RecrutOnlineStandardBundle:StdQuestionType",$options['question_type']->getId());
} else {
$data = "";
}
$builder->add('StdQuestionType', 'entity', array(
'class' => 'TestStandardBundle:StdQuestionType',
'property' => 'name',
'empty_value' => 'Sélectionner un question type',
'required' => true,
'data' => $data,
'query_builder' => function(EntityRepository $er ) use ( $options ) {
if (isset($options['sqtname']) && $options['sqtname'] != '') {
return $er->createQueryBuilder('sqt')
->where("sqt.name!= ".$options['sqtname']);
} else{
return $er->createQueryBuilder('sqt');
}
}
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Test\QuestionBundle\Entity\Question',
'required' => false,
'sqtname' => '',
'question_type' =>''
));
}
Bacteries' solution IS NOT a good one. For example, if you declare your type as service, it is impossible to pass an object to constructor.
A perfect solution is options - just pass data as option to form builder.
If you want to use custom query, you have to set query_builder option as follows:
use Doctrine\ORM\EntityRepository;
...
$message = new Message();
$form = $this->createFormBuilder($message)
->add('group', 'entity', array(
'class' => 'Vendor\MyBundle\Entity\Group',
'label'=>'Group:',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('g')
->... // whatever you want to do
}
))
->getForm();
You can find more info about query builder in Doctrine manual and about options for entity in Symfony2 manual.
Bacteries' solution is a real good one. Just a note to save headache to other guy like me :)
In this part may I point out the use ($company) part.
It was hidden by the frame and of course nothing works properly without it.
$builder->add('ad', 'entity', array(
'class' =>
'\Your\Bundle\Entity\Ad',
'query_builder' =>
function(\Your\Bundle\Repository\AdRepository $er) use ($company) {
return $er->getActiveAdsQueryBuilder($company);
},
)
);
Best way (my opinion) is give to your form entityManager and select all you need in it. But don't forget to declare empty key in setDefaults() otherwise data won't pass to your builder.
Something like this one
public function buildForm(FormBuilderInterface $builder, array $options)
{
$options['em']->getRepository(''); // select all you need
$builder->add('title', 'text')
->add('content', 'textarea');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Main\BlogBundle\Entity\Post',
'validation_groups' => array('post'),
'required' => false,
'em' => null // this var is for your entityManager
));
}
Apply EM as simple option...