Symfony: Multiple money fields in one form - php

Imagine you have an item entity. Within this item entity, there is a method to get the prices for that item. Prices are saved in 3 different formats: EUR, USD and GBP.
The entities would look like this:
Entity WebshopItem.php
class WebshopItem
{
/**
* #var integer
*/
private $id;
/**
* #Gedmo\Translatable
* #var string
*/
private $title;
......
/**
* #var \Doctrine\Common\Collections\Collection
*/
private $prices;
}
Entity WebshopItemPrice.php
class WebshopItemPrice
{
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $currency;
/**
* #var string
*/
private $price;
/**
* #var \WebshopItem
*/
private $webshopItem;
}
Now I would like to create a form, which contains exactly 3 input fields. For that, I thought it would be best to use the money field. So I am creating the form like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
....
->add('prices', new WebshopPricesType());
}
The webshopPricesType looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('eur', 'money', array('currency' => 'EUR', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'))
->add('usd', 'money', array('currency' => 'USD', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'))
->add('gbp', 'money', array('currency' => 'GBP', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'));
}
3 correct fields are rendered now. I only need to fill them on edit and when saving, I have to make sure they are saved. I was thinking about using a data transformer to find the correct entity, but that's not working.
How can I make sure that all 3 fields are prefilled correctly on edit and when clicking save, the 3 prices are saved?
Or should I do it on a whole different way?
Thanks!

I have never been too fond of DataTransformers and therefore I would not use them here, but they can be useful.
In this particular case, I would go for FormEvents and build the form dynamically, based on the data your entity contains.
WebShopItemType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
.....
->add('prices', 'collection', array(
'type' => new WebshopPricesType()
));
}
WebshopPricesType
class WebshopPricesType extends AbstractType{
.....
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Dynamically build form fields, **after** the data has been set
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($builder){
/** #var $data WebshopItemPrice **/
$data = $event->getData();
$builder->add('price', 'money', array('currency' => $data->getCurrency()));
});
}
public function setDefaults(OptionsResolverInterface $resolver){
$resolver->setDefault(array(
'data_class' => 'bundlePath\Entity\WebshopItemPrice'
));
}
.....
}
Given that, let's glue it altogether:
public class SomeController extends Controller{
public function insertAction(){
$item = new WebshopItem();
// be sure to initialize the $prices with new `ArrayCollection`
// in order to avoid NullPointerException
// Also, be sure to bind WebshopItemPrice::$item
$item
->addPrice(new WebshopItemPrice('EUR', 0))
->addPrice(new WebshopItemPrice('USD', 0))
->addPrice(new WebshopItemPrice('GBP', 0));
// this is where POST_SET_DATA gets fired
$form = $this->createForm(new WebShopItemType(), $item);
// form is ready
}
public function editAction(){
$item = ... // fetch or whatever, be sure to fetch prices as well
// this is where POST_SET_DATA gets fired
$form = $this->createForm(new WebShopItemType(), $item);
// form is ready
}
}
I have put this together in Notepad++ and I not sure if I did some typos but as far as logic is concerned - it should work ;)

Related

Symfony 6 - Fail to inject data into a dynamic formtype on POST_SUBMIT

I have a problem with a nested form. I can't get the values I want to pass.
Here is a simple example to reproduce my problem, I would like to pre-fill a form about a user according to the selected house in my form.
Here are the files, if you want to test. I would like to inject the values of roger and billy the good way but my user fields are always empty
The models
class Test
{
/**
* #var string|null
*/
private $house;
/**
* #var TestUser|null
*/
private $user;
// Getters & Setters of course...
}
class TestUser
{
/**
* #var string|null
*/
private $name;
/**
* #var int|null
*/
private $age;
// Getters & Setters again...
}
The main form
class TestType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('house', ChoiceType::class, [
'choices' => [
'first' => 1,
'second' => 2,
],
]
);
$builder->get('house')->addEventListener(FormEvents::POST_SUBMIT, [$this, 'addUser']);
}
function addUser(FormEvent $event)
{
$form = $event->getForm()->getParent();
$house = $event->getForm()->getData();
if (!$house) {
return;
}
// here is the part where I choose the user I want to use
// for the rest of the example (which does not work)
$testUser = $house === 1
? (new TestUser())->setName('roger')->setAge(65)
: (new TestUser())->setName('billy')->setAge(22);
$builder = $form->getConfig()->getFormFactory()->createNamedBuilder('user', TestUserType::class, $testUser, [
'auto_initialize' => false,
]);
$form->add($builder->getForm());
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Test::class,
]);
}
}
The user form type
class TestUserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['label' => 'username'])
->add('age', IntegerType::class, ['label' => 'age']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TestUser::class,
]);
}
}
The controller
public function testForm(Request $request): RedirectResponse|Response
{
// ...
$test = new Test();
$form = $this->createForm(TestType::class, $test);
$form->handleRequest($request);
// ...
}
The view
{{ form_start(formtest) }}
<p>
<button class="btn btn-primary" type="submit">test go</button>
</p>
{{ form_end(formtest) }}
all help is welcome
Is setter actually returning $this?
When ->setAge(65) is executed.
Because it's not clear, it's not in your code you provided here.
you need to link the $user $age $house and $name to the input fields you have.
The reason why you always get an empty output is do to the face that non of the variables refer to any data source.

