Symfony CollectionType: Merge new entries - forms

My symfony form represents an entity Mail which has a one to many relation with another entity called Attachment. Therefore, the MailType form contains a CollectionType field for embedding its AttachmentType forms:
$builder
->add('attachments', CollectionType::class, [
'entry_type' => AttachmentType::class,
'allow_add' => true,
'allow_delete' => false,
'by_reference' => false,
]);
My view will only send new attachments to my Symfony backend. So when storing the form data into the database, I only want to add new attachments of the mail and do not touch any existing attachments.
Unfortunately, Symfony / Doctrine behave differently: If n attachments are contained in the form data then n first existing attachments are overwritten by those new attachments:
existing attachments (in DB): [old1, old2, old3]
new attachments (contained by HTTP request): [new1, new2]
desired result in DB: [old1, old2, old3, new1, new2]
actual result in DB: [new1, new2, old3]
How can I achieve this? I thought by_reference => false will cause the addAttachment method to be called, so I also expected this to work out-of-the-box.
My Mail entity code:
class Mail {
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Attachment", mappedBy="mail", cascade={"persist", "remove"})
*/
protected $attachments;
...
public function addAttachment(\AppBundle\Entity\ttachment $attachment) {
$attachment->setMail($this);
$this->attachments[] = $attachment;
return $this;
}
}
My controller code processing the form:
// $mail = find mail in database
$form = $this->createForm(MailType::class, $mail);
$form->handleRequest($request);
if ($form->isValid()) {
$mail = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($mail);
$em->flush();
}

There are a couple of ways to do what you want. The simplest would be to give empty data or new attachment entities to your form field:
$builder
->add('attachments', CollectionType::class, [
'entry_type' => AttachmentType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'data' => [new Attachment()] // or add more or []
]);
Then in your Mail entity:
public function addAttachment(Attachment $attachment) {
$attachment->setMail($this);
$this->attachments[] = $attachment;
return $this;
}
public function removeAttachment(Attachment $attachment) {
return $this;
}
If you are using removeAttachment for some other functionality and you want to actually remove the Attachment, you can take advantage of the property_path settings of the form field:
'property_path' => 'appendAttachments'
and create addAppendAttachment and removeAppendAttachment:
public function addAppendAttachment(Attachment $attachment) {
$attachment->setMail($this);
$this->attachments[] = $attachment;
return $this;
}
public function removeAppendAttachment(Attachment $attachment) {
return $this;
}

Related

A new entity was found through the relationship

