Doctrine2 one-to-many relation overwrite whole collection - php

Suppose I have two entities Page and Block. It's bi-directional mapping. Each page can have more than one block. Each block could belong to single page.
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
...
/**
* #ORM\OneToMany(targetEntity="Block", mappedBy="page", cascade={"all"}, orphanRemoval=true)
**/
private $blocks;
...
}
/**
* #ORM\Entity
* #ORM\Table(name="block")
*/
class Block
{
...
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="blocks")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*
**/
private $page;
...
}
From time to time blocks that belong to page will change. Some blocks will be added, some of them will be removed. The next case doesn't work for me:
$page->setBlocks([1, 2, 3]);
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3, 4
Expected result after second flush() call is: //Page will have 1, 4
So I need to overwrite completely collection of blocks with merge method.
Constraints:
I can't implement deleteBlock in Page class
I can only call merge() and flush() on $em
Is it possible to implement desired result via annotations or some other trick?

Addressing the Constraints
It cannot be done with your constraints.
Suggested Solution
I'd create a custom function in the BlockRepository Class that deletes all blocks of the given Page, then call that function before calling the setBlock function.
Add this to your Block Repository class:
public function deleteBlocksByPageId($page_id)
{
$qb = $this->createQueryBuilder()
->delete('Block', 'b');
->where('b.page', ':page'));
->setParameter(':page', $page_id);
$numDeleted = $qb->execute();
return $numDeleted;
}
In your Controller:
//Delete all the existing blocks before adding them back in.
$repo = $this->getDoctrine()->getRepository('BundleName:BlockRepository');
$repo->deleteBlocksByPageId($page->getId());
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush()
Conclusion
This is a common pattern for managing items that change often. It required no loops.
If the constraints are self-imposed then you need to loosen them and find another solution, such as the one I offer above. If they are constraints imposed by some higher-up then it's time to have a conversation with him/her about it being counter productive in this instance.

Related

Saving Related Entities Doctrine/Symfony2

