Disclaimer: n00b to Doctrine, not ORMs in general, or the data mapper pattern, just Doctrine. Not sure if I'm missing something (references to the documentation are welcome if they're specific! I've read thoroughly though) or there is another preferred approach. A big part of this question is how to do it the right way, not necessarily how to do it in general.
The deets: I've got a one to many association between two entities: Calendar and Event. One calendar can have many events. Relevant code:
Calendar Entity
<?php namespace MyPackage\Src {
use \Doctrine\Common\Collections\ArrayColleciton;
class Calendar {
/**
* #OneToMany(targetEntity="MyPackage\Src\Event", mappedBy="calendarInstance", cascade={"all"})
*/
protected $associatedEvents;
public function __construct(){
$this->associatedEvents = new ArrayCollection();
}
public function addEvent( Event $eventObj ){
$eventObj->setCalendarInstance($this);
$this->associatedEvents->add($eventObj);
}
public function getEvents(){
return $this->associatedEvents;
}
// Assume entityManager() returns... an entity manager instance
public function save(){
$this->entityManager()->persist($this);
$this->entityManager()->flush();
return $this;
}
}
}
Event Entity
<?php namespace MyPackage\Src {
class Event {
/**
* #ManyToOne(targetEntity="MyPackage\Src\Calendar", inversedBy="associatedEvents")
* #JoinColumn(name="calendarID", referencedColumnName="id", nullable=false)
*/
protected $calendarInstance;
public function setCalendarInstance( Calendar $calObj ){
$this->calendarInstance = $calObj;
}
// Assume entityManager() returns... an entity manager instance
public function save(){
$this->entityManager()->persist($this);
$this->entityManager()->flush();
return $this;
}
}
}
Semantics of the ORM state that "Doctrine will only check the owning side of an association for changes" (<- proof of reading docs). So doing the following works fine when creating a new event (assuming cascade persist is enforced by the Calendar, which I have set with "all"), and then gett'ing the ArrayCollection from the calendar instance.
<?php
// Assume this returns a successfully persisted entity...
$cal = Calendar::getByID(1);
// Create a new event
$event = new Event();
$event->setTitle('Wowza');
// Associate the event to the calendar
$cal->addEvent($event);
$cal->save();
// Yields expected results (ie. ArrayCollection is kept in-sync)
echo count($cal->getEvents()); // "1"
Meow then... Lets say for some funny reason I want to create an association from the event side, like so:
<?php
// Assume this returns a successfully persisted entity...
$cal = Calendar::getByID(1);
// Create a new event
$event = new Event();
$event->setTitle('Dewalt Power Tools');
// Associate VIA EVENT
$event->setCalendarInstance($cal);
$event->save();
// ArrayCollection in the Calendar instance is out of sync
echo count($cal->getEvents()); // "0"
Persisting the event in this manner does work (it gets saved to the DB) - but now the entity manager is out of sync when trying to access associated events from the Calendar. Further, its understandable why: in the setCalendarInstance() method of the Event class, all thats happening is
$this->calendarInstance = $calendar;
I've tried the following (which actually works, it just feels really naughty)
$this->calendarInstance = $calendar;
if( ! $this->calendarInstance->getEvents()->contains($this) ){
$this->calendarInstance->getEvents()->add($this);
}
BUT, this feels very strongly like I'm breaking proper encapsulation, which is important to me. Am I being silly, and its OK to ->add() to the returned ArrayCollection of the Calendar instance, outside of the Calendar class? (Thats what I mean this feels wrong; the ArrayCollection is a protected property of Calendar and I feel like it shouldn't be modified externally).
---- OR ----
Should I enforce creating the association only from the owning side (the Calendar), and not allow doing $event->setCalendarInstance() and then saving the event. Which again brings up another point on encapsulation, that setCalendarInstance must be a public method in order for the Calendar's addEvents() method to work (so how would I prevent some poor soul inheriting my codebase from doing it improperly)?
Thanks, interested to hear approaches to this.
** Edit **
Further funny business: Going the route of creating a new event, passing the calendar instance, and then persisting the new event - here is where discrepancies seem to exist:
// Get an existing Calendar that has one Event
$calObj = Calendar::getByID(1);
echo count($calObj->getEvents()); // "1"
// Add from event side
$event = new Event();
$event->setTitle('Pinkys Brain');
$event->setCalendarInstance($calObj);
$event->save();
// I would think this would be "2" now...
echo count($calObj->getEvents()); // Still "1"
BUT... this works, and feels shloppy.
// Get an existing Calendar that has one Event
$calObj = Calendar::getByID(1);
echo count($calObj->getEvents()); // "1"
// Add from event side
$event = new Event();
$event->setTitle('Pinkys Brain');
$event->setCalendarInstance($calObj);
$event->save();
// Clear the entity manager on the existing $calObj
$calObj->entityManager()->clear();
// Now get a NEW instance of the same calendar
$calObjAgain = Calendar::getByID(1);
echo count($calObjAgain->getEvents()); // "2"
Related
I've been reading this post : Doctrine - how to check if a collection contains an entity
But I actually don't like the solution, as, doctrine already provide the contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely.
So here a Cart Entity own a CartProduct collection as is :
/**
* ...
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
*/
abstract class Cart implements InheritanceInterface{
...
/**
* #ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
*/
private Collection $cartProducts;
...
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
...
}
(CartProduct have to be an Entity look at this simplify EA model. That's a standard way to proceed for related entity holding extra fields)
Now I want to add a new ProductCart Entity to my Cart class.
So I'm adding this method (generated by Symfony make:entity) :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->contains($cartProduct)) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
And then I test this code :
public function testAddCartProduct()
{
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
}
But when I run this test, it fail :
Failed asserting that actual size 2 matches expected size 1.
So I check, and the Cart.cartProducts Collection have two product which are exactly the same objects.
As it's an ArrayCollection, I suppose that it just use this method :
namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
...
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So well, of course in this case it is just return false, And the objects are considered to be different.
So now, I wish I could use PersistentCollection instead of ArrayCollection when implementing the Collection object , because the PersistentCollection.contains() method looks better.
abstract class Cart implements InheritanceInterface{
...
public function __construct()
{
-- $this->cartProducts = new ArrayCollection();
++ $this->cartProducts = new PersistentCollection(...);
}
}
But this require an EntityManager as a parameter, so, seams a little bit overkill to give an EntityManager to an Entity object...
So I finally, I don't know what is the better way to check for a dupplicate entity inside a collection.
Of course, I could implement myself a thing like :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->filter(
function (CartProduct $cp)use($cartProduct){
return $cp->getId() === $cartProduct->getId();
})->count()) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
But it'll require to load every Entity and I really don't like the idea.
Personally I agree with your comment, I don't think the entity itself should have the responsibility to ensure there is no duplicate.
The entity cannot make a request like a repository could, and I don't see how you can be sure there is no duplicate in the database without querying it.
Calling contains will not trigger a fetch in your case, this means the collection will stay as is, which is not what you want anyway because you could have a previously persisted duplicate that will not be part of the collection because you marked it as EXTRA_LAZY.
You also don't want to fetch all the entities of the collection (and transform the results into objects) just to check if you have a collision.
So IMHO you should create a method in the repository of the entity to check for duplicates, a simple SELECT COUNT(id).
Then there is your real problem.
The way you make your test will never find a collision. When you do:
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
You are creating two instances of CartProduct, that's why the call to contains doesn't find anything.
Because contains checks for the object reference, not the content, like you can see in its implementation:
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So in your test case what you're really testing is:
in_array(new CartProduct(), [new CartProduct()], true);
which will always return false.
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 :)
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"})
*/
For example, I have the following two classes (getters / setters omitted for brevity), which are linked both ways in mapping:
class Form
{
private $elements = array();
public function addElement($element)
{
$this->elements[] = $element
$element->setForm($this);
}
}
class Element
{
private $form;
private $name;
}
<one-to-many field="elements" target-entity="Element" mapped-by="form"/>
<many-to-one field="form" target-entity="Form" inversed-by="elements">
<join-column name="formId" referenced-column-name="id" on-delete="CASCADE" on-update="CASCADE"/>
</many-to-one>
If I do the following; adding two elements to the form, but only persisting one element, what I want to happen is for the unpersisted element to be ignored completely by the Entity Manager, but the other element and the form to be inserted into the database:
$form = new Form;
$em->persist($form);
$element = new Element;
$element->setName('firstName');
$form->addElement($element);
$em->persist($element);
$element2 = new Element;
$element2->setName('lastName');
$form->addElement($element2);
$em->flush();
At the moment get the following error:
exception 'Doctrine\ORM\ORMInvalidArgumentException' with message 'A new entity was found through the relationship 'Form#elements' that was not configured to cascade persist operations for entity Element#0000000019217f52000000009c20d747. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Element#__toString()' to get a clue
As far as I can tell there are no cascade options to ignore new entities (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations) and using a preUpdate lifecycle callback to remove the offending entities from the $elements array also doesn't work because the exception is thrown before the callbacks are run.
Is there are any way around this?
Try yo flush only the entities you want:
$form = new Form;
$em->persist($form);
$em->flush($form);
$element = new Element;
$element->setName('firstName');
$form->addElement($element);
$em->persist($element);
$em->flush($element);
//ignored element
$element2 = new Element;
$element2->setName('lastName');
$form->addElement($element2);
As this is the only relevant page for this problem I could find, I'll add my own solution that I came up with. (In my project it's also basically a form with answers, where I manually save answers that were updated, but incomplete answers should not be saved to the database when persisting the form.)
Basically you create two properties for the relation: One that will only be used to load the persisted related elements ($subEntities) and one that will be made available to the public ($chachedSubEntities).
This $cachedSubEntities member will be initialized with the persisted subEntities the first time you call getSubEntities.
class MyEntity extends AbstractEntity
{
/**
* #ORM\OneToMany(targetEntity="SubEntity", mappedBy="parent", indexBy="id")
*/
protected $subEntities;
protected $cachedSubEntities;
public function __construct()
{
$this->subEntities = new ArrayCollection();
}
public function getSubEntities()
{
if (is_null($this->cachedSubEntities)) {
$this->cachedSubEntities = new ArrayCollection($this->subEntities->toArray());
}
return $this->cachedSubEntities;
}
}
i have two table:
News:
id
title
body
and
NewsCopy:
id
title
body
if i add new News i would like also add this same all data for table NewsCopy?
class News extends BaseNews
{
public function save(Doctrine_Connection $conn = null)
{
return parent::save($conn);
}
}
how can i make this simply?
Well, one possible way is to hook up into the Doctrine saving mechanism:
class News{
//..other declarations//
//executed after Save
public function postSave(){
$newsCopy = new NewsCopy();
//set the parameters manually
$newsCopy->id = $this->id;
$newsCopy->title = $this->title;
$newsCopy->body = $this->body;
//OR, even better, create a "cast constructor" the same idea
//$newsCopy = new NewsCopy($this);
$newsCopy->save();
}
}
See "Event Listeners" chapter for more detailed explanation
You can utilize the toArray() method of the existing and populated "News" record object and populate a separate CopyNews object. With the now newly configured object you can do the save with.
I assume doctrine 1.2 - and I do not have a testing environment - so no code :).
You could probably also play with the clone() method and set a new table name ...
All untested - sorry.
The best you can do is to use triggers