The error message:
A new entity was found through the relationship
'AppBundle\Entity\Tarifa#pesos' that was not configured to cascade
persist operations for entity:
AppBundle\Entity\TarifaPeso#0000000072d3bd4300000000232470d3. To solve
this issue: Either explicitly call EntityManager#persist() on this
unknown entity or configure cascade persist this association in the
mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot
find out which entity causes the problem implement
'AppBundle\Entity\TarifaPeso#__toString()' to get a clue.
Tarifa.php
/**
* #ORM\OneToMany(targetEntity="TarifaPeso", mappedBy="tarifa")
*/
private $pesos;
TarifaPeso.php
/**
* #ORM\ManyToOne(targetEntity="Tarifa", inversedBy="pesos", cascade={"persist"})
* #ORM\JoinColumn(name="tarifa_id", referencedColumnName="id")
*/
private $tarifa;
TarifaType.php
->add('pesos', CollectionType::class, array(
'entry_type' => TarifaPesoType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
The controller...
public function newAction(Request $request)
{
$tarifa = new Tarifa();
$form = $this->createForm('AppBundle\Form\TarifaType', $tarifa);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($tarifa);
$entityManager->flush();
$this->addFlash('success', 'project.created_successfully');
return $this->redirectToRoute('admin_post_index');
}
return $this->render('admin/tarifas/new.html.twig', array(
'form' => $form->createView(),
));
}
What am I missing? Really exhausted... please any help?
You should move cascade={"persist"} annotation from TarifaPeso::tarifa to Tarifa::pesos property. Or you can explicitly persist all pesos you get from the form:
$entityManager->persist($tarifa);
foreach ($tarifa->getPesos() as $peso) {
$entityManager->persist($peso);
}
$entityManager->flush();
Ok, it is solved, now it stores the Tarifa id in the TarifaPeso table. The error was I'd removed the line from the AddPeso method:
public function addPeso(\AppBundle\Entity\TarifaPeso $pesos)
{
$this->pesos[] = $pesos;
$pesos->setTarifa($this);
return $this;
}

zendframework 2 Doctrine 2 my post form is not returning the values

i am a little baffled by this;
my post forms is not populating the values received from the returned post values; i suspect the problem is arising from my getJobId() in my jobsort class values;
below is my form:
public function jobSortAction()
{
$form = new CreateJobSortForm($this->getEntityManager());
$jobSort = new JobSort();
$form->setInputFilter($jobSort->getInputFilter());
$id= 11;
$jobSort->setId($id);
$form->bind($jobSort);
if ($this->request->isPost()) {
//$post = $this->request->getPost();
$form->setData($this->request->getPost());
//var_dump($post);
//var_dump($jobSort);
if ($form->isValid()) {
$this->getEntityManager()->persist($jobSort);
$this->getEntityManager()->flush();
}
}
return array('form' => $form);
}
below is the var_dumped values of the 'return post values' and the Jobsort() object. You will note that the returned post values has values for both the Id and the JobId
object(Zend\Stdlib\Parameters)[168]
public 'JobSort' =>
array (size=2)
'jobId' => string '5' (length=1)
'id' => string '11' (length=2)
public 'submit' => string 'Submit' (length=6)
object(Workers\Entity\JobSort)[394]
protected 'inputFilter' => null
protected 'id' => int 11
protected 'jobId' => null
protected 'workerservicelist' => null
yet, when i populate the values, it does not seem to record the values for the jobId
below is my jobsort entity class:
class JobSort
{
protected $inputFilter;
/**
* #ORM\Id
*
* #ORM\Column(name="user_id", type="integer")
*/
protected $id;
/**
* #ORM\Column(name="jobId", type="integer")
*/
protected $jobId;
public function setId($id)
{
return $this->id = $id;
}
public function getId()
{
return $this->id;
}
public function setJobId($jobId)
{
return $this->jobId = $jobId;
}
public function getJobId( )
{
return $this->jobId;
}
is there any advice or suggestions on what i need to do to find out why the values are not been populated
warm regards
Andreea
by the way; the form actually works when i had the Id of CLASS jobsort set to
#ORM\GeneratedValue(strategy="AUTO")
the problem started when i took it out and set it to manual
Hello again
here is my form:
this is the error message i received;
An exception occurred while executing 'INSERT INTO worker_main_jobsort (user_id, jobId) VALUES (?, ?)' with params [11, null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'jobId' cannot be null
here is my form:
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;
use Workers\Form\Fieldset\JobSortFieldset;
class CreateJobSortForm extends Form
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('create-Job-post-form');
// The form will hydrate an object of type "BlogPost"
$this->setHydrator(new DoctrineHydrator($objectManager, 'Workers\Entity\JobSort'));
// Add the user fieldset, and set it as the base fieldset
$JobSortFieldset = new JobSortFieldset($objectManager);
$JobSortFieldset->setUseAsBaseFieldset(true);
$this->add($JobSortFieldset);
// Optionally set your validation group here
// … add CSRF and submit elements …
$this->add(array(
'name' => 'submit',
'type' => 'Submit',
'attributes' => array(
'value' => 'Submit',
'id' => 'submitbutton',
),
));
// Optionally set your validation group here
}
}
and here is the fieldset class:
class JobSortFieldset extends Fieldset
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('JobSort');
$id= 10;
$this->setHydrator(new DoctrineHydrator($objectManager, 'Workers\Entity\JobSort'))
->setObject(new JobSort());
}
}
this addition is in response to rafaame solution;
i amended my form as recommended; however it still not working. i think the issue now is that Rafaame solution is in regarding to zendDB save method, but i am using doctrine persis**t and **flush method . i accordingly get the following error message;
Call to undefined method Workers\Entity\JobSort::save()
below is my amended form:
public function jobSortAction()
{
$form = new CreateJobSortForm($this->getEntityManager() );
$jobSort = new JobSort();
if($this->request->isPost())
{
$form->setData($this->request->getPost());
if ($form->isValid())
{
$entity = $form->getData();
$model = new JobSort();
$model->save($entity);
// $this->getEntityManager()->persist( $model);
// $this->getEntityManager()->flush();
}
}
return array('form' => $form);
}
in response to Rafaame question about what problems i had,the message that i am now receiving is this:
**
EntityManager#persist() expects parameter 1 to be an entity object,
array given.
**
below is my function:
public function jobSortAction()
{
$serviceLocator = $this->getServiceLocator();
$objectManager = $this->getEntityManager();
$form = new CreateJobSortForm($this->getEntityManager());
if ($this->request->isPost())
{
$form->setData($this->request->getPost());
if ($form->isValid()) {
$entity = $form->getData();
$model = new JobSort($objectManager, $serviceLocator);
$model->getEntityManager()->persist($entity);
$model->getEntityManager()->flush();
}
}
return array('form' => $form);
}
my form; i.e where the hydrator should be set
namespace Workers\Form;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;
use Workers\Form\Fieldset\JobSortFieldset;
class CreateJobSortForm extends Form
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('JobSort');
// The form will hydrate an object of type "BlogPost"
$this->setHydrator(new DoctrineHydrator($objectManager, 'Workers\Entity\JobSort'));
// Add the user fieldset, and set it as the base fieldset
$JobSortFieldset = new JobSortFieldset($objectManager);
$JobSortFieldset->setUseAsBaseFieldset(true);
$this->add($JobSortFieldset);
If you check your code, you are creating a JobSort entity, setting only its id and binding it to the form:
$jobSort = new JobSort();
$jobSort->setId($id);
$form->bind($jobSort);
After that, you are dumping $jobSort and $this->request->getPost(). So, obviously, you are getting jobId in the POST data but not in the entity (you didn't set the entity's jobId before binding it to the form). There's nothing wrong with your entity's code.
The solution for this: don't bind anything to the form. You should only bind an entity to the form in the case of an edit action, that you fetch the entity from the database and want to populate the form with its values.
Example of add action:
public function addAction()
{
$serviceLocator = $this->getServiceLocator();
$objectManager = $this->getObjectManager();
$form = new Form\EmailCampaign\Add($serviceLocator, $objectManager);
if($this->request instanceof HttpRequest && $this->request->isPost())
{
$form->setData($this->request->getPost());
if($form->isValid())
{
$entity = $form->getData();
//If you want to modify a property of the entity (but remember that it's not recommended to do it here, do it in the model instead).
//$entity->setJobId(11);
$model = new Model\EmailCampaign($serviceLocator, $objectManager);
$model->save($entity);
if($entity->getId())
{
$this->flashMessenger()->addSuccessMessage('Email campaign successfully added to the database.');
return $this->redirect()->toRoute('admin/wildcard', ['controller' => 'email-campaign', 'action' => 'edit', 'id' => $entity->getId()]);
}
else
{
$this->flashMessenger()->addErrorMessage('There was an error adding the email campaign to the database. Contact the administrator.');
}
}
}
return new ViewModel
([
'form' => $form,
]);
}
Example of edit action:
public function editAction()
{
$serviceLocator = $this->getServiceLocator();
$objectManager = $this->getObjectManager();
$form = new Form\EmailCampaign\Edit($serviceLocator, $objectManager);
$id = $this->getEvent()->getRouteMatch()->getParam('id');
$entity = $objectManager
->getRepository('Application\Entity\EmailCampaign')
->findOneBy(['id' => $id]);
if($entity)
{
$form->bind($entity);
if($this->request instanceof HttpRequest && $this->request->isPost())
{
$form->setData($this->request->getPost());
if($form->isValid())
{
//If you want to modify a property of the entity (but remember that it's not recommended to do it here, do it in the model instead).
//$entity->setJobId(11);
$model = new Model\EmailCampaign($serviceLocator, $objectManager);
$model->save($entity);
$this->flashMessenger()->addSuccessMessage('Email campaign successfully saved to the database.');
}
}
}
else
{
$this->flashMessenger()->addErrorMessage('A email campaign with this ID was not found in the database.');
return $this->redirect()->toRoute('admin', ['controller' => 'email-campaign']);
}
return new ViewModel
([
'form' => $form,
'entity' => $entity,
]);
}
Hope this helps.
EDIT:
What I provided was an example of how to handle the form and the entities with Doctrine 2 + ZF2.
What you have to keep in mind is that Doctrine doesn't work with the concept of models, it just understands entities. The model I'm using in my application is a concept of the MVC (Model-View-Controller) design pattern (that ZF2 uses) and I have decided to wrap the entity manager calls (persist and flush) inside my model's method, that I named save() (in the case the entity needs some special treatment before being save to the database and also because it is not a good practice to use the entity manager directly in the controller - see this slide of Marcos Pivetta presentation http://ocramius.github.io/presentations/doctrine2-zf2-introduction/#/66).
Another thing that you may be misunderstanding is that when you do $form->getData() to a form that has the DoctrineObject hydrator, it will return you the entity object, and not an array with the data (this last happens if it has no hydrator). So you don't need to create the entity after doing $form->getData(), and if you do so, this created entity won't have any information provided by the form.
Your code should work now:
public function jobSortAction()
{
$serviceLocator = $this->getServiceLocator();
$entityManager = $this->getEntityManager();
$form = new CreateJobSortForm($entityManager);
if ($this->request->isPost())
{
$form->setData($this->request->getPost());
if ($form->isValid()) {
//I'm considering you are setting the DoctrineObject hydrator to your form,
//so here we will get the entity object already filled with the form data that came through POST.
$entity = $form->getData();
//Again, if you need special treatment to any data of your entity,
//you should do it here (well, I do it inside my model's save() method).
//$entity->setJobId(11);
$entityManager->persist($entity);
$entityManager->flush();
}
}
return array('form' => $form);
}

