How to update an unidirectional ManyToMany mapping in Symfony2? - symfony

I have been searching for an answer now for several hours and I can't find a clean solution by myself.
I have an entity called "Tag". You can add these tags to nearly everything, f.e. to an article, a news and so on. Therefor my entites (in this example an article) refer to these tags with an unidirectional ManyToMany mapping:
/**
* #ORM\ManyToMany(targetEntity="Tag")
* #ORM\JoinTable(name="tag2article",
* joinColumns={#ORM\JoinColumn(name="article_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id")}
* )
*/
private $tags;
This relation is unidirectional, because then I don't want to have an articleTag, a newsTag etc. or several entities as joinTables. The doctrine manual calls unidirection ManyToMany relations "less common", because you can build such relations as 3 entities with an additional joinTableEntity. But this would result in too much entities of this kind.
When I create an article, these tags are added by:
foreach ($tagArray as $tagId) {
$tag = $entityManager->getRepository('myBundle:Tag')->findOneById($tagId);
if ($tag != null) {
$article->addTag($tag);
}
}
$entityManager->persist($article);
$entityManager->flush();
This works fine on insert. On update I do the same, but it doesn't work and I don't know why. The only solutions I found was to add this article to the tag, but this isn't possible though I am working with an unidirectonal relation.
In my ArticleForm, I just have a hidden field:
->add('tags', 'hidden', array(
'data' => '',
'property_path' => false
))
In this way I can add tags pretty easy with Ajax and write these tagIds in this hidden field.
My overall question is: Why does this way work on insert, but not on update? What can I do to fix this?
Many thanks for your help or any hints!!

Related

Symfony 3 - Update Many-To-Many

