I have a strange problem. I'm working on a Symfony 2 based web app and I embedding a from collection to another one based on the Symfony Cookbook entry (This: http://symfony.com/doc/2.3/cookbook/form/form_collections.html ).
Recently I changed my bundle's mapping data to annotation from yaml.
The problem starts here. Persisting new elements works fine, but I can't remove anything.
I have a Page entity and an Image entity and they have a many to many relation. I embed and image upload form collection to the Page's form.
The old mapping was invalid, the new one is valid but doesn't work.
Here are my old and new mappings:
old yml:
page:
manyToMany:
images:
targetEntity: Image
inversedBy: page
cascade: [persist, remove]
orderBy:
image_order: DESC
no mapping on the image
New mapping:
Page:
/**
* #ORM\ManyToMany(targetEntity="Image", inversedBy="pages", cascade={"persist", "remove"})
* #ORM\JoinTable(name="page_image",
* joinColumns={#ORM\JoinColumn(name="page_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="image_id", referencedColumnName="id")}
* )
* #ORM\OrderBy({"image_order" = "DESC"})
*/
protected $images;
Image:
/**
* #ORM\ManyToMany(targetEntity="Page", mappedBy="images")
*/
protected $pages;
The action that handles the edit form submission:
/**
* Edits an existing Page entity.
*
* #Route("/{id}/update", name="page_update")
* #Method("post")
* #Template("AdamantiumBackendBundle:Page:edit.html.twig")
*/
public function updateAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AdamantiumBackendBundle:Page')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Page entity.');
}
$editForm = $this->createForm(new PageType(), $entity);
$deleteForm = $this->createDeleteForm($id);
$request = $this->getRequest();
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->persist($entity);
$em->flush();
apc_clear_cache('user');
return $this->redirect($this->generateUrl('page_edit', array('id' => $id)));
}
return $this->render('AdamantiumBackendBundle:Page:edit.html.twig',array(
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
The embeded collection from the Page form:
->add('images', 'collection', array('type' => new ImageType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'options' => array('data_class' => 'Adamantium\BackendBundle\Entity\Image'),
))
I have the add and remove Images methods, I have getters and setters. (and as I said earlier everything worked fine with the invalid yml mapping)
So please tell me what am I missing or what do I do wrong.
Thanks!!
Related
I'm using Symfony3.4 with softDeletable module in some entities.
I have a ZoneMaterial entity with two arrayCollection of entities :
/**
* #ORM\ManyToMany(targetEntity="EPC", cascade={"persist"}, orphanRemoval=true)
* #ORM\JoinTable(name="app_zone_material_epc",
* joinColumns={#ORM\JoinColumn(name="zoneMaterial_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="epc_id", referencedColumnName="id")}
* )
*/
private $epcs;
/**
* #ORM\ManyToMany(targetEntity="EPI", cascade={"persist"}, orphanRemoval=true)
* #ORM\JoinTable(name="app_zone_material_epi",
* joinColumns={#ORM\JoinColumn(name="zoneMaterial_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="epi_id", referencedColumnName="id")}
* )
*/
private $epis;
[...]
public function addEpc(EPC $e)
{
$this->epcs[] = $e;
}
public function removeEpc(EPC $e)
{
$this->epcs->removeElement($e);
}
public function getEpcs()
{
return $this->epcs;
}
public function addEpi(EPI $e)
{
$this->epis[] = $e;
}
public function removeEpi(EPI $e)
{
$this->epis->removeElement($e);
}
public function getEpis()
{
return $this->epis;
}
I link/unlink them with a form defined as followed :
->add('epcs', EntityType::class, array( 'label' =>'MPC',
'class' => 'AppBundle\Entity\EPC',
'query_builder' => function ($repository) use ($options) {
return $repository
->createQueryBuilder('a')
->orderBy('a.name', 'ASC');
},
'multiple' => true,
'expanded' => true))
->add('epis', EntityType::class, array( 'label' =>'EPI',
'class' => 'AppBundle\Entity\EPI',
'query_builder' => function ($repository) use ($options) {
return $repository
->createQueryBuilder('a')
->orderBy('a.name', 'ASC');
},
'multiple' => true,
'expanded' => true))
And my updateAction function in my controller looks like :
public function updtZoneMaterialAction(Request $request, $id){
$doctrine = $this->getDoctrine();
$entityManager = $doctrine->getManager();
$zoneMaterial = $doctrine->getRepository(ZoneMaterial::class)->find($id);
$form = $this->createForm(ZoneMaterialType::class, $zoneMaterial);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Persist informations
$entityManager->persist($zoneMaterial);
$entityManager->flush();
//->redirection
}
//Render view
return $this->render('AppBundle:Zone:updt_zone_material.html.twig', array(
'form' => $form->createView(),
'id' => $id
));
}
The softDeletable extension is installed on my two entities EPI and EPC but it seems that each time I "unlink" an EPI or an EPC from a ZoneMaterial, the concerned entity is remove (with a deletedAt set to new DateTime("NOW")).
each time I unlink an EPI or an EPC from a ZoneMaterial, the concerned entity is removed
This is what you've told doctrine by configuring orphanRemoval=true.
The setting for orphan removal tells Doctrine whether or not it should remove the entities that got unlinked from their aggregate root.
This concept is very powerful and enables a form of programming that considers persistence as an implementation detail.
Rather than manually telling the ORM than an entity must be removed, one can model their domain in such a way that simply removing the entity from the collection is enough to automatically synchronise the database to reflect the state of the model.
If that is not what you want, you should not set orphanRemoval to true.
After long time of lerking on these boards I have finally decided to make my first post. I have recently started to play around with Symfony (2.4, don't yell at me please :) ). Using doctrine's terminal commands I generated CRUD events. Now, that is great and all, except that you have to pass the ID in the url, for example: www.mydomain.com/account/16/. This will pre-fill the form with the data from a row that has id 16 in mysql. My question is, how do I manipulate the pre-made CRUD (only interested in the update) so that I don't have to pass the id to the url, but rather, it renders the form based on the id the logged in user has associated with their account?
Here is my code:
class AccountController extends Controller
{
/**
* Displays a form to edit an existing Event entity.
* #Route("/account/{id}", name="post_login_account_edit")
* #PreAuthorize("isFullyAuthenticated()")
*/
public function editAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('UserBundle:User')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Event entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('UserBundle:Account:account.html.twig', array(
'entity' => $entity,
'edit_form' => $editForm->createView()
));
}
/**
* Creates a form to edit a Event entity.
*
* #param User $entity The entity
*
* #return \Symfony\Component\Form\Form The form
*/
private function createEditForm(User $entity)
{
$form = $this->createForm(new UserType(), $entity, array(
'action' => $this->generateUrl('post_login_account_update', array('id' => $entity->getId())),
'method' => 'PUT',
));
$form->add('submit', 'submit', array('label' => 'Update'));
return $form;
}
/**
* Edits an existing User entity.
* #Route("/account/{id}/update", name="post_login_account_update")
* #PreAuthorize("isFullyAuthenticated()")
*/
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('UserBundle:User')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Event entity.');
}
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->flush();
return $this->redirect($this->generateUrl('post_login_account'));
}
return $this->render('UserBundle:Account:account.html.twig', array(
'entity' => $entity,
'edit_form' => $editForm->createView()
));
}
}
Simply get logged in user in controller:
$entity = $this->get('security.context')->getToken()->getUser();
I have entity which has multiple Photos:
/**
* related images
* #ORM\OneToMany(targetEntity="Photo", mappedBy="entity",cascade={"persist"})
* #ORM\OrderBy({"uploaded_at" = "ASC"})
*/
private $photos;
Photos have ManyToOne relation with entity
/**
* #ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Entity", inversedBy="photos")
* #ORM\JoinColumn(name="entity_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $entity;
all setters and getter are set I'm foliving symfony collection documentation: http://symfony.com/doc/current/reference/forms/types/collection.html
FormType:
->add('photos', 'collection', array(
'type' => new PhotoFormType(),
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
'prototype' => true
))
PhotoType:
$builder
->add('title', null, ['label' => 'front.photo.title', 'required' => true])
->add('image', 'file', array('required' => false))
;
For upload I'm using vichUploadableBundle, Images are save just fine, but entity_id is not save and has null. I don't know what I did miss here.
Following would be best solution on this issue so far investigate or research with symfony form component.
FormType:
->add("photos",'collection', array(
'type' => new PhotoFormType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
Entity class
public function addPhoto(Photo $photo)
{
$photo->setEntity($this);
$this->photos->add($photo);
}
public function removePhoto(Photo $photo)
{
$this->photos->removeElement($photo);
}
Best practice is not to use loop to bind reference entity manually . Remember by_reference must be false. like 'by_reference' => false.
Run into the same issue, still I remember there is better solution.
You need to specify add and remove functions in the entity with collection.
class Entity
{
// ...
public function addPhoto(Photo $photo)
{
$this->photos->add($photo);
$photo->setEntity($this);
}
public function removePhoto(Photo $photo)
{
$this->photos->removeElement($photo);
}
}
So in such a case you wouldn't need a loop in the controller.
Also if
orphanRemoval=true
is set, no problems with delete.
I've went to this also. I think the main problem is that even the main entity has cascade={"persist"} , the child entites do not get the ID when you are creating a new entry.
So what I did, that is kind of a hack, but works fine is this.
// $em->persist($entity); After persisting entity:
foreach ($entity->getPhotos() as $photo) {
$photo->setEntity($entity);
}
Basically persisting the ID in the childs after their father is created.
But on another point, at least how I understand Doctrine, please correct me if I'm wrong. Try to add an orphanRemoval / fetch additional properties:
FATHER Entity has:
/**
* Related images.
* #ORM\OneToMany(targetEntity="Photo", mappedBy="entity", cascade={"persist"}, orphanRemoval=true, fetch="EAGER")
* #ORM\OrderBy({"uploaded_at" = "ASC"})
*/
private $photos;
Photo Entity is persisted so I add to controller handler to set for every photo Entity. Don't know if it's right solution but it's working.
/** #var Photo $photo */
foreach ($entity->getPhotos() as $photo){
$photo->setEntity($entity);
$em->persist($photo);
}
I have 5 entities:
User,
Person,
UserAffiliation,
PersonAffiliation and
Affiliation
Here is the schema:
Some details:
A WebUser is a Person who is registered to the website. For each Webuser, there is a person ID.
A person can be a web user, an author etc.
Each WebUser has 0 or more affiliations. Those affiliations were created by this WebUser and linked in the able UserAffiliations.
The WebUser can also link the affiliations he createed to a person (if the person is an author) and the Entity PersonAffiliation will be populated.
I am trying now to give the possibility to the webuser to assign an affiliation to an author (person). For that, the I have:
In the Entity Person
#ORM\OneToMany(targetEntity="PersonAffiliation", mappedBy="person", cascade={"persist", "remove"})
protected $affiliations;
In the PersonAffiliation
#ORM\ManyToOne(targetEntity="Person", inversedBy="affiliations")
#ORM\JoinColumn(name="person_id", referencedColumnName="id")
protected $person;
#ORM\ManyToOne(targetEntity="Affiliation", inversedBy="person_affiliations")
#ORM\JoinColumn(name="affiliation_id", referencedColumnName="id")
protected $affiliation;
In the Entity User:
#ORM\OneToMany(targetEntity="UserAffiliation", mappedBy="user")
protected $affiliations;
#ORM\ManyToOne(targetEntity="Person")
#ORM\JoinColumn(name="person_id", referencedColumnName="id")
protected $person;
In the Entity UserAffiliation
#ORM\ManyToOne(targetEntity="User", inversedBy="affiliations")
#ORM\JoinColumn(name="user_id", referencedColumnName="id")
protected $user;
#ORM\ManyToOne(targetEntity="Affiliation", inversedBy="user_affiliations")
#ORM\JoinColumn(name="affiliation_id", referencedColumnName="id")
protected $affiliation;
In the form, I am doing the next:
$builder->add('affiliations', 'entity', array(
'class' => 'SciForumVersion2Bundle:PersonAffiliation',
'query_builder' => function($em) use ($person){
return $em->createQueryBuilder('pa')->where('pa.person_id = :id')->setParameter('id', $person->getId());
},
'property' => 'affiliation',
'multiple' => true,
'expanded' => true,
));
But all this is not working properly as I would like.
Explanation: When I try to add a new affiliation, it is only added for the WebUser and I can't link it through the form to the author (Person).
Do you have an idea on how to resolve this, or maybe a good tutorial?
This should be handled in the Entity1Controller.php:
public function createAction(Request $request)
{
$securityContext = $this->get('security.context');
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new Entity1Type()
,null,array('attr' => array('securitycontext' => $securityContext)));
$form->bind($request);
if ($form->isValid()){
$data = $form->getData();
$entity1id = $data->getId();
$entity2id = $data->getEntity2Id();
$entity1medicaid=$data->getMedicaidID();
$entity1=$em->getRepostiory('projectBundle:Entity1')->findOneById($entity1id);
$entity2=$em->getRepository('projectprojectBundle:Entity2')->findOneById($entity2id);
if (null === $entity1){
$entity1=new entity1();
$entity1->setEntity2id($entity2id);
$entity1->setID($entity1id);
}
if (null === $entity2){
$entity2=new entity2();
$entity2->setID($entity2id);
}
$em->persist($entity1);
$em->persist($entity2);
$em->flush();
return $this->redirect($this->generateUrl('entity1', array()));
}
return $this->render('Bundle:entity1:new.html.twig', array(
'form' => $form->createView()
,'attr' => array('securitycontext' => $securityContext
)
)
);
}
You may also have to set cascade persist in your association mappings. Entity1.yml:
project\projectBundle\Entity\Entity1:
type: entity
table: entity1
repositoryClass: project\projectBundle\Entity\Entity1Repository
fields:
id:
type: bigint
id: true
generator:
strategy: AUTO
property:
type: string
length: 255
unique: true
manyToMany:
entity2:
targetEntity: entity2
mappedBy: entity1
cascade: ["persist"]
In theory, symfony will make entity2 under the hood, making the second if null clause unnecessary, but that always bothers me, so I prefer to do it explicitly.
Well if this form is binding the collection to a WebUser Entity is because you are passing in to the Form creation in the Controller an object of such class, it means:
$webUser = new WebUser();
$this->createForm(new SubmissionAffiliationFormType(), $webUser);
or you are delegating the decision of which class to use to Symfony Forms by not setting the DefaultOptions and telling explicitly the data_class it must be bind to:
class SubmissionAffiliationFormType extends AbstractType
{
//...
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\DemoBundle\Entity\Person',
);
}
}
I have an issue with one of my many-to-many relations. I have debugged my form and I see that the chosen objects are actually in the entity object after form validation, but it never gets saved to the database, so there must be an issue with my mapping code, but I copy-pasted from a working one just substituted the fields and paths...
Here's the company object's relevant code
/**
* #ORM\ManyToMany(targetEntity="BizTV\ContentManagementBundle\Entity\Template", inversedBy="companies")
* #ORM\JoinTable(name="templatePermissions")
*/
private $templatePermissions;
/**
* Add templatePermissions
*
* #param BizTV\ContentManagementBundle\Entity\Template $templatePermissions
*/
public function addTemplate(\BizTV\ContentManagementBundle\Entity\Template $templatePermissions)
{
$this->templatePermissions[] = $templatePermissions;
}
and the template object
/**
* #ORM\ManyToMany(targetEntity="BizTV\BackendBundle\Entity\company", mappedBy="templatePermissions")
*/
private $companies;
/**
* Add companies
*
* #param BizTV\BackendBundle\Entity\company $companies
*/
public function addCompany(\BizTV\BackendBundle\Entity\company $companies)
{
$this->companies[] = $companies;
}
/**
* Get companies
*
* #return Doctrine\Common\Collections\Collection
*/
public function getCompanies()
{
return $this->companies;
}
The code for updating (and creating is similar, and has the same problem) is just a standard ...
public function updateAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('BizTVContentManagementBundle:Template')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Template entity.');
}
$editForm = $this->createForm(new TemplateType(), $entity);
$deleteForm = $this->createDeleteForm($id);
$request = $this->getRequest();
$editForm->bindRequest($request);
if ($editForm->isValid()) {
//DEBUG
// foreach($entity->getCompanies() as $c) {
// echo $c->getCompanyName();
// }
// die;
$em->persist($entity);
$em->flush();
$this->getRequest()->getSession()->setFlash('success', 'Template '.$entity->getId().' har uppdaterats.' );
return $this->redirect($this->generateUrl('listTemplates'));
}
Here's my form, like I said, it works perfectly for putting the stuff into my object (template entity) but it doesn't get persisted to DB...
$builder
->add('companies', 'entity', array(
'label' => 'Företag som har tillgång till mallen',
'multiple' => true, // Multiple selection allowed
'expanded' => true, // Render as checkboxes
'property' => 'company_name',
'class' => 'BizTV\BackendBundle\Entity\company',
))
;
What am I missing?
Pay attention for picking owning/inverse side. The owning side should be the one which is responsible for persist action. In your case owning side should be Template entity not Company.
For more info look here:
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/association-mapping.html#picking-owning-and-inverse-side
Have you company entity class name starts with lower case? If no, you have a typo:
BizTV\BackendBundle\Entity\company
try add cascade on persist
At the end your relation definition in Company entity should look sth like this:
/**
* #ORM\ManyToMany(targetEntity="BizTV\BackendBundle\Entity\company", inversedBy="templatePermissions", cascade={"persist"})
*/
private $companies;