Symfony2 Forms - Dependent entity fields

Relating to a cars application, I have a form where the user can choose brand and model ; the aim is that the model depends on the brand. For example, when you choose "Brand A", then only models related to brand A are displayed.
For now, I am already blocked on the initial form (where no data is set) ; there is a default brand displayed, and I would like models relative to this brand being displayed.
I followed some tutorials to add an event subscriber, but I can't make it works ; I am unable to get brand's field value into my subscriber... I tryed to get the Brand entity, but logically it is empty (because the form has not been submitted).
Anybody has an idea ?
FormType :
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder->add( 'brand', 'entity', array(
'class' => 'MyBundle:Brand',
'property' => 'name',
));
$builder->add( 'model', 'entity', array(
'class' => 'MyBundle:Model',
'property' => 'name',
));
/* Add event for the first model choices */
$builder->addEventSubscriber( new ModelChoicesSubscriber() );
}
FormSubscriber :
class ModelChoicesSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return array( FormEvents::PRE_SET_DATA => 'preSetData' );
}
public function preSetData( FormEvent $event ) {
$data = $event->getData();
$form = $event->getForm();
if( $data == null ) {
return;
}
$brand = $form->get( 'brand' );
}
}

Symfony2 - Set a selected value for the entity field

I'm trying to set a selected value inside an entity field. In accordance with many discussions I've seen about this topic, I tried to set the data option but this doesn't select any of the values by default:
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('place', 'entity', array(
'class' => 'RoyalMovePhotoBundle:Place',
'property' => 'name',
'empty_value' => "Choisissez un club",
'mapped' => false,
'property_path' => false,
'data' => 2
))
->add('begin')
->add('end')
->add('title')
->add('description')
;
}
// ...
}
By looking for more I've found that some people had to deactivate the form mapping to the entity. That seems logical so I tried to add 'mapped' => false to the options, without success...
If it can help, here's my controller:
class EventController extends Controller
{
// ...
public function addAction()
{
$request = $this->getRequest();
$em = $this->getDoctrine()->getManager();
$event = new Event();
$form = $this->createForm(new EventType(), $event);
$formHandler = new EventHandler($form, $request, $em);
if($formHandler->process()) {
$this->get('session')->getFlashBag()->add('success', "L'évènement a bien été ajouté.");
return $this->redirect($this->generateUrl('photo_event_list'));
}
return $this->render('RoyalMovePhotoBundle:Event:add.html.twig', array(
'form' => $form->createView()
));
}
}
And the EventHandler class:
class EventHandler extends AbstractHandler
{
public function process()
{
$form = $this->form;
$request = $this->request;
if($request->isMethod('POST')) {
$form->bind($request);
if($form->isValid()) {
$this->onSuccess($form->getData());
return true;
}
}
return false;
}
public function onSuccess($entity)
{
$em = $this->em;
$em->persist($entity);
$em->flush();
}
}
I'm a bit stuck right now, is there anyone who got an idea?
You only need set the data of your field:
class EventController extends Controller
{
// ...
public function addAction()
{
$request = $this->getRequest();
$em = $this->getDoctrine()->getManager();
$event = new Event();
$form = $this->createForm(new EventType(), $event);
// -------------------------------------------
// Suppose you have a place entity..
$form->get('place')->setData($place);
// That's all..
// -------------------------------------------
$formHandler = new EventHandler($form, $request, $em);
if($formHandler->process()) {
$this->get('session')->getFlashBag()->add('success', "L'évènement a bien été ajouté.");
return $this->redirect($this->generateUrl('photo_event_list'));
}
return $this->render('RoyalMovePhotoBundle:Event:add.html.twig', array(
'form' => $form->createView()
));
}
}
In order to option appear selected in the form, you should set corresponding value to entity itself.
$place = $repository->find(2);
$entity->setPlace($place);
$form = $this->createForm(new SomeFormType(), $entity);
....
For non-mapped entity choice fields, the method I found easiest was using the choice_attr option with a callable. This will iterate over the collection of choices and allow you to add custom attributes based on your conditions and works with expanded, multiple, and custom attribute options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('place', 'entity', array(
//...
'choice_attr' => function($place) {
$attr = [];
if ($place->getId() === 2) {
$attr['selected'] = 'selected';
//for expanded use $attr['checked'] = 'checked';
}
return $attr;
}
))
//...
;
}
When you use the query_builder option, and the data option expects an collection instance, and you don't want to touch your controller by adding setDatas for only certain fields, and you already have your querybuilder and the ids of the repopulating options in your form type class, you can repopulate a selection as following:
// Querybuilder instance with filtered selectable options
$entities = $qb_all;
// Querybuilder instance filtered by repopulating options (those that must be marked as selected)
$entities_selected = $qb_filtered;
Then in your add() Method
'data' => $entities_selected->getQuery()->getResult(), // Repopulation
'query_builder' => $entities,
EDIT: Real use case example
You want to repopulate a checkbox group rendered with following elements:
Label: What is your favourite meal?
4 Checkboxes: Pasta, Pizza, Spaghetti, Steak
And you want to repopulate 2 Checkboxes:
Pizza, Steak
$qb_all would be a QueryBuilder instance with the all 4 selectable Checkboxes
$qb_filtered would be a new additional QueryBuilder instance with the repopulating Checkboxes Pizza, Steak. So a "filtered" version of the previous one.