Symfony 2.7 choice callback not working in form

I have a class with a multiple choice property:
...
/**
* #ORM\Column(type="array", name="majority_types")
* #Constraints\Choice(callback="getAvailableMajorityTypes", multiple="true")
*/
private $majorityTypes;
...
public static function getAvailableMajorityTypes()
{
return array(
self::SIMPLE_MAJORITY,
self::UNANIMITY_MAJORITY,
self::THREE_FIFTHS_MAJORITY,
self::ONE_THIRD_MAJORITY,
self::FOUR_FIFTHS_MAJORITY
);
}
...
I also have a form class for this class:
...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('majorityTypes', ChoiceType::class, array(
'multiple' => true,
))
...
->getForm();
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyClass',
));
}
But the choices from getAvailableMajorityTypes are not rendered.
I simply followed these steps: http://symfony.com/doc/master/reference/constraints/Choice.html#supplying-the-choices-with-a-callback-function, but for some reason it doesn't work.
Edit:
I see that using static choices as annotations neither works (choices={"foo1", "foo2"}). The only way it works is passing the choices directly in the add method when creating the form. I've not found out the problem yet.
If I refer to your words:
But the choices from getAvailableMajorityTypes are not rendered.
It seems you're confused between rendering of options in your form's select field and the Choice constraint.
You've only implemented the constraint in your code, but you also need to add the options to your select. Like this:
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('majorityTypes', ChoiceType::class, array(
'multiple' => true,
'choices' => YourEntity::getAvailableMajorityTypes()
))
I've never used this annotation, however in the documentation the callback is public static:
// src/AppBundle/Entity/Author.php
namespace AppBundle\Entity;
class Author
{
public static function getGenders()
{
return array('male', 'female');
}
}
If you follow the documentation and make your method static as well the annotation should work.

Symfony 2 add dynamic form; values from database

I would like to create a google categories matching(first field categorie from database and second field a user autocomplete field from google categories) form where i have an entity CategoriesConfig :
private $id;
/**
* #var string
*
* #ORM\Column(name="category_site", type="string", length=100)
*/
private $categorySite;
/**
* #var string
*
* #ORM\Column(name="category_google", type="string", length=100)
*/
private $categoryGoogle;
In my Controller i tried this
/**
* #Route("/adminDashboard/categoriesMatching", name="googleShopping_categories")
* #Security("has_role('ROLE_SUPER_ADMIN')")
*/
public function categoriesMatchingAction(Request $request)
{
// create a task and give it some dummy data for this example
$idSite = $this->get('session')->get('_defaultWebSite')->getId();
$categories = $this->getDoctrine()->getRepository('DataSiteBundle:SiteCategory')->findBy(array('IdSite' => $idSite));;
$categories_config = new CategoriesConfig();
//var_dump($categories);exit;
$form = $this->createForm(new CategoriesConfigType($categories), $categories_config);
return $this->render('GoogleShoppingBundle:Default:categoriesMatching.html.twig', array(
'form' => $form->createView()
));
}
And my form type : CategoriesConfigType:
class CategoriesConfigType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
private $site_categories;
public function __construct ($site_categories) {
$this->site_categories = $site_categories;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($this->site_categories as $k => $categorie){
$builder
->add('categorySite')
->add('categoryGoogle');
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
I would like to have as many categories rows as row fields(website itecategorie and google categorie)
The result is like that:
Thank you in advance!
Your loop on $this->categories is uneffective, because the elements you add have the same name each time (categorySite and categoryGoogle), so the FormBuilder replaces the form field each time, instead of adding another one.
However, if you want your form to handle a Collection of CategoryConfigs, you need to take a different approach.
1) Create a CategoriesConfigType (as you did), but who is responsible of only a single CategoriesConfig entity
class CategoriesConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('categorySite')
->add('categoryGoogle');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
2) Then use CollectionType field to manipulate your form as a whole collection of CategoryConfigTypes:
class YourCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('categoriesConfigs', CollectionType::class, array(
'entry_type' => CategoriesConfigType::class,
'entry_options' => array('required' => false)
);
}
}

Load options into drop down on a form based on user's id in symfony 2

