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);
}
Related
So I'm starting a new Symfony 5 application, and having some trouble in a editing controller, imho, symfony is executing a dumb query.
So, below is the code of my controller and where is executing duplicate query and the queries. If anyone could help me out, would be very thankfull, If more code is needed, please let me know.
The repeated query, just occurs when submiting the request, not getting it. So just in the POST method.
EDIT: So... I found out, something that is causing, in formtype 'by_reference'=> false, but I want to use it. So I could solve it in a ugly way, doing:
$product = $this->productRepository->find($id);
$product->getImages()->initialize();
It looks like, if I pre load entity, it works fine, maybe because of something that by reference does, not take from entity cache, like the example in docs:
From symfony docs: https://symfony.com/doc/current/reference/forms/types/collection.html
// If you set by_reference to false, submitting looks like this:
$article->setTitle('...');
$author = clone $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);
class ProductController extends AbstractController
{
public function editProduct(Request $request, int $id)
{
// Here it executes the query for getting the product, its ok
$product = $this->productRepository->find($id);
// Here it executes the query of the images, for handling the forms
$form = $this->createForm(ProductType::class, $product);
// Here is the problem, it executes the same query AGAIN!
$form->handleRequest($request);
}
}
So, I don't know why when I call $form->handleRequest(), It executes a query, that already runned, bellow is some usefull code, that maybe I mess up.
Query runned twice:
SELECT t0.id AS id_1, t0.image AS image_2, t0.product_id AS product_id_3 FROM product_image t0 WHERE t0.product_id = ?
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('images', CollectionType::class, [
'label' => false,
'entry_type' => ImageType::class,
'entry_options' => [
'label' => false,
],
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
'attr' => ['class' => 'row text-center text-lg-left'],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Product::class,
]);
}
class: App\Entity\Catalog\Product
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\OneToMany(targetEntity="ProductImage", mappedBy="product", cascade={"persist"}, orphanRemoval=true)
*/
private $images;
/**
* #return ProductImage[]|ArrayCollection
*/
public function getImages()
{
return $this->images;
}
/**
* #param ProductImage $productImage
* #return Product
*/
public function addImage(ProductImage $productImage): self
{
$productImage->setProduct($this);
$this->images->add($productImage);
return $this;
}
/**
* #param ProductImage $productImage
* #return $this
*/
public function removeImage(ProductImage $productImage): self
{
$productImage->setProduct(null);
$this->images->removeElement($productImage);
return $this;
}
}
class: App\Entity\Catalog\ProductImage
/**
* #ORM\Entity(repositoryClass=ProductImageRepository::class)
*/
class ProductImage
{
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="images")
* #ORM\JoinColumn(name="product_id", onDelete="CASCADE")
*/
private $product;
}
I'm on Symfony 2.8. In my Work entity I've got:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\WorkClaim", mappedBy="claimedWork", cascade={"persist"})
*/
private $claims;
/**
* #param WorkClaim $claim
*/
public function addClaim(WorkClaim $claim)
{
$claim->setClaimedWork($this);
$this->claims->add($claim);
}
/**
* #param WorkClaim $claim
*/
public function removeClaim(WorkClaim $claim) { /* to be done */ }
(and getter/setter and of course $this->claims = new ArrayCollection(); in the Constructor)
In my WorkClaim entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Work", inversedBy="claims")
*/
private $claimedWork;
(and getter/setter)
My WorkType class has this in the form builder:
->add('claims', CollectionType::class, array(
'entry_type' => WorkClaimType::class,
'allow_add' => true,
'by_reference' => false,
))
WorkClaimType has just some typical fields (EntityType to choose country, two DateType fields and NumberType).
No matter what I do, the addClaim() is called, but the new WorkClaim entities are added with NULL claimed_work_id. What did I miss?
Do I really have to persist every WorkClaim separately in Work controller on createAction()? And if so, how to correctly iterate through them? Trying to call getClaims() on form data gives me no records.
I am using symfony2 with doctrine 2.
I have a many to many relationship between two entities :
/**
* #ORM\ManyToMany(targetEntity="\AppBundle\Entity\Social\PostCategory", inversedBy="posts")
* #ORM\JoinTable(
* name="post_postcategory",
* joinColumns={#ORM\JoinColumn(name="postId", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="postCategoryId", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
private $postCategories;
Now I want to let the user only select one category. For this I use the option 'multiple' => false in my form.
My form:
->add('postCategories', 'entity', array(
'label'=> 'Catégorie',
'required' => true,
'empty_data' => false,
'empty_value' => 'Sélectionnez une catégorie',
'class' => 'AppBundle\Entity\Social\PostCategory',
'multiple' => false,
'by_reference' => false,
'query_builder' => $queryBuilder,
'position' => array('before' => 'name'),
'attr' => array(
'data-toggle'=>"tooltip",
'data-placement'=>"top",
'title'=>"Choisissez la catégorie dans laquelle publier le feedback",
)))
This first gave me errors when saving and I had to change the setter as following :
/**
* #param \AppBundle\Entity\Social\PostCategory $postCategories
*
* #return Post
*/
public function setPostCategories($postCategories)
{
if (is_array($postCategories) || $postCategories instanceof Collection)
{
/** #var PostCategory $postCategory */
foreach ($postCategories as $postCategory)
{
$this->addPostCategory($postCategory);
}
}
else
{
$this->addPostCategory($postCategories);
}
return $this;
}
/**
* Add postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*
* #return Post
*/
public function addPostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$postCategory->addPost($this);
$this->postCategories[] = $postCategory;
return $this;
}
/**
* Remove postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*/
public function removePostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$this->postCategories->removeElement($postCategory);
}
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
/**
* Constructor
* #param null $user
*/
public function __construct($user = null)
{
$this->postCategories = new \Doctrine\Common\Collections\ArrayCollection();
}
Now, when editing a post, I also have an issue because it uses a getter which ouputs a collection, not a single entity, and my category field is not filled correctly.
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
It's working if I set 'multiple' => true but I don't want this, I want the user to only select one category and I don't want to only constraint this with asserts.
Of course there are cases when I want to let the user select many fields so I want to keep the manyToMany relationship.
What can I do ?
If you want to set the multiple option to false when adding to a ManyToMany collection, you can use a "fake" property on the entity by creating a couple of new getters and setters, and updating your form-building code.
(Interestingly, I saw this problem on my project only after upgrading to Symfony 2.7, which is what forced me to devise this solution.)
Here's an example using your entities. The example assumes you want validation (as that's slightly complicated, so makes this answer hopefully more useful to others!)
Add the following to your Post class:
public function setSingleCategory(PostCategory $category = null)
{
// When binding invalid data, this may be null
// But it'll be caught later by the constraint set up in the form builder
// So that's okay!
if (!$category) {
return;
}
$this->postCategories->add($category);
}
// Which one should it use for pre-filling the form's default data?
// That's defined by this getter. I think you probably just want the first?
public function getSingleCategory()
{
return $this->postCategories->first();
}
And now change this line in your form:
->add('postCategories', 'entity', array(
to be
->add('singleCategory', 'entity', array(
'constraints' => [
new NotNull(),
],
i.e. we've changed the field it references, and also added some inline validation - you can't set up validation via annotations as there is no property called singleCategory on your class, only some methods using that phrase.
You can setup you form type to not to use PostCategory by reference (set by_reference option to false)
This will force symfony forms to use addPostCategory and removePostCategory instead of setPostCategories.
UPD
1) You are mixing working with plain array and ArrayCollection. Choose one strategy. Getter will always output an ArrayCollection, because it should do so. If you want to force it to be plain array add ->toArray() method to getter
2) Also I understand that choice with multiple=false return an entity, while multiple=true return array independend of mapped relation (*toMany, or *toOne). So just try to remove setter from class and use only adder and remover if you want similar behavior on different cases.
/** #var ArrayCollection|PostCategory[] */
private $postCategories;
public function __construct()
{
$this->postCategories = new ArrayCollection();
}
public function addPostCategory(PostCategory $postCategory)
{
if (!$this->postCategories->contains($postCategory) {
$postCategory->addPost($this);
$this->postCategories->add($postCategory);
}
}
public function removePostCategory(PostCategory $postCategory)
{
if ($this->postCategories->contains($postCategory) {
$postCategory->removePost($this);
$this->postCategories->add($postCategory);
}
}
/**
* #return ArrayCollection|PostCategory[]
*/
public function getPostCategories()
{
return $this->postCategories;
}
In my case, the reason was that Doctrine does not have relation One-To-Many, Unidirectional with Join Table. In Documentations example is show haw we can do this caind of relation by ManyToMany (adding flag unique=true on second column).
This way is ok but Form component mixes himself.
Solution is to change geters and seters in entity class... even those generated automatically.
Here is my case (I hope someone will need it). Assumption: classic One-To-Many relation, Unidirectional with Join Table
Entity class:
/**
* #ORM\ManyToMany(targetEntity="B2B\AdminBundle\Entity\DictionaryValues")
* #ORM\JoinTable(
* name="users_responsibility",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="responsibility_id", referencedColumnName="id", unique=true, onDelete="CASCADE")}
* )
*/
private $responsibility;
/**
* Constructor
*/
public function __construct()
{
$this->responsibility = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*
* #return User
*/
public function setResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility = null)
{
if(count($this->responsibility) > 0){
foreach($this->responsibility as $item){
$this->removeResponsibility($item);
}
}
$this->responsibility[] = $responsibility;
return $this;
}
/**
* Remove responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*/
public function removeResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility)
{
$this->responsibility->removeElement($responsibility);
}
/**
* Get responsibility
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getResponsibility()
{
return $this->responsibility->first();
}
Form:
->add('responsibility', EntityType::class,
array(
'required' => false,
'label' => 'Obszar odpowiedzialności:',
'class' => DictionaryValues::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('n')
->where('n.parent = 2')
->orderBy('n.id', 'ASC');
},
'choice_label' => 'value',
'placeholder' => 'Wybierz',
'multiple' => false,
'constraints' => array(
new NotBlank()
)
)
)
I know its a pretty old question, but the problem is still valid today.
Using a simple inline data transformer did the trick for me.
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('profileTypes', EntityType::class, [
'multiple' => false,
'expanded' => true,
'class' => ProfileType::class,
]);
// data transformer so profileTypes does work with multiple => false
$builder->get('profileTypes')
->addModelTransformer(new CallbackTransformer(
// return first item from collection
fn ($data) => $data instanceof Collection && $data->count() ? $data->first() : $data,
// convert single ProfileType into collection
fn ($data) => $data && $data instanceof ProfileType ? new ArrayCollection([$data]) : $data
));
}
PS: Array functions are available in PHP 7.4 and above.
I have this mapping betwenn two entities:
class Orders {
// Other attributes
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order")
*/
protected $orderProducts;
// Other set/get methods
public function getOrderProducts()
{
return $this->orderProducts;
}
}
class Product {
// Other attributes
/**
* #ORM\OneToMany(targetEntity="\Tanane\FrontendBundle\Entity\OrderHasProduct", mappedBy="product")
*/
protected $orderProducts;
// Other set/get methods
public function getOrderProducts()
{
return $this->orderProducts;
}
}
And of course since many Orders can have many products but also there is an extra attribute this other entity is needed:
class OrderHasProduct
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
}
When I edit a order I should able to add/remove the products on that order but I don't know how to achieve this. I knew that I must use a form collection but how? I mean a collection should be embed as follow:
$builder->add('product', 'collection', array(
'type' => new OrderHasProductType(),
'allow_add' => true,
'allow_delete' => true
));
When I should create a new OrderHasProductType form and I think I understand until this point but my question now is, what happens to the ID of the order? What is the proper way to handle an embedded form a relationship n:m with extra parameters?
Can any give me some code example to order my ideas?
Extra resources
Orders Entity Complete Source
Product Entity Complete Source
Orders Form Type Complete Source
OrderHasProduct Form Type Complete Source
I think your situation is slightly complicated by having not a standard Doctrine many-to-many relationship with two Entities, but two separate one-to-many and many-to-one relationships, with three Entities.
Normally, with a full many-to-many, the process is to have, for example, an OrderType form, containing a Collection field full of ProductTypes representing the Products assigned to the Order.
('allow_add' => true means that if Symfony sees an entry with no ID it expects it to be a brand new item added via Javascript, and is happy to call the form Valid and add the new item to the Entity. 'allow_delete' => true conversely means that if one of the items is missing then Symfony will remove it from the Entity.)
However, you have one further level of Entities, it goes Order->OrderHasProduct->Product. So logically your OrderType form contains a Collection of OrderHasProductType forms (as you've put above), which in turn contains a ProductType form.
So your OrderType becomes more like this:
$builder->add('orderHasProducts', 'collection', array(
'type' => new OrderHasProductType(),
'allow_add' => true,
'allow_delete' => true
));
And you also have another level for the Products:
OrderHasProductType
$builder->add('product', 'collection', array(
'type' => new ProductType(),
'allow_add' => true,
'allow_delete' => true
));
And a ProductType as well:
$builder->add('product', 'entity', array(
'class' => 'ProductBundle:Product'
));
Symfony should be happy to map your Entities to the correct level of Types. In your View you will need to include Javascript which will understand that adding a Product to an Order also involves the ProductHasOrder level - best to put some data in and see how Symfony turns that into a form, and then mimic the structure in the Javascript.
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!!