How to manage collections through an API with symfony2 and forms

I'm building an API where a user can update an entity with a collection as part of it. This works fine if I use forms throughout, but I'm building up the API. My entity looks like this:
<?php
class MyEntity {
// ...
/**
* #ORM\OneToMany(targetEntity="TitleEntity", mappedBy="entityID", cascade={"persist"})
*/
protected $myTitles;
public function getMyTitles() {
return $this->myTitles;
}
public function setMyTitles($titles) {
foreach($titles as $key => $obj) { $obj->setEntity($this); }
$this->myTitles = $collection;
}
public function addMyTitle($obj) {
$obj->setEntity($this);
$this->myTitles[] = $obj;
}
public function removeMyTitle($obj) {
$this->myTitle->removeElement($obj);
}
}
The myTitles is an entity that has an ID, the ID of the entity it is attached to, and then a title.
For the API, I'm passing a JSON content body back as a PUT request for the MyEntity object, so I end up with an array of the titles, and I'm prepping them like this to bind to a form for validation:
$myTitles = array();
foreach($titles as $key => $title) {
$titleObj = new TitleEntity();
$titleObj->setTitle($title);
}
$myEntity->setTitles($titles);
but it complains with:
The form's view data is expected to be of type scalar, array or an instance of
\ArrayAccess, but is an instance of class stdClass. You can avoid this error by
setting the "data_class" option to "stdClass" or by adding a view
transformer that transforms an instance of class stdClass to scalar, array or
an instance of \ArrayAccess
It looks like this happens because I call getMyTitles() before I bind my entity to the form I'm using to validate against.
I'm binding to the form using an array:
$form = $this->createForm(new AddEntity(), $myEntity);
$data = array( // Set all my data );
$form->bind($data);
if($form->isValid() {
// ...
If I do the createForm() call first, and then add the titles afterward, I get this:
Call to a member function removeElement() on a non-object
which occurs inside removeMyTitle().
How do I handle this?
Edit
Here is the AddEntity() type:
<?php
class AddEntity extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text')
->add('subheading', 'text')
->add('description', 'textarea')
->add('myTitles', 'collection', array(
'type' => new AddMyTitles(), // Basic type to allow setting the title for myTitle entities
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'options' => array(
'required' => false,
),
));
}
public function getName()
{
return 'addEntity';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\TestBundle\Entity\Entity',
));
}
You need data transformers here.
http://symfony.com/doc/2.0/cookbook/form/data_transformers.html
Basically, you have told the form it is getting an array, and you've given it something else. The transformer is supposed to handle this.
If you need more help I'd need more information.
Also, somewhat bafflingly, you refer to 'myCollections' in your prose but don't show it, in your code.
^^^^^^^^^ fixed by edit.