I have a question for you and to give you some idea of what i'm doing, i will try and explain the idea. I have a system where user's are able to add geckos to a database, when that gecko is added, it saves the user id into a column called user_id - this works perfect and sets me up for what i am trying to achieve now.
I have a system where user's are able to add weight entries for that gecko, problem is, right now it just loads every gecko in the database, not the one's that are specific to that user.
Here is a portion from my Weight.php entity:
/**
* #ORM\ManyToOne(targetEntity="Gecko", inversedBy="weights")
* #ORM\JoinColumn(name="gecko_id", referencedColumnName="id")
*/
private $geckoId;
Which is linked to this part in the Gecko.php:
/**
* #ORM\OneToMany(targetEntity="Weight", mappedBy="geckoId", cascade={"persist", "remove"})
*/
private $weights;
And here is the user part inside Gecko.php entity which links the current user's id to save to the database:
/**
* #ORM\ManyToOne(targetEntity="Breedr\UserBundle\Entity\User", inversedBy="geckos")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
And the linked part in the User.php entity:
/**
* #ORM\OneToMany(targetEntity="Breedr\GeckoBundle\Entity\Gecko", mappedBy="user", cascade={"persist", "remove"})
*/
protected $geckos;
Now, here is my Weight entities Form (WeightType.php):
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('weighedDate')
->add('weight')
->add('geckoId')
;
}
Which gives you a drop down based on the parts above that look like this:
So what i am now trying to achieve is to ONLY show the geckos that are linked with the current user's ID. What is the best way to achieve this?
Thanks in advance :)
Andy
EDIT:
Here is my WeightType file:
<?php
namespace Breedr\GeckoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WeightType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('weighedDate')
->add('weight')
->add('geckoId')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Breedr\GeckoBundle\Entity\Weight'
));
}
/**
* #return string
*/
public function getName()
{
return 'breedr_geckobundle_weight';
}
}
EDIT 2:
Here is my create form snippet:
private function createCreateForm(Weight $entity)
{
$form = $this->createForm(new WeightType(), $entity, array(
'action' => $this->generateUrl('weight_create'),
'method' => 'POST',
));
$form->add('submit', 'submit', array('label' => 'Create'));
return $form;
}
You must use entity Field Type + query_build option. Thus You can build a custom query in order to filter the results, for instance:
<?php
namespace AppBundle\Form\Type;
use Doctrine\ORM\EntityRepository;
// ...
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
));
On your specific case your form type might looks like something like this:
<?php
# maybe you need to fix some namespaces...
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class WeightType extends AbstractType
{
/** #var int */
protected $currentUserId;
/**
* param int $currentUserId It can be passed from controller
* when creating form instance
*/
public function __construct($currentUserId)
{
$this->currentUserId = $currentUserId;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$id = $this->currentUserId;
$builder->add('users', 'entity', array(
'class' => 'GeckoBundle:Gecko',
'query_builder' => function (EntityRepository $er) use ($id) {
return $er->createQueryBuilder('g')
->where('user_id = ?')
->setParameter(0, $id);
},
));
}
}
On the controller...
<?php
//...
public function add()
{
$currentUserId = $this->getUser()->getId(); # may be it...
$form = $this->createForm(new WeigthType($currentUserId));
}

Symfony2 Form FormType Itself collection

I'm trying to create a symfony 2 form 'PersonType', which has a PersonType collection field that should map a given person's children.
And I'm getting this error,
{"message":"unable to save order","code":400,"errors":["This form should not contain extra fields."]}
Here is my Person entity,
class Person
{
private $id;
/**
* #ORM\OneToMany(targetEntity="Person", mappedBy="parent", cascade={"persist"})
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="children")
* #ORM\JoinColumn(name="orderitem_id", referencedColumnName="id", nullable=true)
*/
private $parent;
}
And my Type,
class PersonType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id')
->add('children', 'collection', array(
'type' => new PersonType()
))
;
}
UPDATE :
I've seen that the problem was because the option :
'allow_add' => true,
'by_reference' => false
wasn't in the Type, I've deleted it because when i insert them, the form don't appear and the page crash with no error.
I'm very confused because with this error, people can't have children :/
Does anyone already faced the same problem? (A formType nested over itself)
ACUTALLY :
I've duplicate my personType to PersonchildrenType to insert this last in the first...
I was having the same problem except that the error message was :
FatalErrorException: Error: Maximum function nesting level of 'MAX' reached, aborting!
Which is normal because "PersonType" is trying to build a form with a new "PersonType" field that is also trying to build a form with a new "PersonType" field and so on...
So the only way I managed to solve this problem, for the moment, is to proceed in two different steps :
Create the parent
Create a child and "link" it to the parent
You can simply do this in your controller
public function addAction(Person $parent=null){
$person = new Person();
$person->setParent($parent);
$request = $this->getRequest();
$form = $this->createForm(new PersonType(), $person);
if($this->getRequest()->getMethod() == 'POST'){
$form->bind($request);
if ($form->isValid()) {
// some code here
return $this->redirect($this->generateUrl('path_to_person_add', array(
'id' => $person->getId()
); //this redirect allows you to directly add a child to the new created person
}
}
//some code here
return $this->render('YourBundle::yourform.html.twig', array(
'form' => $form->createView()
));
}
I hope this can help you to solve your problem.
Tell me if you don't understand something or if I'm completly wrong ;)
Try to register your form as a service, like described here: http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services, and modify your form like this:
class PersonType extends AbstractType
{
public function getName()
{
return 'person_form';
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id')
->add('children', 'collection', array(
'type' => 'person_form',
'allow_add' => true,
'by_reference' => false
))
;
}
}

Categories