I have been looking around for a clean solution on how to update (keep in sync) a many to many relationship?
I have the following scenario:
A Sprint Entity owns the Many To Many relationship towards the Ticket entity.
When editing a Ticket (or Sprint, but I am not there yet), I want to be able to select (checkboxes) the Sprints that this ticket belongs to.
Upon persistance (save), I want to update my join table tickets_sprint (which is just a join table on ticket_id, sprint_id).
Adding Sprints to the Ticket seems easy enough, but removing Sprints from the Ticket is not reflected at all.
Code
Ticket Entity contains this method for adding a Ticket to a Sprint:
public function setSprints($sprints) {
/**
* #var $sprint \AppBundle\Entity\Sprint
*/
foreach ($sprints as $sprint) {
$this->sprints[] = $sprint;
$sprint->addTicket($this);
}
}
I have read here that the only way to go would be to remove all relations and re-save them upon persistance.
Coming from the Laravel world, this hardly feels like a good idea :)
This is how it is done in Laravel:
/**
* #param \App\User $user
* #param \App\Http\Requests\StoreUserRequest $request
* #return \Illuminate\Http\RedirectResponse
* Update the specified resource in storage.
*/
public function update(User $user, StoreUserRequest $request)
{
$user->fill($request->input());
$user->employee_code = strtolower($user->employee_code);
$user->roles()->sync($request->role ? : []);
$user->save();
\Session::flash('flash_message_success', 'The user was successfully updated.');
return redirect()->route('frontend::users.show', [$user]);
}
All suggestions are welcome!
The EntityType that you may use to create a multiple selectbox doesn't have a by_reference option like CollectionType.
If your Ticket Entity use the "inversedBy" side, you don't need to add the reference in the other object. So you can symply do this :
public function setSprints($sprints) {
$this->sprints = $sprints;
}
Maybe this will be enough to add and remove your elements automatically (Sorry didn't try).
Otherwise you have to do it manually and you can create a new method to remove elements returns by the difference between your new ArrayCollection and the old one.

How detach relation without deleting entity in Sonata AdminBundle?

I have two entities: Specialisation and Course.
One specialisation has many courses, so relations are "OneToMany" and "ManyToOne".
I want to create specialisations and courses separately and then attach many courses to specialisation through multiple select.
And I also need to remove(detach) courses from specialisation but without deleting courses-entities.
So, I did it such way:
->add('courses', 'sonata_type_model', [
'multiple' => true,
'property' => 'title',
])
But when I remove related course from select-field in specialisation-edit -page, course-object deleting from DB too.
I tried to remove orphanRemoval property from relation, but then when I try to detach courses from specialisation, nothing happens.
So, my question is:
How I can achieve only detaching child-entities from parent-entity in SonataAdminBundle?
I solved it!
Solution:
I decided to use save-hooks (methods prePersist and preUpdate in my SpecialisationAdmin Class).
The main idea - to unset all related courses from specialisation and then set those that came from form.
But if I remove any courses from specialisation on edit-page, I would not get their objects in specialisation object in preUpdate method.
And if I dont get courses objects, I cant set their specialisation to NULL.
So, the solution of this problem is to use snapshot property to get all courses that specialisation had before submitting form and set their specialisation to NULL, and then set current specialisation to courses that came from form:
/**
* #param Specialisation $specialisation
*/
public function prePersist($specialisation)
{
$this->preUpdate($specialisation);
}
/**
* #param Specialisation $specialisation
*/
public function preUpdate($specialisation)
{
if (isset($specialisation->getCourses()->snapshot)) {
foreach ($specialisation->getCourses()->getSnapshot() as $course) {
$course->setSpecialisation(null);
}
}
foreach ($specialisation->getCourses() as $course) {
$course->setSpecialisation($specialisation);
}
}

Symfony: Ordered one-to-many relationship

I have two separate entities that I want to link by a one-to-many relationship. But I want this relationship to be ordered, meaning every time I call on the first entity, the elements of the second entity come in a pre-ordered way. I cannot use 'order by' calls because this order has nothing to do with the fields of the second entity. I thought about having one field of the first entity be an array of entities, but I'm not sure how to accomplish that either..
EDIT
So far I have something like this:
/**
* #ORM\OneToMany(targetEntity="FusionDesign\BlogBundle\Entity\Element", mappedBy="page")
*/
private $elements;
and
/**
* #ORM\ManyToOne(targetEntity="FusionDesign\BlogBundle\Entity\Page", inversedBy="elements")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*/
private $page;
I'm aware that I can put "ORDER BY whatever ASC" somewhere in there but that orders according to a column in Element, and that's not what I need, because Element entities and Page entities would never be persisted at the same time, nor by the same process. What I want to do is constructing a basic CMS where the user could generate new pages. First choose the kind of elements a page could potentially have (like header image, banner, title, and so on) and persist Element entities with fields describing the html, routing and controller content according to those choices. Then, when a new page is created, give the user the choice to order those potential elements at will, and bind Element entities following an order that reflects the layout desired.
I thought about having something like this
/**
* #var array
*
* #ORM\Column(name="structure", type="array")
*/
private $structure;
Where the array stores Element entities but I have no idea how to do that.
You just need to define the orderBy attribute in doctrine's mapping configuration for the relation.
YAML Mapping Example:
'Example\Entity\Article':
# [..]
oneToMany:
images:
targetEntity: 'Example\Entity\Article\Image\ArticleImage'
mappedBy: 'article'
orderBy: # <--- here
position: 'ASC'

Doctrine select many from the one (many-to-one unidirectional (different bundles))

Working on a legacy project which restricts the options available, has left me in a situation where I need to solve the following problem, ideally with doctrine.
I have two entities in different bundles that have a unidirectional many-to-one link.
BundleA has dependency on BundleB and the entities are linked similar to this:
BundleA/Entity/TheMany:
/**
* #var TheOne $theOne
* #ORM\ManyToOne(targetEntity="BundleB\Entity\TheOne")
* #ORM\JoinColumn(name="theone_id", referencedColumnName="id", onDelete="SET NULL")
*
*/
private $theOne;
From BundleB I now need to select all TheOne entities, and for each I need all of the TheMany entities.
The query also needs to be sortable on any property of TheOne entity, or the count of related TheMany entities.
It is fairly simple in Doctrine to build a query which brings back all TheOne entities and one of TheMany for each... however I am having some difficulty coming up with a Doctrine query that will bring back all of the related TheMany entities rather than just one.
I was hoping someone might have encountered a similar issue and therefore have some insight?
This may not have been explained clearly enough, in which case please direct me to explain further.
In the end I was able to achieve what I needed by using GROUP_CONCAT (which required inclusion of https://github.com/beberlei/DoctrineExtensions).
The query looks something like this:
$queryBuilder->select(
'to,
GROUP_CONCAT(DISTINCT tm.id SEPARATOR \',\') as theManyIds,
COUNT(DISTINCT tm.id) as HIDDEN theManyCount'
)
->from('BundleB\Entity\TheOne', 'to')
->leftJoin(
'BundleA\Entity\TheMany',
'tm',
Join::WITH,
'to.id = tm.theOne'
)
->groupBy('to.id')
->orderBy($sortString, $direction)
->setFirstResult($start)
->setMaxResults($limit);
I compromised by accepting the consequences of linking the two bundles - however that could have been avoided by making use of Native SQL and Result Set Mapping (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/native-sql.html).
So what you are trying to do is to get all the ones and for each one find all the many. But you want to put all the many in one array or you want to create an array of array for the entities ? (what i did here)
$em = $this->getDoctrine()->getManager();
$theones = $em->getRepository('BundleA:theOne')
->createQueryBuilder('p')
->OrderBy(//your ordering)
->getQuery()
->getArrayResult()
$theManies = [];
for($theones as $theOne){
$theManies [] = $em->getRepository('BunbleB:theMany')
->createQueryBuilder('p')
->Where('p.theOne = :theOne')
->setParameter('theOne', $theOne)
->getQuery()
->getArrayResult();
$finalOnes[$theOne->getId()] = sizeof($theManies)
}
asort($finalOnes);
return array_keys($finalOnes);

Symfony2: Error persisting ManyToMany/OneToMany Relationships

I don't know why, maybe i am missing some basic logic but I always run again into the same issue. I can't persists ManyToMany collections, and it also faces me with OneToMany collections, though I can work around that.
I read through the doctrine documentation, and I think I do understand the thing with mappedBy and inversedBy (where the last one is always the owner and therefor responsible for persisting the data, please correct me if I am wrong).
So here's a basic example that I have right now, which I can't figure out.
I have an Entity called Site:
#Site.php
...
/**
* #ORM\ManyToMany(targetEntity="Category", mappedBy="sites")
*/
protected $categories;
and another one called Category:
#Category.php
...
/**
* #ORM\ManyToMany(targetEntity="Site", inversedBy="categories")
* #ORM\JoinTable(name="sites_categories")
*/
protected $sites;
Using the Symfony2 entity genenerator it added me some getters and setters to my Entites which look like this.
Site:
#Site.php
...
/**
* Add categories
*
* #param My\MyBundle\Entity\Category $categories
*/
public function addCategory(\My\MyBundle\Entity\Category $categories)
{
$this->categories[] = $categories;
}
/**
* Get categories
*
* #return Doctrine\Common\Collections\Collection
*/
public function getCategories()
{
return $this->categories;
}
The same counts for
Category:
#Category.php
...
/**
* Add sites
*
* #param My\MyBundle\Entity\Site $sites
*/
public function addSite(\My\MyBundle\Entity\Site $sites)
{
$this->sites[] = $sites;
}
/**
* Get sites
*
* #return Doctrine\Common\Collections\Collection
*/
public function getSites()
{
return $this->sites;
}
Fair enough.
Now in my controller, I am trying to persist a Site object:
public function newsiteAction() {
$site = new Site();
$form = $this->createFormBuilder($site); // generated with the FormBuilder, so the form includes Category Entity
// ... some more logic, like if(POST), bindRequest() etc.
if ($form->isValid()) {
$em = $this->getDoctrine()
->getEntityManager();
$em->persist($site);
$em->flush();
}
}
The result is always the same. It persists the Site Object, but not the Category entity. And I also know why (I think): Because the Category entity is the owning side.
But, do I always have to do something like this for persisting it? (which is actually my workaround for some OneToMany collections)
$categories = $form->get('categories')->getData();
foreach($categories as $category) {
// persist etc.
}
But I am running into many issues here, like I would have to do the same loop as above for deleting, editing etc.
Any hints? I will really give a cyber hug to the person who can clear my mind about that. Thanks!
.
.
.
UPDATE
I ended up changing around the relationship (owning and inverse side) between the ManyToMany mapping.
If somebody else runs into that problem, you need to be clear about the concept of bidrectional relationships, which took me a while to understand too (and I hope I got it now, see this link).
Basically what anserwed my question is: The object you want to persist must always be the owning site (The owning site is always the entity that has "inversed by" in the annotiation).
Also there is a concept of cascade annotation (see this link, thanks to moonwave99)
So thanks, and I hope that helps somebody for future reference! :)
Regarding OneToMany relationship, you want to know about cascade annotation - from Doctrine docs [8.6]:
The following cascade options exist:
persist : Cascades persist operations to the associated entities.
remove : Cascades remove operations to the associated entities.
merge : Cascades merge operations to the associated entities.
detach : Cascades detach operations to the associated entities.
all : Cascades persist, remove, merge and detach operations to associated entities.
following docs example:
<?php
class User
{
//...
/**
* Bidirectional - One-To-Many (INVERSE SIDE)
*
* #OneToMany(targetEntity="Comment", mappedBy="author", cascade={"persist", "remove"})
*/
private $commentsAuthored;
//...
}
When you add comments to the author, they get persisted as you save them - when you delete the author, comments say farewell too.
I had same issues when setting up a REST service lately, and cascade annotation got me rid of all the workarounds you mentioned before [which I used at the very beginning] - hope this was helpful.

Resources