Symfony2 Saving data of related Entity after submiting form - php

I'm starting developing with Symfony2 and looks like I need help. I have Product entity related with SynchronizationSetting entity. I can edit product data by form maped with his entity. But I also need to modify some data related to product in SynchronizationSetting. To do that I've modified the form so it look like that (Vendor\ProductBundle\Form\ProductType.php):
...
->add('synchronization_setting', 'choice', array(
'choices' => array('daily' => 'Daily', 'weekly' => 'Weekly', 'never' => 'Never'))
After form is submitted selected checkbox values are passed to setSynchronizationSetting method in Product Entity. Then I do that (Vendor\ProductBundle\Entity\SynchronizationSetting.php):
public function setSynchronizationSetting($data)
{
$synchronizationSetting = new SynchronizationSetting();
$synchronizationSetting->setDaily(in_array('daily', $data) ? '1' : '0');
...
}
And now I need to somehow save those SynchronizationSetting entity into database. I read that calling entity manager from here is very bad practice so... how should I save this?

One possible way (I'm not sure if it's good practice)
public function setSynchronizationSetting($data)
{
$synchronizationSetting = new SynchronizationSetting();
$synchronizationSetting->setDaily(in_array('daily', $data) ? '1' : '0');
}
public function retSynchronizationSetting()
{
return $this->synchronizationSetting;
}
Then in your controller in place where you handle form data you call retSynchronizationSetting() and save entity using EntityManager.

Related

How do I specify default values in a Symfony form

I am trying to specify a default value in a form so when creating an entity the form field has a value (not null or empty). However when the entity is being edited it should obviously show the stored value and not the default.
My entity initializes itself as part of the construction - so when an entity is new and not yet persisted these values should be set.
How do I inform the FormType to use the default value over the persisted state? Everything I try seems to suggest it's one or the other not both?
How is this done Symfony 3.2+???
EDIT |
controller:
public function newAction (Request $request)
{
$quoteItem = new QuoteItem();
$form = $this->createForm('UniflyteBundle\Form\QuoteItemType', $quoteItem, ['allow_extra_fields' => true]);
$form->add('QuoteFlight', QuoteFlightType::class);
}
Form type:
public function configureOptions (OptionsResolver $resolver)
{
$resolver->setDefaults([
//'data' => new \UniflyteBundle\Entity\QuoteFlight()
'data_class' => QuoteFlight::class
]);
}
public function buildForm (FormBuilderInterface $builder, array $options)
{
$builder
->add('specMajorSetupCharge', null, [
//'empty_data' => QuoteFlight::SPEC_MAJOR_SETUP_CHARGE,
'data' => QuoteFlight::SPEC_MAJOR_SETUP_CHARGE,
'label' => '* Setups Charge'
])
// ...
}
http://symfony.com/doc/current/components/form.html#setting-default-values
If you need your form to load with some default values (or you're building an "edit" form), simply pass in the default data when creating your form builder.
$quoteItem = new QuoteItem();
$quoteItem->getQuoteFlight()->setSpecMajorSetupCharge(QuoteFlight::SPEC_MAJOR_SETUP_CHARGE).
$form = $this->createForm(QuoteItemType::class, $quoteItem);
// ...
Using data option is no good, because:
http://symfony.com/doc/current/reference/forms/types/form.html#data
The data option always overrides the value taken from the domain data (object) when rendering. This means the object value is also overriden when the form edits an already persisted object, causing it to lose it's persisted value when the form is submitted.
So the recomendation is to set explicitly the data in the underlined object on initialization, either in __constructor() or before bind the object to the form.
To answer my own question and avoid confusion for anyone in the future:
$quoteItem = new QuoteItem();
// THIS LINE WAS MISSING
$quoteItem->setQuoteFlight(new QuoteFlight());
$form = $this->createForm('UniflyteBundle\Form\QuoteItemType', $quoteItem, ['allow_extra_fields' => true]);
$form->add('QuoteFlight', QuoteFlightType::class);
Without the added line the QuoteFlight entity was NULL when the form rendered during create.

"Unable to reverse value for property path" for Symfony2 form ChoiceType field on Select2

Long story short, in Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
And it works.
I tried to put there an empty Select field:
->add('actors', ChoiceType::class, array(
'mapped' => false,
'multiple' => true,
'attr'=>array(
'class' => "select2-ajax",
'data-entity'=>"actor"
)
))
The Select2 Ajax works, everything in DOM looks the same as in previous example, but on form submit I get errors in the profiler: This value is not valid.:
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[actors] = [0 => 20, 1 => 21]
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "actors": Could not find all matching choices for the given values
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Could not find all matching choices for the given values
The funny part is the data received is the same as they were when it was an EntityType: [0 => 20, 1 => 21]
I marked field as not mapped, I even changed field name to other than Movie entity's field name. I tried adding empty choices, I tried to leave it as EntityType but with custom query_builder, returning empty collection. Now I'm out of ideas.
How should I do it?
EDIT after Raymond's answer:
I added DataTransformer:
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
foreach($actors as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return array();
foreach($actorIds as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors[] = $actor;
}
return $actors;
}
}
Added it at the end of the MovieType buildForm():
$builder->get('actors')
->addModelTransformer(new ActorToNumberTransformer($this->manager));
$builder->get('actors')
->addViewTransformer(new ActorToNumberTransformer($this->manager));
And added service:
common.form.type.work:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
Nothing changed. On form submit, reverseTransform() gets the proper data, but profiler shows the same error. That's a big mistery for me now...
You'll need to add a DTO (Data Transformer ) to transform the value received from your form and return the appropriate object .
Since you're calling the value from Ajax it doesn't recognized it anymore as a an object but a text value.
Examples :
Symfony2 -Use of DTO
Form with jQuery autocomplete
The correct way isn't Data Transformer but Form Events, look here:
http://symfony.com/doc/current/form/dynamic_form_modification.html#form-events-submitted-data
In the example you have the field sport (an entity, like your Movie) and the field position (another entity, like actors).
The trick is to use ajax in order to reload entirely the form and use
PRE_SET_DATA and POST_SUBMIT.
I'm using Symfony 3.x but I think it's the same with 2.8.x
When you add data transformers and nothing seems to change, it sounds like the data never goes through your data transformers. The transformation probably fails before your new data transformers are called. Try to add a few lines to your code:
$builder->get('actors')->resetViewTransformers();
$builder->get('actors')->resetModelTransformers();
// and then add your own

Get data from database not from form

I'm having a problem when trying to compare a data database, with data input by form.
I have a "pedido", this has many "items". I need compare "item" by "item" if this is modifies in the form.
Then I need get original data from database and data modified from form.
The problem is when i try get original data from database.
Always get the data modified by form.
How Can i get the original data from database after the submit the form?
NOTE: i have tried get PedidoAuxiliar before and after HandleRequest.This doesn´t work!
UPDATE CODE: Input how compare the items
This is my controller editAction:
public function editarAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$pedido = $em->getRepository('PedidosBundle:Pedido')->find($id);
//$pedidoAuxiliar = $em->getRepository('PedidosBundle:Pedido')->find($id);
$formulario = $this->createForm(new PedidoType(), $pedido, array(
'action' => $this->generateUrl('my_routing', array('id' => $id)),
'attr' => array(
'novalidate' => 'novalidate'
),
'method' => 'POST',
));
$formulario->handleRequest($request);
if($formulario->isValid()){
$pedidoAuxiliar = $em->getRepository('PedidosBundle:Pedido')->find($id);
foreach($pedido->getArticulos() as $articulo){
foreach($pedidoAuxiliar->getArticulos() as $articuloAuxiliar){
if($articuloAuxiliar->getId() == $articulo->getId()){
if($articuloAuxiliar->getCantidad() == $articulo->getCantidad()){
//Some code...
To get data from db u can use EntityManager::refresh($entity) its overwrite entity data using db. So u must use data from form, not entity to compare.
But u always can ust Doctrine to check changes eg: http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/change-tracking-policies.html
because Doctrine hold info about old and new value, but is not so easy to get (outside listeners)
I have found a solution. I have created a new connection entity manager in my config.yml.
Now I call my entity data from my new entity manager and i get datas from database! Thanks!

Entity form field with inactive records

I have a form definition like this (for demonstration purposes):
$builder->add('field', 'entity', [
'class' => EntityA::class,
'query_builder' => function($repo) {
return $repo->createQueryBuilder('e')
->andWhere('e.active = 1');
}
]);
This ensures that only active records can be selected in the dropdown field when using this form.
This leads to this case: When I edit an entity using the form definition from above, and this entity as an inactive EntityA assigned, it won't appear in the dropdown field. When I hit the save button, it will get the first active (if any) EntityA assigned. Also the form will suggest to the user that a different Entity is assigned than it actually is.
The correct way would be that the form displays all active records and the one inactive one that is currently assigned.
I looked into Form event listeners but this seems overly complicated. Also, extending the form just for editing could be a thing but it seems not "the right way" to me.
How can I solve this issue, preferably without using 3rd party bundles?
Get the object and load different data into dropdown depends of type of action: edit/create:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
$form ->add('field', 'entity', [
'class' => EntityA::class,
'query_builder' => function($repo) use ($data) {
if ($data->getId()) {
// Edit mode: append the pre-selected record to dropdown
return $repo->createQueryBuilder('e')
->andWhere('e.active = 1')
->orWhere('e.id = :id')
->setParameter('id', $data->getId());
} else {
// Display only active records
return $repo->createQueryBuilder('e')->andWhere('e.active = 1');
}
}
]);
});

Symfony, forms and many to one

We're running into a small code-design smell with symfony and our forms. It is not a problem per se, but makes me wonder if we could attain our goals any other way.
For the sake of simplicity, let me briefly explain a setup: let "Product" be an entity that represents a product in a database, meant to be sold in an online store. Since the online store is designed to have several languages in it, every single bit of information that could be related to a language is in the entity "Product_descriptions" that is related in a manyToOne fashion to the "Product". Finally we have designed a "Language" entity, representing every single language the user can see the store in.
As you can imagine, the code is pretty standard stuff:
class Language
{
private $language_id;
private $language_name;
private $language_code;
//Some other stuff.
};
class Product
{
private $product_id;
private $product_reference;
private $product_weight;
private $product_descriptions; //As an arrayCollection of "Product_description" objects.
//Some other stuff.
};
class Product_description
{
private $product_description_id;
private $product_name;
private $product_long_description;
private $product_short_description;
private $product; //A reference to the Product itself.
private $language; //A reference to the language this is meant to be seen in.
};
Okay, now for the problem itself. The setup, as expected, works wonderfully. It is in the backend where the smell resides.
To create new products we have designed a symfony form Type. In the same form we would like to be able to set all the product information as well as the information for every possible language. The smell comes in when we need to feed all possible "Language"s to the form type, check if a "Product_description" exists for a "Language" and "Product", show the empty text field (in case it does not exist) or the filled field... Our solution requests that a repository for all languages is injected into the form . Let me show you how it goes (please, take into consideration that this is not the real code... something may be missing):
class ProductType extends AbstractType
{
private $language_repo;
public function __construct($r)
{
$this->language_repo=$r;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('product_name', 'text')
->add('product_code', 'text');
$product=$builder->getData();
//We retrieve all languages here, to check if an entry for that
//language exists and show its data.
$languages=$this->language_repo->findAll();
foreach($languages as $key => &$lan)
{
//Here we look for existing data... This will return null if there's none.
$product_description=$product->get_description_for_language($lan);
$default_name=$product_description ? $product_description->getProductName() : '';
$default_long=$product_description ? $product_description->getProductLongDescription() : '';
$default_short=$product_description ? $product_description->getProductShortDescription() : '';
//Here we manually create the name_#language_id# form data... That we will retrieve later.
$builder->add('name_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_name))
->add('long_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_long))
->add('short_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_short));
}
$builder->add('save', 'submit', array('label' => 'Save data'));
}
//And some other stuff here.
}
As you can see, we are manually setting some data keys that we need to retrieve later in the controller. The setup works, of course. Any new language will yield an empty form field. Any existing language shows the related information.
Now for the controller, this gets messier even... When we're submitting the form we go like this:
private function process_form_data(Form &$f, Product &$item, Request &$request)
{
//Find all languages...
$em=$this->getDoctrine()->getManager();
$languages=$em->getRepository("MyBundle:Language")->findAll();
//Get submitted data for that language..
foreach($languages as $key => &$lan)
{
$name_language=$f->get('name_'.$lan->getLanguageId())->getData();
$long_language=$f->get('long_'.$lan->getLanguageId())->getData();
$short_language=$f->get('short_'.$lan->getLanguageId())->getData();
//Check if the language entry exists... Create it, if it doesn't. Feed the data.
$product_description=$product->get_description_for_language($lan);
if(!$product_description)
{
$product_description=new Product_description();
$product_description->setLanguage($lan);
$product_description->setProduct($product);
}
$product_description->setName($name_language);
$product_description->setLongDescription($long_language);
$product_description->setShortDescription($short_language);
$em->persist($product_description);
}
//Do the product stuff, persist, flush, generate a redirect...Not shown.
}
It works, but seems to me that is not the "symfony" way of doing things. How would you do this?. Have you found a more elegant approach?.
Thanks a lot.
I think you should revisit the way you translate the entities...
An existing way is to use the DoctrineExtensionBundle, translatable to be precise...
You'll find more info here :
https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/translatable.md
Here is an extract to see how it can work :
<?php
// first load the article
$article = $em->find('Entity\Article', 1 /*article id*/);
$article->setTitle('my title in de');
$article->setContent('my content in de');
$article->setTranslatableLocale('de_de'); // change locale
$em->persist($article);
$em->flush();
( now the article has a german translation )

Categories