I've encountered a problem regarding removing objects, or - to be precise - removing their associations in Symfony2.
Let's assume that we've got 3 entities - Parent, Child and a Grandchild.
For the sake of clarity I'll stick to this erhm, naming.
When removing Parent, I want the association between Parent-Child, and Child-Grandchild to be removed, that is, without any additional dirty workaround; I'd like the ORM layer to do the job.
So, without further ado, here are entities with annotations:
class Parent
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Children", mappedBy="Parent")
*/
private $children;
class Children
/**
* #var Parent
*
* #ORM\ManyToOne(targetEntity="Parent", inversedBy="Children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Grandchildren", mappedBy="Children")
*/
private $Grandchildren;
class GrandChildren
/**
* #var Children
*
* #ORM\ManyToOne(targetEntity="Children", inversedBy="Grandchildren")
* #ORM\JoinColumn(name="children_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $children;
And here is a simple standard deleteAction of a Parent:
public function deleteAction(Request $request, $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$parent = $em->getRepository('AppBundle:Parent')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Parent entity.');
}
$children = $em->getRepository('AppBundle:Children')->findBy(
array(
'parent' => $id,
)
);
$em->remove($parent);
$em->flush();
}
return $this->redirect($this->generateUrl('parent'));
}
With a code as above, after flush, the relationship between Parent-Children disappears (the join column is nulled), but a relation between Children-Grandchildren remains (i.e. the join column children_id isn't reseted to NULL as in Parent-Children situation, the column remains populated with id's).
So, what is the best way to establish this cascading removal? So that when Parent is removed the inherited associations are removed as well. According to the documentation I should do something like: http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-tags-to-be-removed
that is - just iterate through it, preferably encapsulate the process.
But still it doesn't look very clean to me, anyone has a better, cleaner solution?
Any insight would be helpful, cheers.
Related
I have an Account entity which has a collection of Section entities. Each Section entity has a collection of Element entities (OneToMany association). My problem is that instead of fetching all elements belonging to a section, I want to fetch all elements that belong to a section and are associated with a specific account. Below is my database model.
Thus, when I fetch an account, I want to be able to loop through its associated sections (this part is no problem), and for each section, I want to loop through its elements that are associated with the fetched account. Right now I have the following code.
$repository = $this->objectManager->getRepository('MyModule\Entity\Account');
$account = $repository->find(1);
foreach ($account->getSections() as $section) {
foreach ($section->getElements() as $element) {
echo $element->getName() . PHP_EOL;
}
}
The problem is that it fetches all elements belonging to a given section, regardless of which account they are associated with. The generated SQL for fetching a section's elements is as follows.
SELECT t0.id AS id1, t0.name AS name2, t0.section_id AS section_id3
FROM mydb.element t0
WHERE t0.section_id = ?
What I need it to do is something like the below (could be any other approach). It is important that the filtering is done with SQL.
SELECT e.id, e.name, e.section_id
FROM element AS e
INNER JOIN account_element AS ae ON (ae.element_id = e.id)
WHERE ae.account_id = ?
AND e.section_id = ?
I do know that I can write a method getElementsBySection($accountId) or similar in a custom repository and use DQL. If I can do that and somehow override the getElements() method on the Section entity, then that would be perfect. I would just very much prefer if there would be a way to do this through association mappings or at least by using existing getter methods. Ideally, when using an account object, I would like to be able to loop like in the code snippet above so that the "account constraint" is abstracted when using the object. That is, the user of the object does not need to call getElementsByAccount() or similar on a Section object, because it seems less intuitive.
I looked into the Criteria object, but as far as I remember, it cannot be used for filtering on associations.
So, what is the best way to accomplish this? Is it possible without "manually" assembling the Section entity with elements through the use of DQL queries? My current (and shortened) source code can be seen below. Thanks a lot in advance!
/**
* #ORM\Entity
*/
class Account
{
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Section")
* #ORM\JoinTable(name="account_section",
* joinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="section_id", referencedColumnName="id")}
* )
*/
protected $sections;
public function __construct()
{
$this->sections = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Section
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="MyModule\Entity\Element", mappedBy="section")
*/
protected $elements;
public function __construct()
{
$this->elements = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Element
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var Section
* #ORM\ManyToOne(targetEntity="MyModule\Entity\Section", inversedBy="elements")
* #ORM\JoinColumn(name="section_id", referencedColumnName="id")
*/
protected $section;
/**
* #var \MyModule\Entity\Account
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Account")
* #ORM\JoinTable(name="account_element",
* joinColumns={#ORM\JoinColumn(name="element_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")}
* )
*/
protected $account;
// Getters and setters
}
If I understand correctly, you want to be able to retrieve all Elements of all Sections of an Account, but only if those Elements are associated with that Account, and this from a getter in Account.
First off: An entity should never know of repositories. This breaks a design principle that helps you swap out the persistence layer. That's why you cannot simple access a repository from within an entity.
Getters only
If you only want to use getters in the entities, you can solve this by adding to following 2 methods:
class Section
{
/**
* #param Account $accout
* #return Element[]
*/
public function getElementsByAccount(Account $accout)
{
$elements = array();
foreach ($this->getElements() as $element) {
if ($element->getAccount() === $account) {
$elements[] = $element->getAccount();
}
}
return $elements;
}
}
class Account
{
/**
* #return Element[]
*/
public function getMyElements()
{
$elements = array()
foreach ($this->getSections() as $section) {
foreach ($section->getElementsByAccount($this) as $element) {
$elements[] = $element;
}
}
return $elements;
}
}
Repository
The solution above is likely to perform several queries, the exact amount depending on how many Sections and Elements are associated to the Account.
You're likely to get a performance boost when you do use a Repository method, so you can optimize the query/queries used to retrieve what you want.
An example:
class ElementRepository extends EntityRepository
{
/**
* #param Account $account [description]
* #return Element[]
*/
public function findElementsByAccount(Account $account)
{
$dql = <<< 'EOQ'
SELECT e FROM Element e
JOIN e.section s
JOIN s.accounts a
WHERE e.account = ?1 AND a.id = ?2
EOQ;
$q = $this->getEntityManager()->createQuery($dql);
$q->setParameters(array(
1 => $account->getId(),
2 => $account->getId()
));
return $q->getResult();
}
}
PS: For this query to work, you'll need to define the ManyToMany association between Section and Account as a bidirectional one.
Proxy method
A hybrid solution would be to add a proxy method to Account, that forwards the call to the repository you pass to it.
class Account
{
/**
* #param ElementRepository $repository
* #return Element[]
*/
public function getMyElements(ElementRepository $repository)
{
return $repository->findElementsByAccount($this);
}
}
This way the entity still doesn't know of repositories, but you allow one to be passed to it.
When implementing this, don't have ElementRepository extend EntityRepository, but inject the EntityRepository upon creation. This way you can still swap out the persistence layer without altering your entities.
I have a project in Symfony 2.3, using Doctrine ORM 2.3.4, and I'm using class inheritance:
a parent class
/**
* #ORM\Entity
* #ORM\Table(name="parent")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"child"="Child"})
*/
class Parent
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getId()
{
return $this->id;
}
// other fields & methods
}
and a child one
/**
* #ORM\Entity
* #ORM\Table(name="child")
*/
class Child extends Parent
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
*/
private $id;
public function getId()
{
return $this->id;
}
}
The problem comes when I persist the child object, flush and then I try to retrieve the child id:
// ChildController::createAction
$em = $this->getDoctrine()->getManager();
$child = new Child();
// set child fields
$em->persist($child);
$em->flush();
$child->getId(); // <- not working
On the database the child row is saved correctly, and if I change the child method getId
public function getId()
{
return parent::getId();
}
it works.
Can anyone please explain this to me?
Many thanks.
The parent entity needs to give visibility of it's properties to it's children.
Change your $id property visibility to "protected".
It's a little bit late, but maybe it helps others.
When you take a look at your table definition that Doctrine generated, you will see why it is this way. E.g. mine in postgres:
...
CONSTRAINT fk_5d9f75a1bf396750 FOREIGN KEY (id)
REFERENCES parent (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE CASCADE
...
As you can see, Doctrine uses for your child table id the id of the parent.
Like #John Cartwright said, make your $id in parent protected.
In addition to this define the getter only in the parent and everything works just fine.
Background:
In my application I have an entity that has a self referencing ManyToOne association (many children can point to a single parent). And I have a feature that does mass updates on many entities at one time using the Doctrine ORM. To keep performance from dropping dramatically due to many entities being loaded I detach entities once they've been updated.
Problem:
When I detach an entity that has children and later try to update any of those children Doctrine complains that it doesn't know the parent anymore. Even if I merge the parent entity before trying to update the child.
Question:
What am I doing wrong when I detach the parent entity? I've tried doing cascade="merge" and/or "detach" on the parent column and Doctrine still complains about the parent being an unknown entity when I try to persist.
I've mocked up a simple example that reproduces this. See below.
Test Code:
Entity\Thing.php
/**
* #ORM\Entity()
* #ORM\Table(name="things")
*/
class Thing
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Thing", inversedBy="children", cascade={"detach","merge"})
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent;
/**
* #ORM\OneToMany(targetEntity="Thing", mappedBy="parent")
*/
protected $children;
/**
* #ORM\Column(type="string", length=64)
*/
protected $name;
public function __construct($name = null)
{
$this->children = new ArrayCollection();
$this->name = $name;
}
// .. SNIP ...
}
Test Action:
public function testThingAction($_route)
{
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeThingBundle:Thing');
// simple setup of a couple things in the DB
$t1 = $repo->findByName('Thing1');
if (!$t1) {
$t1 = new Thing('Thing1');
$t2 = new Thing('Thing2');
$t2->setParent($t1);
$em->persist($t1);
$em->persist($t2);
$em->flush();
return $this->redirect($this->generateUrl($_route));
}
list($t1, $t2) = $repo->findAll();
// detach and re-merge Thing1
// This should cause Thing1 to be removed and then re-added
// to the doctrine's known entities; but it doesn't!?
$em->detach($t1);
$em->merge($t1);
// try to update T2
$t2->setName('Thing2 - ' . time());
$em->persist($t2);
// will fail with:
// A new entity was found through the relationship Thing#parent
$em->flush();
return array();
}
The issue is that the child has a relationship to a specific parent object that is no longer managed by Doctrine. When you call $entityManager->merge($entity) you get a new managed entity back from that function.
When you get that back, you need to manually call setParent() on each of your children with the newly managed entity.
Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2
The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).
I've used a pretty simple example of a Classroom having many Pupils to keep it simple.
So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.
Lets first define our Doctrine Entities.
The Classroom Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="classroom")
*/
class Classroom
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $miscVars;
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
*/
protected $pupils;
public function __construct()
{
$this->pupils = new ArrayCollection();
}
// ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============
}
The Pupil Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="pupil")
*/
class Pupil
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $moreVars;
/**
* #ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
* #ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
*/
protected $classroom;
// ========== GENERATED FUNCTIONS BELOW ============
}
And our generic Action function:
public function someAction(Request $request, $id)
{
$em = $this->getDoctrine()->getEntityManager();
$classroom = $em->find('AcmeTestBundle:Classroom', $id);
$form = $this->createForm(new ClassroomType(), $classroom);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// Normally you would do the following:
$em->persist($classroom);
$em->flush();
// But how do I create a new row with a new ID
// Including new rows for the Many side of the relationship
// ... other code goes here.
}
}
return $this->render('AcmeTestBundle:Default:index.html.twig');
}
I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.
Thanks in advance to any assistance.
The thing with clone is...
When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.
If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:
public function __clone() {
// Get current collection
$pupils = $this->getPupils();
$this->pupils = new ArrayCollection();
foreach ($pupils as $pupil) {
$clonePupil = clone $pupil;
$this->pupils->add($clonePupil);
$clonePupil->setClassroom($this);
}
}
NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.
When you clone your parent class, this function will create a new collection full of child object clones as well.
$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();
$em->persist($cloneClassroom);
$em->flush();
You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
*/
protected $pupils;
I did it like this and it works fine.
Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.
/**
* Clone element with values
*/
public function __clone(){
// we gonna clone existing element
if($this->id){
// get values (one-to-many)
/** #var \Doctrine\Common\Collections\Collection $values */
$values = $this->getElementValues();
// reset id
$this->id = null;
// reset values
$this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
// if we had values
if(!$values->isEmpty()){
foreach ($values as $value) {
// clone it
$clonedValue = clone $value;
// add to collection
$this->addElementValues($clonedValue);
}
}
}
}
/**
* addElementValues
*
* #param \YourBundle\Entity\ElementValue $elementValue
* #return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
if (!$this->getElementValues()->contains($elementValue))
{
$this->elementValues[] = $elementValue;
$elementValue->setElement($this);
}
return $this;
}
Somewhere just clone it:
// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
I do this:
if ($form->isValid()) {
foreach($classroom->getPupils() as $pupil) {
$pupil->setClassroom($classroom);
}
$em->persist($classroom);
$em->flush();
}
I have an Entity that looks like this:
class Privilege
{
/**
* #Id #Column(type="bigint")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Column(type="string",length=255)
*/
private $name;
/**
* #Column(type="string",length=255)
*/
private $slug;
/**
* #OneToMany(targetEntity="Privilege", mappedBy="parent")
*/
private $children;
/**
* #ManyToOne(targetEntity="Privilege", inversedBy="children")
* #JoinColumn(name="p_id", referencedColumnName="id")
*/
private $parent;
If a Privilege Entity does not have a parent, the field is NULL. I have a basic query like this:
$qb = $this->em->createQueryBuilder()
->select('p')
->from('\Dashboard\Entity\Privilege', 'p')
->andWhere('p.parent IS NULL');
$q = $qb->getQuery();
$privileges = $q->getResult();
I would like the array result I return from this method to look similar to this:
root1:
child1:
subchild1a
subchild2a
child2:
subchild1b
subchild2b
subchild3b
subsubchild1b
child3:
subchild1c
root2:
....
....
Is there a way to HYDRATE the results from Doctrine 2 so it builds the array results this way? If not, how would you build this array? I am still playing around with Doctrine 2, and I noticed each element in my $privileges array has a $privilege->getChildren() which returns a PersistentCollection, obviously not the actual record.
If I have to build this nested tree myself (ie: no built in way in Doctrine to do it), how do I turn this PersistentCollection returned into the actual data so I can build some sort of recursive method to build it for me? I am looking through the docs, but obviously in the wrong place.
The results are already in a nested tree. The PersistentCollection can be iterated as if it was an array:
foreach($parent->getChildren() as $child) {
// $child is an instance of Privilige
}
Still you should try $privileges = $q->getArrayResult(); and see if that gives a result you would prefer.
I think what you're looking for is called "One-To-Many self-referenced association" in documentation: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-one-self-referencing
Here's code for hierarchy of categories from the docs:
<?php
/** #Entity **/
class Category
{
// ...
/**
* #OneToMany(targetEntity="Category", mappedBy="parent")
**/
private $children;
/**
* #ManyToOne(targetEntity="Category", inversedBy="children")
* #JoinColumn(name="parent_id", referencedColumnName="id")
**/
private $parent;
// ...
public function __construct() {
$this->children = new \Doctrine\Common\Collections\ArrayCollection();
}
}
"This effectively models a hierarchy of categories and from the database perspective is known as an adjacency list approach."
So I think this should do all the job for you, and create the hierarchy of arrays you need.
Since you already have your annotations like in docs, your $parent->getChildren() should already contain all the hierarchy, as #rojoca said.