I have one specific issue. I have two entities:
class MyPlaylist {
...
/**
* #var Array
* #ORM\OneToMany(targetEntity="MyPlaylistContent", mappedBy="myPlaylist", orphanRemoval=true)
* #ORM\OrderBy({"position" = "DESC"})
*/
private $myPlaylistItems;
and
class MyPlaylistContent {
....
/**
* #ORM\ManyToOne(targetEntity="MyPlaylist", inversedBy="myPlaylistItems")
*/
private $myPlaylist;
Now I have this in my service
....
$myPlaylist = new MyPlaylist();
$myPlaylist->setUser($user);
$myPlaylist->setActive(true);
// add tracks
foreach ($playlist->getMyPlaylistItems() as $item) {
$entity = new MyPlaylistContent();
$entity->setTrack($item->getTrack());
$entity->setMyPlaylist($myPlaylist);
$this->em->persist($entity);
}
$this->em->persist($myPlaylist);
$this->em->flush();
\Doctrine\Common\Util\Debug::dump($myPlaylist);
return $myPlaylist;
so, I return a new playlist. If I look at the database, all works fine. I have both entities and in MyPlaylistContent - 3 tracks. But
\Doctrine\Common\Util\Debug::dump($myPlaylist); shows next
["active"]=> bool(true) ["myPlaylistItems"]=> array(0) { }
On the page, the app shows the empty playlist (no tracks). If I refresh the page, I can see all tracks.
The point is, if you open the page, the controller will call the service, build the content and return the list as a response.
It looks as the same example, but it does not work for me
http://symfony.com/doc/current/book/doctrine.html#saving-related-entities
What is wrong here? Why don't I get tracks for the current entity?
You forget to add MyPlaylistContent to MyPlaylist.
Use this snippet into foreach
$myPlaylist->addMyPlaylistContent($myPlaylistContent);
Of course change name or implement method accordingly
First note: this is because objects are "normal" php objects, they have nothing to do with doctrine so, relationships are only a doctrine concept. EntityManager in doctrine will handle this kind of processes, not php itself. If you take a look to your classes methods you will probably notice that no "connection" (assignments) are made between those objects. If you would like, you can modify MyPlaylistContent to add itself to MyPlaylist once assigned.
Something like
class MyPlaylistContent
{
[...]
public function setMyPlaylist(MyPlaylist $mp)
{
$this->myPlaylist = $myPlaylist;
$mp->addMyPlaylistContent($this);
return $this;
}
Second note: hope your names are more consistents of these ones :)

Hierarchal data with Doctrine2 using closure table model

I have some existing data stored using the closure table model. I'm new to Doctrine, and trying to implement an Entity for this the "Doctrine way", and not really sure how to proceed. The philosophy I'm trying to follow is that the Entity should just be a plain-old-PHP-object, and that some kind of annotation should be used to configure the parent-child associations.
In this post I'll use Category as an example entity. Here's what I imagine the entity looking like:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table(name="categories)
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Column(name="categoryID", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $categoryID;
/**
* #ORM\Column(name="title", type="string", length=255)
*/
protected $title;
/**
* #MyORM\TreeParent(targetEntity="Category", closureTable="categories_paths", ancestorColumn="ancestorID", descendantColumn="descendantID")
*/
protected $parent;
/**
* #MyORM\TreeChildren(targetEntity="Category", closureTable="categories_paths", ancestorColumn="ancestorID", descendantColumn="descendantID")
*/
protected $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getChildren()
{
return $this->children;
}
public function addChild(Category $child)
{
$this->children[] = $children;
}
public function getParent()
{
return $this->parent;
}
public function setParent(Category $parent)
{
$this->parent = $parent;
}
}
The closure table looks as follows:
categories_paths(ancestorID, descendantID, pathLength)
This table is essentially a join table -- it only stores the parent-child relations, so I don't think it makes sense for there to be an entity here, similar to how there's no entity when creating a many-to-many relationship with #JoinTable.
I'd like to be able to use my Category entity like any other Entity, with $parent / $children populated when I fetch it from the repository and when $em->flush() is called, have SQL executed to reflect newly added children.
Some examples of SQL used here:
Add a new child:
INSERT INTO categories_paths (ancestorID, descendantID, pathLength)
SELECT a.ancestorID, d.descendantID, a.pathLength+d.pathLength+1
FROM categories_paths a, categories_paths d
WHERE a.descendantID = $parentCategoryID AND d.ancestorID = $childCategoryID
Move a subtree to a new parent:
// Delete all paths that end at $child
DELETE a FROM categories_paths a
JOIN categories_paths d ON a.descendantID=d.descendantID
LEFT JOIN categories_paths x
ON x.ancestorID=d.ancestorID AND x.descendantID=a.ancestorID
WHERE d.ancestorID = $subtreeCategoryID and x.ancestorID IS NULL
// Add new paths
INSERT INTO categories_paths (ancestorID, descendantID, pathLength)
SELECT parent.ancestorID, subtree.descendantID,
parent.pathLength+subtree.pathLength+1
FROM categories_paths parent
JOIN categories_paths subtree
WHERE subtree.ancestorID = $subtreeCategoryID
AND parent.descendantID = $parentCategoryID;
Get all children of a Category:
SELECT * FROM categories
JOIN categories_paths cp ON cp.descendantID=categories.categoryID
WHERE cp.ancestorID = $catogeryID
AND cp.depth=1
I have a few questions here. First of all, does this seem like a reasonable approach / something that is possible to implement with Doctrine? If not, is there a better way to approach this?
If this does seem like a reasonable approach, I'm wondering how to go about attacking this? I'm more looking for where I need to put these files / how I need to set up classes vs. someone giving me an actual implementation. Any documentation or examples that would help me get started would be much appreciated. I have pretty much zero experience with Doctrine--hopefully I'm not missing anything obvious here.
I think if you want to build a hierarchical database you should look for the doctrine ODM project. All the things you want are built in into that and you can customize your node.
There's a mongoDB adapter and also you can take a look at DoctrinePHPCR project that has adapters for several databases.
Even if you want to implement your own approach using doctrine ORM you can look at their implementations to get an idea how they work. They have node based relationship so you always have reference to adjacent nodes in the tree in your object.
Hope that helps.

Removing OneToMany elements, Doctrine2

I've got this model;
Itinerary, Venue, ItineraryVenue.
I needed many to many relation between itineraries and venues but also I wanted to store some specific data about the relation (say notes, own photo, etc.), so I decided to introduce a new entity named ItineraryVenue.
So Itinerary has collection of ItineraryVenues which in turn, refer to Venues.
My problem is that I can't remove ItineraryVenue from a Itinerary object.
$itinerary->itineraryVenues->removeElement($itineraryVenue);
$em->flush();
removes element from the php collection, but doesn't remove this $itineraryVenue from database.
I've managed to force Doctrine2 to remove $itineraryVenue, but only when I annotate the Itinerary::$itineraryVenues with orphanRemoval=true.
Since orphan removal treats Venue as a private property it also removes Venue entity, I don't want that.
Is there an relation configuration option or is removing "by hand" the olny way to make it work as I want?
Hard to believe it, it's a common relation pattern.
Entities definitions:
class Itinerary
{
/**
* #ORM\OneToMany(targetEntity="ItineraryVenue", mappedBy="itinerary", cascade={"persist", "remove"})
*/
private $itineraryVenues;
function __construct()
{
$this->itineraryVenues = new ArrayCollection();
}
}
class ItineraryVenue
{
/**
* #ORM\ManyToOne(targetEntity="Itinerary", inversedBy="itineraryVenues")
*/
private $itinerary;
/**
* #ORM\ManyToOne(targetEntity="Venue")
*/
private $venue;
function __construct()
{
}
}
class Venue
{
}
You are doing things right: orphanRemoval - is what you need. So, you should override default Itinerary::removeItineraryVenue like
public function removeItineraryVenue(\AppBundle\Entity\ItineraryVenue $itineraryVenue)
{
$itineraryVenue->setItinerary(null);
$this->itineraryVenues->removeElement($itineraryVenue);
}
The full working example is here https://github.com/kaduev13/removing-onetomany-elements-doctrine2.

optional multiple oneToMany relation in symfony2

I have entities in Doctrine Symfony2: User, Channel, Video and Comment; user can report one of them. I designed Report entity with these fields:
userId
status
reportTime
description
how can I reference to reported Entity ?? because all reported fields are similar for all entities I want to use just one table for Report and add these fields to Report Entity:
referenceEntityName(a string and may be one of these: User, Channel, Video, Comment)
Channel(ManytoOne relation to Channel entity)
Video(ManytoOne relation to Video entity)
Comment(ManytoOne relation to Comment entity)
User(ManytoOne relation to User entity)
Is this best practice or I should create separate tables for each kind of report ??
Edit:
based on #Alex answer, I improved Report class and add these methods:
setEntity($entity){
if ($obj instanceof Video){
$this->referenceEntityName = 'Video';
$this->setVideo();
}
elseif($obj instanceof Comment){
$this->referenceEntityName == 'Comment'
$this->setComment();
}
//...
}
getEntity(){
if($this->referenceEntityName == 'Video'){
$this->getVideo()
}// ifelse statements for other entities ...
}
I till have 4 relation that just one of them is used for each instance, isn't it a bit messy!?
and again is this best practice or I should do something else?
what if I want to use FormBuilder class, isn't there any problem??
In a simple solution, whereby for example you only had Users (and not Videos, Comments and Channels), the solution would be simple; each User can have many Reports, and each Report must belong to only one User. This is a one-to-many relationship - one User has many Reports. In Symfony 2 and Doctrine, this would be modelled as such:
// src/Acme/DemoBundle/Entity/User.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class User
{
// ...
/**
* #ORM\OneToMany(targetEntity="Report", mappedBy="user")
*/
protected $reports;
public function __construct()
{
$this->reports = new ArrayCollection();
}
// ...
}
and
// src/Acme/DemoBundle/Entity/Report.php
// ...
class Report
{
// ...
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="reports")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
// ...
}
In this instance, to create a Report and associate it with a User, we would:
// get the User the Report will belong to
$user = $em->getRepository('AcmeDemoBundle:User')->find(1);
// create the Report
$report = new Report();
// add the User to the Report
$report->setUser($user);
// then persist it, etc ...
Note, the setUser() method is available because the console command was run to generate them automatically. This is highly recommended as it created the necessary type hinting for you. For pre Symfony 2.5 installations, the command is:
php app/console doctrine:generate:entities Acme
>= 2.5 installations, the command is:
php bin/console doctrine:generate:entities Acme
Your requirements complicate this simple example somewhat, as Reports can also belong to Comments and Videos etc. For the sake of the example, let's call these things Entities. A bad approach would be to simply add 3 new properties to the Report, one for each of the new Entities, and then add 3 new setter methods for the Entities. This is bad for 2 reasons: a Report will only ever belong to one of the Entities, and therefore 3 of the properties and setter methods will never be used for each Report entity. Secondly, if you add a new Entity to your business model, or remove one, you need to edit your Report entity, and also the database schema.
A better method is to simply have one property and set method in your Report, that can be applied to all of your Entities. So instead of calling setUser, we could call a setEntity, and have it accept any of the 4. With this approach in mind, let's look back at the first example, and take note of the type hinting in the function signature that would have been produced for the setUser method:
public function setUser(Acme\DemoBundle\Entity\User $user)
See that it requires to be of type Acme\DemoBundle\Entity\User. How do we overcome this, and have it accept any of the 4 Entities? The solution is to have all Entities be derived from a parent class. Then make the function type hint at the base class:
public function setUser(Acme\DemoBundle\Entity\Base $entity)
The base class will contain all common elements, notably a 'name', and as array collection of Reports:
// src/Acme/DemoBundle/Entity/Base.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Base
{
// ...
/**
* #ORM\Column(name="name", type="text")
*/
protected $name
/**
* #ORM\OneToMany(targetEntity="Report", mappedBy="baseEntity")
*/
protected $reports;
public function __construct()
{
$this->reports = new ArrayCollection();
}
// ...
}
and then for each child, for example a User and a Video:
// src/Acme/DemoBundle/Entity/User.php
// ...
use AcmeDemoBundle\Entity\Base;
class User extends Base
{
/**
* #ORM\Column(name="firstname", type="text")
*/
protected $firstName;
// ...
}
and the Video
// src/Acme/DemoBundle/Entity/Video.php
// ...
use AcmeDemoBundle\Entity\Base;
class Video extends Base
{
/**
* #ORM\Column(name="title", type="text")
*/
protected $title;
// ...
and change our Report Entity:
// src/Acme/DemoBundle/Entity/Report.php
// ...
class Report
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Base", inversedBy="reports")
* #ORM\JoinColumn(name="base_id", referencedColumnName="id")
*/
protected $baseEntity;
// ...
}
Remember to run the doctrine command to generate the setBaseEntity method. When you do, notice that it will now accept any class derived of Base
Then, to put on a Report on a Video for example, we get the Video, create a Report, and add the Video to the Report:
$video = // get the video you want
$report = new Report();
$report->setBaseEntity($video);
To retrieve all Reports belonging to a Comment, we get the Comment, and get the Reports:
$video = // get the video you want
$reports = $video->getReports();
foreach($reports as $report){
$reportText = $report->getText(); // assuming the Report has a `text` field
}
Update:
The inheritance relationship between these Entities can be modelled in the database with Doctrine using Single Table Inheritance:
/**
* #ORM\Entity
* #ORM\Table(name="base_entities")
* #ORM\InheritanceType("SINGLE_TYPE")
* #ORM\Discriminator(name="entity_type", type="string")
* #ORM\DiscriminatorMap({"user" = "User", "comment" = "Comment", "video" = "Video", "channel" = "Channel"})
*/

Doctrine - self-referencing entity - disable fetching of children

I have a very simple entity(WpmMenu) that holds menu items connected to one another in a self-referencing relationship (adjecent list it's called)?
so in my entity I have:
protected $id
protected $parent_id
protected $level
protected $name
with all the getters/setters the relationships are:
/**
* #ORM\OneToMany(targetEntity="WpmMenu", mappedBy="parent")
*/
protected $children;
/**
* #ORM\ManyToOne(targetEntity="WpmMenu", inversedBy="children", fetch="LAZY")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", onUpdate="CASCADE", onDelete="CASCADE")
*/
protected $parent;
public function __construct() {
$this->children = new ArrayCollection();
}
And everything works fine. When I render the menu tree, I get the root element from the repository, get its children, and then loop through each child, get its children and do this recursively until I have rendered each item.
What happens (and for what I am seeking a solution)is this:
At the moment I have 5 level=1 items and each of these items have 3 level=2 items attached (and in the future I will be using level=3 items as well). To get all elements of my menu tree Doctrine executes:
1 query for the root element +
1 query to get the 5 children(level=1) of the root element +
5 queries to get the 3 children(level=2) of each of the level 1 items +
15 queries (5x3) to get the children(level=3) of each level 2 items
TOTAL: 22 queries
So, I need to find a solution for this and ideally I would like to have 1 query only.
So this is what I am trying to do:
In my entities repository(WpmMenuRepository) I use queryBuilder and get a flat array of all menu items ordered by level. Get the root element(WpmMenu) and add "manually" its children from the loaded array of elements. Then do this recursively on children. Doing this way I could have the same tree but with a single query.
So this is what I have:
WpmMenuRepository:
public function setupTree() {
$qb = $this->createQueryBuilder("res");
/** #var Array */
$res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult();
/** #var WpmMenu */
$treeRoot = array_pop($res);
$treeRoot->setupTreeFromFlatCollection($res);
return($treeRoot);
}
and in my WpmMenu entity I have:
function setupTreeFromFlatCollection(Array $flattenedDoctrineCollection){
//ADDING IMMEDIATE CHILDREN
for ($i=count($flattenedDoctrineCollection)-1 ; $i>=0; $i--) {
/** #var WpmMenu */
$docRec = $flattenedDoctrineCollection[$i];
if (($docRec->getLevel()-1) == $this->getLevel()) {
if ($docRec->getParentId() == $this->getId()) {
$docRec->setParent($this);
$this->addChild($docRec);
array_splice($flattenedDoctrineCollection, $i, 1);
}
}
}
//CALLING CHILDREN RECURSIVELY TO ADD REST
foreach ($this->children as &$child) {
if ($child->getLevel() > 0) {
if (count($flattenedDoctrineCollection) > 0) {
$flattenedDoctrineCollection = $child->setupTreeFromFlatCollection($flattenedDoctrineCollection);
} else {
break;
}
}
}
return($flattenedDoctrineCollection);
}
And this is what happens:
Everything works out fine, BUT I end up with each menu items present twice. ;) Instead of 22 queries now I have 23. So I actually worsened the case.
What really happens, I think, is that even if I add the children added "manually", the WpmMenu entity is NOT considered in-sync with the database and as soon as I do the foreach loop on its children the loading is triggered in ORM loading and adding the same children that were added already "manually".
Q: Is there a way to block/disable this behaviour and tell these entities they they ARE in sync with the db so no additional querying is needed?
With immense relief (and a lots of learning about Doctrine Hydration and UnitOfWork) I found the answer to this question. And as with lots of things once you find the answer you realize that you can achieve this with a few lines of code. I am still testing this for unknown side-effects but it seems to be working correctly.
I had quite a lot of difficulties to identify what the problem was - once I did it was much easier to search for an answer.
So the problem is this: Since this is a self-referencing entity where the entire tree is loaded as a flat array of elements and then they are "fed manually" to the $children array of each element by the setupTreeFromFlatCollection method - when the getChildren() method is called on any of the entities in the tree (including the root element), Doctrine (NOT knowing about this 'manual' approach) sees the element as "NOT INITIALIZED" and so executes an SQL to fetch all its related children from the database.
So I dissected the ObjectHydrator class (\Doctrine\ORM\Internal\Hydration\ObjectHydrator) and I followed (sort of) the dehydration process and I got to a $reflFieldValue->setInitialized(true); #line:369 which is a method on the \Doctrine\ORM\PersistentCollection class setting the $initialized property on the class true/false. So I tried and IT WORKS!!!
Doing a ->setInitialized(true) on each of the entities returned by the getResult() method of the queryBuilder (using the HYDRATE_OBJECT === ObjectHydrator) and then calling ->getChildren() on the entities now do NOT trigger any further SQLs!!!
Integrating it in the code of WpmMenuRepository, it becomes:
public function setupTree() {
$qb = $this->createQueryBuilder("res");
/** #var $res Array */
$res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult();
/** #var $prop ReflectionProperty */
$prop = $this->getClassMetadata()->reflFields["children"];
foreach($res as &$entity) {
$prop->getValue($entity)->setInitialized(true);//getValue will return a \Doctrine\ORM\PersistentCollection
}
/** #var $treeRoot WpmMenu */
$treeRoot = array_pop($res);
$treeRoot->setupTreeFromFlatCollection($res);
return($treeRoot);
}
And that's all!
Add the annotation to your association to enable eager loading. This should allow you to load the entire tree with only 1 query, and avoid having to reconstruct it from a flat array.
Example:
/**
* #ManyToMany(targetEntity="User", mappedBy="groups", fetch="EAGER")
*/
The annotation is this one but with the value changed
https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch
You can't solve this problem if using adjacent list. Been there, done that. The only way is to use nested-set and then you would be able to fetch everything you need in one single query.
I did that when I was using Doctrine1. In nested-set you have root, level, left and right columns which you can use to limit/expand fetched objects. It does require somewhat complex subqueries but it is doable.
D1 documentation for nested-set is pretty good, I suggest to check it and you will understand the idea better.
This is more like a completion and more cleaner solution, but is based on the accepted answer...
The only thing needed is a custom repository that is going to query the flat tree structure, and then, by iterating this array it will, first mark the children collection as initialized and then will hydratate it with the addChild setter present in the parent entity..
<?php
namespace Domain\Repositories;
use Doctrine\ORM\EntityRepository;
class PageRepository extends EntityRepository
{
public function getPageHierachyBySiteId($siteId)
{
$roots = [];
$flatStructure = $this->_em->createQuery('SELECT p FROM Domain\Page p WHERE p.site = :id ORDER BY p.order')->setParameter('id', $siteId)->getResult();
$prop = $this->getClassMetadata()->reflFields['children'];
foreach($flatStructure as &$entity) {
$prop->getValue($entity)->setInitialized(true); //getValue will return a \Doctrine\ORM\PersistentCollection
if ($entity->getParent() != null) {
$entity->getParent()->addChild($entity);
} else {
$roots[] = $entity;
}
}
return $roots;
}
}
edit: the getParent() method will not trigger additional queries as long as the relationship is made to the primary key, in my case, the $parent attribute is a direct relationship to the PK, so the UnitOfWork will return the cached entity and not query the database.. If your property doesn't relates by the PK, it WILL generate additional queries.

Categories