Update
I found in the Symfony docs the answer and an example tutorial to my case: http://symfony.com/doc/current/reference/forms/types/collection.html#basic-usage
How can I assign multiple email fields to a form in Symfony 3.1?
In my entity I have:
/**
* #var array
* #ORM\Column(name="notification_emails", type="array", nullable=true)
*/
private $notificationEmails;
/**
* #return array
*/
public function getNotificationEmails()
{
return $this->notificationEmails;
}
/**
* #param array $notificationEmails
*/
public function setNotificationEmails($notificationEmails)
{
$this->notificationEmails = $notificationEmails;
}
In my form I have:
$builder->add(
'notificationEmails',
CollectionType::class,
array(
'entry_type' => EmailType::class,
'label' => 'Add more emails separated by comma',
'attr' => array(
'multiple' => 'multiple',
),
)
);
But this doesn't work :(
You need Collection type field, only if you are providing multiple email fields for each one email; with some help of javascript and having Add new email button.
If users are going to add emails separated by comma, you don't need a Collection Type field. Keep it simple Text type field with help text as you have done.
Now, in setter [setNotificationEmails], you should split the csv email string to array and feed to ORM. ORM does the rest in order to save in the database.
You should do the vice-versa in getter [getNotificationEmails] (converting array to string). So your form can represent comma separated emails.
In case above doesn't work, as I doubt, the form might not read data from getter. In that case, you can always use a Transformer. It's so useful.
Related
I'm new to Symfony and I want to do something using symfony 4. To simplify it, let's say for example I have a shopping basket where I can add or remove articles AND select the amount of each article I've chosen.
So on the doctrine side I have three entities :
class Basket {
protected $id;
protected $name;
}
class Article{
protected $id;
protected $name;
}
class Buying {
//ManyToOne
protected $basket;
//ManyToOne
protected $article;
protected $count;
}
I've done this form by making the HTML by hand and using some nasty JS code, but now I'd like to make this using Symfony 4's Forms.
I thought the best way would be to create my own form type for that "Buying" entity which would have two fields, one of them being a Select containing every articles, and the other being the $count value, and then have the possibility to add as many "Buyings" as I want, but I can't think of a way to do this and the documentation don't seem to cover this kind of case.
You'd need a couple of form types for this and you may need to jiggle your entities a bit. Here's the gist of it:
First you need one for each individual item purchased and its count. The EntityType will give you a select with all your articles, exactly what you are looking for.
// BuyingType.php
$builder->add('article', EntityType::class, ['class' => Article::class]);
$builder->add('count', NumberType::class, [
'constraints' => [
new Count(['min' => 1]),
],
]);
The second one will be a CollectionType representing the whole basket.
// BasketType.php
$builder->add('basket', CollectionType::class, [
'entry_type' => BuyingType::class,
'allow_add' => true,
'allow_delete' => true,
]);
I have tried looking around for a possible solution to this but with no luck.
What I have is a Many to many relationship between properties and postcodes, I can't display the postcodes in a select for example due to the amount of possible entries.
My solution was to have it as a text field in the form and then catch it on PrePersist to search for the matching record and then apply this to the entity before persisting to the db.
The problem is when the form is validating it is still trying to pass the string value to the setter which is expecting an entity object.
Is there anyway to prevent this from causing an error?
I have attached my form code for you.
Thanks,
Harry
$propertyData = new PropertyData();
$builder
->add('reference')
->add('listing_type', 'choice', array('choices' => $propertyData->getListingTypes()))
->add('listing_status', 'choice', array('choices' => $propertyData->getStatusList()))
->add('title')
->add('house_number')
->add('address_line_1')
->add('address_line_2')
->add('town', 'text', array('data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilTown'))
->add('county')
->add('country')
->add('council')
->add('region')
->add('postcode', 'text', array('data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilPostcode'))
->add('short_description')
->add('long_description')
->add('size_sq_ft')
->add('floor_level')
->add('property_age')
->add('tenure_type', 'choice', array('choices' => $propertyData->getTenureTypes()))
->add('garage')
->add('num_living_rooms')
->add('num_bathrooms')
->add('num_bedrooms')
->add('num_floors')
->add('num_receptions')
->add('property_type')
//->add('prices')
;
You need a data transformer to convert your string input to an entity before processing the form.
$builder
// ...
->add('postcode', 'text', array(
'data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilPostcode'
))
// ...
;
$builder->get('postcode')->addModelTransformer(new CallbackTransformer(
//Render an entity to a string to display in the text input
function($originalInput){
$string = $originalInput->getPostcode();
return $string;
},
//Take the form submitted value and convert it before processing.
//$submittedValue will be the string because you defined
// it in the builder that way
function($submittedValue){
//Do whatever to fetch the postcodes entity:
$postcodeEntity = $entityManager->find('AppBundle\postcodes', $submittedValue);
return $postcodeEntity;
}
));
This is just an example (I haven't tested it), you will need to change some stuff to match how your entities look.
I'm pretty deep into a complex Symfony2 project, with many entities and join tables in forms etc. but I'm having a strange issue with the "multiple" attribute within the form builder.
Basically I have a form where a user can add an illness to the CRM, and each illness can be attached to a specific store (depending on the language used). There is a list of stores that can be chosen by the user within this form, and the values are stored in a join table. However I only want one store to be chosen (i.e. a select drop down) rather than a multiple select list but using the false value for the multiple attribute throws an error which I will outline later.
Firstly, here is the buildForm() code in my Type.php file:
$builder->add('name' , 'text');
$builder->add('description' , 'textarea');
$builder->add('store', 'entity',
array(
'class' => 'AppBundle:Store',
'empty_value' => 'Choose store',
'property' => 'name',
'multiple' => false,
));
$builder->add('save', 'submit', array(
'attr' => array(
'class' => 'btn btn-primary'
),
));
And the entry for the store field in my Entity:
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Store", inversedBy="illness", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="store_id", referencedColumnName="id")
* })
*/
public $store;
However, if I used the false declaration for the multiple attribute in the form Type, when the form is submitted I receive the following error:
Warning: spl_object_hash() expects parameter 1 to be object, string given
because it looks like it's passing the text value of the select box, rather than the relevant Entity. When set as a multiple select box (i.e. set to true) then it works fine and persists as it should.
My controller code:
$addIllness = new Illness();
$form = $this->createForm(new IllnessType($em), $addIllness);
$form->handleRequest($request);
if ($form->isValid()) {
$em->persist($addIllness);
$em->flush();
return $this->redirect($this->generateUrl('app_illness_table'));
}
Having it as a multiple select box is not the end of the world, though I'd rather have it as a drop down select so the user cannot select more than one store - rather than me having to add an error message or note to tell them otherwise.
If anyone has an ideas as to why this may be happening, or has encountered it before please let me know, I would be very grateful!
Thank you
Michael
In Symfony 2.6, I am using an entity form type that is unmapped:
$form
->add(
'myEntity', // Form field name
'entity',
[
'mapped' => false, // Not mapped
'class' => 'MyVendor\MyBundle\Entity\MyEntity',
'choices' => $MyEntityCollection, // list of MyEntity
'property' => 'name',
'empty_value' => 'Please select MyEntity',
'empty_data' => null,
'attr' => [
'label' => 'My label'
]
]
);
This allows user to properly select an item of MyEntity or leave it blank. According to that, I am adding a EventSubscriber to modify the preSubmitted data if any value is selected, and leave it as it is if no choice has been made.
Here is the eventSubscriber:
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'preSubmitData'
];
}
/**
* #param FormEvent $event
*/
public function preSubmitData(FormEvent $event)
{
if( null === ($entity = $event->getForm()->get( 'myEntity' )->getData() ) ){
return;
}
// Set value if field has been defined
$event
->getForm()
->setData( $entity )
;
}
If user selects a choice other than blank, when I debug the preSubmitData function:
$event->getForm()->get('entity')->getData() gives null
$event->getData() gives an array having as 'entity' key the selected entity ID (just the scalar value)
My questions are:
Shouldn't $event->getForm()->get('entity')->getData() have the selected entity?
Why is $event->getForm()->get('entity')->getData() giving null if $event->getData() has at least the entity ID in it?
Is there any way to get the entity here (as it happens with the mapped entities) without having to call the entity manager and querying the entity via its ID?
Thanks in advance!
Edit
For the big picture, in my global form (other fields not described here) I have 2 depending fields:
A select A (not described here) with some options from a tree. This option does exist in the global form entity as a property.
A second B select named myEntity (described here). It doesn't exist as the global form entity as a property, thus the mapped = false. If any choice is made here, then the first select (A)'s option is overridden by this one. Else the first choice remains as the entity property value.
Hope is clearer now.
Ok, it was giving null because we are on the preSubmit event, and here data sent is not yet mapped within an entity.
Changing the event to submit gives the mapped entity as needed.
I have a form that is the bottleneck of my ajax-request.
$order = $this->getDoctrine()
->getRepository('AcmeMyBundle:Order')
->find($id);
$order = $order ? $order : new Order();
$form = $this->createForm(new OrderType(), $order);
$formView = $form->createView();
return $this->render(
'AcmeMyBundle:Ajax:order_edit.html.twig',
array(
'form' => $formView,
)
);
For more cleaner code I deleted stopwatch statements.
My OrderType has next fields:
$builder
->add('status') // enum (string)
->add('paid_status') // enum (string)
->add('purchases_price') // int
->add('discount_price') // int
->add('delivery_price') // int
->add('delivery_real_price', null, array('required' => false)) // int
->add('buyer_name') // string
->add('buyer_phone') // string
->add('buyer_email') // string
->add('buyer_address') // string
->add('comment') // string
->add('manager_comment') // string
->add('delivery_type') // enum (string)
->add('delivery_track_id') // string
->add('payment_method') // enum (string)
->add('payment_id') // string
->add('reward') // int
->add('reward_status') // enum (string)
->add('container') // string
->add('partner') // Entity: User
->add('website', 'website') // Entity: Website
->add('products', 'collection', array( // Entity: Purchase
'type' => 'purchase',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'property_path' => 'purchases',
'error_bubbling' => false,
));
Purchase type:
$builder
->add('amount')
->add('price')
->add('code', 'variant', array(
'property_path' => 'variantEntity',
'data_class' => '\Acme\MyBundle\Entity\Simpla\Variant'
))
;
Also Purchase type has a listener that is not significant here. It is represented in Symfony profiler below as variant_retrieve, purchase_form_creating. You can see that it takes about 200ms.
Here I put the result of profilers:
As you can see: $this->createForm(...) takes 1011ms, $form->createView(); takes 2876ms and form rendering in twig is also very slow: 4335ms. As stated by blackfire profiler all the deal in ObjectHydrator::gatherRowData() and UnitOfWork::createEntity().
Method createEntity() called 2223 times because there is some field that mapped with Variant entity and has form type Entity. But as you can see from above code there is no entity types for variant. My VariantType is simple extended text form type that has modelTransformer. To not mess up everything you can see code for similar Type class at docs.
I found with XDebug that buildView for VariantType has been called in Purchase's buildView with text form type. But after that from somewhere buildView for VariantType was called again and in this case it has entity form type. How can it be possible? I tried to define empty array in choices and preferred_choices on every my form type but it didn't change anything. What I need to do to prevent EntityChoiceList to be loaded for my form?
The described behavior looks as the work of the guesser. I have the feeling that there is need to show an some additional code (listeners, VariantType, WebsiteType, PartnerType).
Let's assume a some class has association variant to Variant and FormType for this class has code ->add('variant') without explicit specifying type (as I see there is a lot of places where the type is not specified). Then DoctrineOrmTypeGuesser comes in the game.
https://github.com/symfony/symfony/blob/2.7/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php#L46
This code assign the entity type (!) to this child. The EntityRepository::findAll() is called and all variants from DB are hydrated.
As for another form optimization ways:
Try to specify type in all possible cases to prevent a type guessing;
Use SELECT with JOINs to get an order as new sub-requests to DB are sent to set an underlying data for an every form maps relation;
Preserve keys for collection elements on a submission as a removing of a single element without a keys preserving will trigger unnecessary updates.
I also had the same problem with the entity type, I needed to list cities, there were like mire then 4000, what I did basically is to inject the choices into the form. In your controller you ask the Variants from the database, in a repository call, hydrate them as array, and you select only the id and the name, or title, and then you pass into the form, as options value. With this the database part will be much quicker.