I want to test Form types from Symfony2. I have a custom form type and my test looks like this:
/**
* #param \Acme\UserBundle\Entity\User $user
*/
function let(\Acme\UserBundle\Entity\User $user)
{
$this->beConstructedWith($user);
}
function it_is_initializable()
{
$this->shouldHaveType('Acme\UserBundle\Form\Type\RegistrationFormType');
}
/**
* #param \Symfony\Component\Form\FormBuilderInterface $builder
*/
function it_builds_form(\Symfony\Component\Form\FormBuilderInterface $builder)
{
$this->buildForm($builder, []);
}
And I get: Fatal error: Call to a member function add() on a non-object In buildForm method I invoke $this->add method from FormBuilderInterface how can I solve this ?
You didn't post your form code, but I suspect that the problem is the fluent interface that the builder's add() method uses. If you have multiple calls to add() like this:
$builder
->add('username')
->add('email')
->add(...)
->add(...)
->add('save', 'submit');
Then the problem will occur after the first add(), because that isn't returning an object (hence the "Call to a member function add() on a non-object" error message).
If you are using the fluent style, you need to "train" the $builder collaborator, so that phpspec/mockery can return the same builder object for successive calls to add():
$builder->add(Argument::any(), Argument::any())->willReturn($builder);
$this->buildForm($builder, []);
I think that Symfony 2 forms are may not be the best candidate for phpspec testing, as you really want to test only the public API for your classes and not to test code that you don't own (i.e. framework/3rd-party libraries).
The form type that you are testing isn't the actual form that is produced, it's more like the "blueprint" used to build the form when it is needed, so I think it's harder to test that a form has certain fields or options, etc. as this isn't called by your code, it happens automatically when the forms framework processes the form type.
The work to create the real form happens inside the builder, which in the context of this form type spec is a collaborator rather than a real builder object (and also is not your code to test).
Related
I have a Symfony Form with a custom ChoiceType that needs to filter the available choices, based on the value of another field (a sibling form-type inside the same form).
I am using EasyAdmin CMS that doesn't allow its fields to pass down additional dynamic type_options to their form-types, because of it YAML configuration. And I'd like the the custom ChoiceFormType to have encapsulate the logic on how to resolve its own options.
But modifying the options based on other form data seems impossible.
form data is NULL inside buildForm(); I can access it just from FormEvents.
inside FormEvents::PRE_SET_DATA I cannot replace the current type with one having the modified options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder, $options) {
$parent = $event->getForm()->getParent();
$parent_data = $parent-getData();
$other_field_value = $parent_data->getOtherFieldValue();
$filtered_choices = array_filter(fn($choice) => $choice !== $other_field_value);
$options['choices'] = $filtered_choices;
$parent->add($form, get_class($this), $options); // THIS FAILS
}
Trying to set the options, resolved dynamically based on the form data, fails with:
A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.
But the $event->setData() works just on the current FormType, not on the the whole form.
What's the pattern to make a FormType set its own options dynamically, based on the value of a sibling form type?
So my question is very specific but I couldn't figure out how to make it even after several months of reflection. The following topic will be about Symfony, Doctrine and generating fixtures on-the-go for the tests.
I want to generate fixtures on the go from a test. The goal is to provide a very specific set of fixtures for each tests using helpers without sacrify the readability. That is the goal, so my idea was to create a tests/Resources/EntityProxy which is a mirror of the src/Entity folder, containing the same amount of classes with the exact same name. Each EntityProxy extends from its related Entity, use a custom trait to fill the properties easily.
You guessed it, I want to only use in tests the EntityProxy and use it directly into the functions to tests them. And there is a major issue with that, as Doctrine doesn't recognize the EntityProxy as an entity even if it extends from a real Entity.
Is there a way to say to Doctrine to persist an EntityProxy as its extended Entity?
__
The following code is an example of what I want as en EntityProxy:
namespace Tests\Resources\EntityProxy;
class User extends App\Entity\User
{
use FixtureGenerationTrait;
public function static makeDefault(): self
{
return static::generate([
'username' => self::getFaker()->username,
'email' => self::getFaker()->email,
...
]);
}
public function static make(array $data = []): self
{
$entity = static::makeDefault();
$entity = static::setValues($entity, $data);
return $entity;
}
}
And can be used in the tests like following: User::make(['name' => 'John Wick']);
I want to create a settings page, which only has a form in it. If the form is submitted it only updates settings entity but never creates another one. Currently, I achieved this like:
/**
* #param SettingsRepository $settingsRepository
* #return Settings
*/
public function getEntity(SettingsRepository $settingsRepository): Settings
{
$settings = $settingsRepository->find(1);
if($settings == null)
{
$settings = new Settings();
}
return $settings;
}
In SettingsController I call getEntity() method which returns new Settings entity (if the setting were not set yet) or already existing Settings entity (if setting were set at least once).
However my solution is quite ugly and it has hardcoded entity id "1", so I'm looking for a better solution.
Settings controller:
public function index(
Request $request,
SettingsRepository $settingsRepository,
FlashBagInterface $flashBag,
TranslatorInterface $translator,
SettingsService $settingsService
): Response
{
// getEntity() method above
$settings = $settingsService->getEntity($settingsRepository);
$settingsForm = $this->createForm(SettingsType::class, $settings);
$settingsForm->handleRequest($request);
if ($settingsForm->isSubmitted() && $settingsForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
$em->flush();
return $this->redirectToRoute('app_admin_settings_index');
}
return $this->render(
'admin/settings/index.html.twig',
[
'settings_form' => $settingsForm->createView(),
]
);
}
You could use Doctrine Embeddables here.
Settings, strictly speaking, should not be mapped to entities, since they are not identifiable, nor meant to be. That is, of course, a matter of debate. Really, a Settings object is more of a value object than an entity. Read here for more info.
So, in cases like these better than having a one to one relationship and all that fuzz, you probably will be fine with a simple Value Object called settings, that will be mapped to the database as a Doctrine Embeddable.
You can make this object a singleton by creating instances of it only in factory methods, making the constructor private, preventing cloning and all that. Usually, it is enough only making it immutable, meaning, no behavior can alter it's state. If you need to mutate it, then the method responsible for that should create a new instance of it.
You can have a a method like this Settings::createFromArray() and antoher called Settings::createDefaults() that you will use when you new up an entity: always default config.
Then, the setSettings method on your entity receieves only a settings object as an argument.
If you don't like inmutablity, you can also make setter methods for the Settings object.
When form is submitted, object's set method (League#setInformation) is called with corresponding data. All is working correctly. (See code below as an example)
I need to pass additional parameters to setInformation, namely current user id which is stored in session data.
That trick would help keeping session and model separate. Maybe useful in different situations too.
Do you know a way to deal with it?
class LeagueFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name');
$builder->add('information', 'collection', [
'type' => new LeagueInformationFormType(),
]);
}
public function setDefaultOptions(\Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver) {
$resolver->setDefaults([
'data_class' => 'xxx\Models\League',
]);
}
public function getName() {
return 'league';
}
}
class League {
public function getInformation() {
//...
}
public function setInformation($data) {
...
}
}
What I would do is declare form as a service, and inject the data from session. If you can, try to refactor your setInformation() function to two functions for example, so you dont have to provide all information through that one. However I think form events will help you set everything as you like.
If you are using Doctrine2 and the League class is actually a Doctrine2 Entity, I would recommend using a Doctrine2 subscriber/listener.
You can configure the subscriber/listener to do something either just before sending the data to the databse (onFlush), just after telling doctrine about a new entity (persist) or just before updating an existing record (update), whichever is the most appropiate in your case.
Inject the SecurityContext (#security.context in your DIC) into the subscriber/listener to pull out the current user information. (Make sure you check there is a user, because the subscriber wil also be run when nobody is logged in and a League object is saved)
The main advantage of this is that is does not pollute your form or controller. And if for some reason you create a League entity some other way the current user wil also be set.
Some docs:
http://doctrine-orm.readthedocs.org/en/latest/reference/events.html
It's a different story if you are not using Doctrine2 though.
I'm sorry for my bad English, I'm not a native speaker. Feel free to correct my text if needed.
The question is really simple (it's in the end of this text), but I've written a rationale with some research and tests.
Rationale
If you want, you can skip this rationale and jump directly to the question itself.
I've been trying for several hours on trying to get a ManyToMany relationship to persist from the inverse side using doctrine:generate:entities and doctrine:generate:crud on Symfony2 console.
From the owning side, the relationship is saved in the database with the generated crud out of the box, but not from the inverse side (this is expected: http://docs.doctrine-project.org/en/2.0.x/reference/association-mapping.html#owning-side-and-inverse-side)
What I want is to make it work from the inverse side as well without changing the autogenerated controllers; I'd like to change only the model (entity).
The easy way would be to add a couple of custom code lines to the controller:
// Controller that works the way I want
// Controller/AlunoController.php
...
public function createAction(Request $request)
{
$entity = new Aluno();
$form = $this->createForm(new AlunoType(), $entity);
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
// begin custom code
foreach ($entity->getResponsaveis() as $responsavel) {
$responsavel->addAluno($entity);
}
// end custom code
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('aluno_edit', array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
...
The previous code works, but it is not what I want, because those custom lines of code refer to the relationship logic, and should be centralized in the Entity itself (if not, it would have to be duplicated all over the controllers that update this entity).
So, what I did next was to change my adders and removers in the Entity file to execute that required logic, adding the code to automatically update both inverse and owning side (as recommended in http://docs.doctrine-project.org/en/2.0.x/reference/association-mapping.html#picking-owning-and-inverse-side and https://stackoverflow.com/a/7045693/1501575)
// Entity/Aluno.php
...
/**
* Add responsaveis
*
* #param \MyBundle\Entity\Responsavel $responsaveis
* #return Aluno
*/
public function addResponsavei(\MyBundle\Entity\Responsavel $responsaveis)
{
/* begin custom code */
//var_dump('addResponsavei');
$responsavel->addAluno($this);
/* end custom code */
$this->responsaveis[] = $responsaveis;
return $this;
}
...
This should work, because it does work when the same code is in the controller, but it actually does not.
The problem is that the method Aluno##addResponsavei() is never being called when $form->bind($request) runs in the controller (first code sample up there) (I realized this with that var_dump() line. I've also put var_dumps in some other getters and those other methods were called as normal).
So, all the regular setters are indeed called inside $form->bind($request), but not this one. This is weird, because the method names were autogenerated by `doctrine:generate:entities', which I assumed would make $form->bind() know how to call all the setters, getters and adders.
Question
Why is $form->bind() not calling the adder method (Aluno##addResponsavei())?
Is there a special naming convention not followed by doctrine:generate:crud that is preventing the method from being found and executed?
Solution
Thanks for the comment from user1452962 and later the answer from Elnur Abdurrakhimov, I've got it to work and it is actually pretty simple.
All I had to do is to add the option 'by_reference' to false in the properties that hold the inverse side of the relationship, and suddenly addResponsavei() began to be called.
// Form/AlunoType.php
...
$builder
->add('nome')
->add('cidade_natal')
->add('nascimento')
->add('email')
->add('endereco')
->add('nome_sem_acento')
->add('data_hora_cadastro')
->add('responsaveis', null, array('by_reference' => false))
->add('turmas', null, array('by_reference' => false))
...
This way, the relationship logic is gone from the controller, and that what I was looking for. Thank you guys.
You need to set the by_reference option to false in order for adders to be called.