Let's say I have a main form (foo entity form) where I have an embedded form (bar entity embedded form).
Let's also say that foo - 1/many - bar (of course).
Now, I want to display all possible bar entities in the system, even if they aren't associated with foo. So before bind form with foo entity, I usually do some query, extract data and, if bar isn't already associated with foo, associate it (basically i create some "virtual" association that haven't to be persisted under certain circumstance. I can't use symfony2 native method as I need to handle some attributes and Symfony2 don't let me do that)
All works like a charm. Now I added to bar form a non-mapped field that should help me to know whenever to save or not the association.
Into controller I check for the presence of this field and if not, I artificially unset the index of the collection from request object. When I dump the request all is good (embedded elements without flag aren't there anymore).
BUT
When I bind request object to entity, all embedded form elements are still there. This is driving me totally cray.
Code example
(I will not paste entity code as the issue is not there. I will not paste form code also)
public function createAction()
{
$foo = new Foo();
$foo_form = $this->createForm(new FooType(), $foo);
if ($request->getMethod() == 'POST') {
$parameter_array = $request->request->all();
$bar_array = $parameter_array['foo']['bar'];
//If I dump here, of course, all bar are setted
foreach ($bar_array as $index => $bar) {
if (!isset($bar['associate'])) { //this is the flag
unset($parameter_array['foo']['bar'][$index]);
}
}
$request->request->replace($parameter_array);
//If i dump $request->request->all(); all non-flagged bar are gone
$foo_form->bind($request);
$foo->getBars(); //If I dump this all bar(s) are still there (even the not-flagged ones)
}
}
I've found a workaround. As I can't controller - or at least it seems I cannot - directly parameter bag when entities are involved, I've simply act upon object after form and object are binded.
My code is now this
public function createAction()
{
$foo = new Foo();
$foo_form = $this->createForm(new FooType(), $foo);
if ($request->getMethod() == 'POST') {
$foo_form->bind($request);
if ($foo_form->isValid()) {
$parameter_array = $request->request->all();
if (isset($parameter_array['foo']['bar'])) {
$bars = $foo->getBars();
$bar_array = $parameter_array['foo']['bar'];
foreach ($bar_array as $index => $bar) { //Of course here
if (!isset($bar['associate'])) { // I can use array_filter
$bars->remove($index); // or something similar. Is just more readable that way for this answer
}
}
$foo->setBars($bars);
}
}
}
}
Is there a better solution?
Related
I have this situation:
Symfony 4.4.8, in the controller, for some users, I change some properties of an entity before displaying it:
public function viewAction(string $id)
{
$em = $this->getDoctrine()->getManager();
/** #var $offer Offer */
$offer = $em->getRepository(Offer::class)->find($id);
// For this user the payout is different, set the new payout
// (For displaying purposes only, not intended to be stored in the db)
$offer->setPayout($newPayout);
return $this->render('offers/view.html.twig', ['offer' => $offer]);
}
Then, I have a onKernelTerminate listener that updates the user language if they changed it:
public function onKernelTerminate(TerminateEvent $event)
{
$request = $event->getRequest();
if ($request->isXmlHttpRequest()) {
// Don't do this for ajax requests
return;
}
if (is_object($this->user)) {
// Check if language has changed. If so, persist the change for the next login
if ($this->user->getLang() && ($this->user->getLang() != $request->getLocale())) {
$this->user->setLang($request->getLocale());
$this->em->persist($this->user);
$this->em->flush();
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::TERMINATE => [['onKernelTerminate', 15]],
];
}
Now, there is something very weird happening here, if the user changes language, the offer is flushed to the db with the new payout, even if I never persisted it!
Any idea how to fix or debug this?
PS: this is happening even if I remove $this->em->persist($this->user);, I was thinking maybe it's because of some relationship between the user and the offer... but it's not the case.
I'm sure the offer is persisted because I've added a dd('beforeUpdate'); in the Offer::beforeUpdate() method and it gets printed at the bottom of the page.
alright, so by design, when you call flush on the entity manager, doctrine will commit all the changes done to managed entities to the database.
Changing values "just for display" on an entity that represents a record in database ("managed entity") is really really bad design in that case. It begs the question what the value on your entity actually means, too.
Depending on your use case, I see a few options:
create a display object/array/"dto" just for your rendering:
$display = [
'payout' => $offer->getPayout(),
// ...
];
$display['payout'] = $newPayout;
return $this->render('offers/view.html.twig', ['offer' => $display]);
or create a new non-persisted entity
use override-style rendering logic
return $this->render('offers/view.html.twig', [
'offer' => $offer,
'override' => ['payout' => $newPayout],
]);
in your template, select the override when it exists
{{ override.payout ?? offer.payout }}
add a virtual field (meaning it's not stored in a column!) to your entity, maybe call it "displayPayout" and use the content of that if it exists
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.
in my project I have some forms with choice types with a lot of options.
So I decided to build an autocomplete choice type based on jquery autocomplete, which adds new <option> HTML elements to the original <select> on runtime. When selected they are submitted correctly, but can't be handled within the default ChoicesToValuesTransformer, since the don't exist in my form when I create it.
How can I make symfony accept my dynamically added values?
I found this answer Validating dynamically loaded choices in Symfony 2 , where the submitted values are used to modify the form on the PRE_SUBMIT form event, but couldn't get it running for my situation. I need to change choices known to the current type instead of adding a new widget to the form
To deal with dynamically added values use 'choice_loader' option of choice type. It's new in symfony 2.7 and sadly doesn't have any documentaion at all.
Basically it's a service implementing ChoiceLoaderInterface which defines three functions:
loadValuesForChoices(array $choices, $value = null)
is called on build form and receives the preset values of object bound into the form
loadChoiceList($value = null)
is called on build view and should return the full list of choices in general
loadChoicesForValues(array $values, $value = null)
is called on form submit and receives the submitted data
Now the idea is to keep a ArrayChoiceList as private property within the choice loader. On build form loadValuesForChoices(...) is called, here we add all preset choices into our choice list so they can be displayed to the user. On build view loadChoiceList(...) is called, but we don't load anything, we just return our private choice list created before.
Now the user interacts with the form, some additional choices are loaded via an autocomplete and put into th HTML. On submit of the form the selected values are submitted and in our controller action first the form is created and afterwards on $form->handleRequest(..) loadChoicesForValues(...) is called, but the submitted values might be completly different from those which where included in the beginning. So we replace our internal choice list with a new one containing only the submitted values.
Our form now perfectly holds the data added by autocompletion.
The tricky part is, that we need a new instance of our choice loader whenever we use the form type, otherwise the internal choice list would hold a mixture of all choices.
Since the goal is to write a new autocomplete choice type, you usually would use dependency injection to pass your choice loader into the type service.
But for types this is not possible if you always need a new instance, instead we have to include it via options. Setting the choice loader in the default options does not work, since they are cached too. To solve that problem you have to write a anonymous function which needs to take the options as parameters:
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
return AutocompleteFactory::createChoiceLoader();
},
));
Edit:
Here is a reduced version of the choice loader class:
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
class AutocompleteChoiceLoader implements ChoiceLoaderInterface
{
/** #var ChoiceListInterface */
private $choiceList;
public function loadValuesForChoices(array $choices, $value = null)
{
// is called on form creat with $choices containing the preset of the bound entity
$values = array();
foreach ($choices as $key => $choice) {
// we use a DataTransformer, thus only plain values arrive as choices which can be used directly as value
if (is_callable($value)) {
$values[$key] = (string)call_user_func($value, $choice, $key);
}
else {
$values[$key] = $choice;
}
}
// this has to be done by yourself: array( label => value )
$labeledValues = MyLabelService::getLabels($values);
// create internal choice list from loaded values
$this->choiceList = new ArrayChoiceList($labeledValues, $value);
return $values;
}
public function loadChoiceList($value = null)
{
// is called on form view create after loadValuesForChoices of form create
if ($this->choiceList instanceof ChoiceListInterface) {
return $this->choiceList;
}
// if no values preset yet return empty list
$this->choiceList = new ArrayChoiceList(array(), $value);
return $this->choiceList;
}
public function loadChoicesForValues(array $values, $value = null)
{
// is called on form submit after loadValuesForChoices of form create and loadChoiceList of form view create
$choices = array();
foreach ($values as $key => $val) {
// we use a DataTransformer, thus only plain values arrive as choices which can be used directly as value
if (is_callable($value)) {
$choices[$key] = (string)call_user_func($value, $val, $key);
}
else {
$choices[$key] = $val;
}
}
// this has to be done by yourself: array( label => value )
$labeledValues = MyLabelService::getLabels($values);
// reset internal choice list
$this->choiceList = new ArrayChoiceList($labeledValues, $value);
return $choices;
}
}
A basic (and probably not the best) option would be to unmap the field in your form like :
->add('field', choiceType::class, array(
...
'mapped' => false
))
In the controller, after validation, get the data and send them to the entity like this :
$data = request->request->get('field');
// OR
$data = $form->get('field')->getData();
// and finish with :
$entity = setField($data);
I have a reletivly simple change logging class that stores the date, an integer to indicate the type of change and 2 varchar(50)s that hold the old and new data for the change.
I can create and populate an instance of the class but when I come to flush it I get an "Error: Maximum function nesting level of '200' reached, aborting!" error.
I've read about the Xdebug issue and configured the max nests up to 200 but as you can see this isn't enough. The save process should be very simple and there should be no need for so many nested functions, so increasing it further will just hide the problem, whatever it is. I have far more complicated classes in this app that persisit and flush without a problem.
The issue is always at
NormalizerFormatter ->normalize ()
in app/cache/dev/classes.php at line 4912 -
Having looked at this a bit more I think the issue may be that the change instance is created and saved during the preUpdate event of another class:
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof Property) {
$entityManager = $eventArgs->getEntityManager();
$changeArray = $eventArgs->getEntityChangeSet();
foreach ($changeArray as $field => $values) {
$eventType = "";
switch ($field) {
case 'price' :
$eventType = PropertyEvent::EVENTTYPE_PRICE;
BREAK;
case 'status' :
$eventType = PropertyEvent::EVENTTYPE_STATUS;
BREAK;
}
if ($eventType != "") {
$event = new PropertyEvent($entity->getID(), $eventType, $values[0], $values[1]);
$entityManager->persist($event);
$entityManager->flush();
}
}
$entity->setUpdatedDate();
}
}
Why would that be an issue?
Doctrine only has one lifecycle event process so regardless of the entity you're using adding a flush within a lifecycle event will send you back round the loop and into your event handler again... and again ... and...
Have a look at this blog post
http://mightyuhu.github.io/blog/2012/03/27/doctrine2-event-listener-persisting-a-new-entity-in-onflush/
Basically the answer is to use
$eventManager -> removeEventListener('preUpdate', $this);
to remove the event. Then create the new entity, persist, flush and finally re-attach the event you are in.
$eventManager -> addEventListener('preUpdate', $this);
I think your issue is the $entity->setUpdatedDate(); call, which probably calls another update event and re-calls your handler.
I have a case where we create registration for sports events.
The registration contains some fields specific to each sport. Some of which will be named similarly although they will be different for each sport. Example: "favorite position on the field":
For Basketball it would be a choice field between:
Point guard
Shooting guard
etc...
For baseball, it would be the same choice field but with some different choices available:
Pitcher
Infield
Outfield
...
When first creating the form (for display), the sport is passed as part of the data in the registration:
$registration = new Registration;
$registration->setEvent($event);
and $event->getSport(); would return the sport for that event.
So far so good, and adding a listener to the generation of my form, I can set only the fields specific to that sport:
public static function getSubscribedEvents()
{
return [FormEvents::POST_SET_DATA => 'preSetData'];
}
/**
* #param event DataEvent
*/
public function preSetData(DataEvent $event)
{
$form = $event->getForm();
if (null === $event->getData()) {
return;
}
// (The get event here means the real life sports gathering)
$sport = $event->getData()->getEvent()->getSport();
/**
* Then I customize the fields depending on the current sport
*/
}
The problem comes when the user submits this form back. In this case, $event->getData()->getEvent() is null.
The "event" (real life one) is a document_id field in the registration form (using MongoDB here).
If I listen to the ::BIND event instead of ::PRE_SET_DATA, then I can access everything, but it's too late to customize the form as it is already bound. ::PRE_BIND does the same as ::PRE_SET_DATA.
How can I correctly retrieve my Event and Sport Documents here in order to customize my form and validate it appropriately?
Why would you need an event to do such task? You can define the fields in the buildForm() action of the form class. To access the event object simply use $options['data']->getEvent()
So ... Finally found how to do this properly.It requires subscribing to two different events.
First time the form is built, some data is passed to it, therefore, the PRE_SET_DATA event contains that data and everything works fine as explained in the question.
On the moment the form is submitted, it is first created with NO data, therefore the data accessed in PRE_SET_DATA will be null. In this case we skip over the form customization:
public function preSetData(DataEvent $event)
{
$myEvent = $event->getData()->getEvent();
if (null === $myEvent) {
return;
}
$this->customizeForm();
}
This ensures that we don't run into issues when submitting the form and no data is passed, however getData() will return an empty object and not NULL.
Now, when the form is submitted, we will bind it to the data received. That's when we want to interfere. So we'll also subscribe to the PRE_BIND event:
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_BIND => 'preBind',
FormEvents::PRE_SET_DATA => 'preSetData',
];
}
In pre-bind, the data we receive is only an array of values and not an object graph.
But if we injected the object manager in our listener, then we can find our objects and work with them:
public function preBind(DataEvent $event)
{
$data = $event->getData();
$id = $data['event'];
$myEvent = $this->om
->getRepository('Acme\DemoBundle\Document\Event')
->find(new \MongoId($id));
if($myEvent === null){
$msg = 'The event %s could not be found';
throw new \Exception(sprintf($msg, $id));
}
$this->customizeForm();
}