Unable to persist complex entity graph in Symfony2/Doctrine2 - php

I'm a bit limited in the details I can provide due to a NDA, so please bear with me.
I have a complex entity graph. It consists of:
A 1-to-1 relationship between a Parent and Child.
The Child contains an ArrayCollection of FooChild entities. Cascade all.
FooChild represents a many-to-many join table between Foo and Child, but also contains some metadata that Child needs to track. Cascade persist on each side (Foo and Child)
Parents aren't required to have a Child.
To be 100% clear regarding FooChild, the relationship is many-to-many, but because of the metadata, it contains many-to-one relationship definitions:
/**
* #ORM\Entity
* #ORM\Table(name="foo_children", indexes={
* #ORM\Index(name="fooid_idx", columns={"foo_id"}),
* #ORM\Index(name="childid_idx", columns={"child_id"}),
* })
*/
class FooChild
{
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Foo", cascade={"persist"})
* #ORM\JoinColumn(name="foo_id", referencedColumnName="id", nullable=false)
*/
protected $foo;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Child", inversedBy="fooChildren", cascade={"persist"})
* #ORM\JoinColumn(name="child_id", referencedColumnName="id", nullable=false)
*/
protected $child;
/**
* #ORM\Column(type="smallint")
*/
private $count;
// methods
}
Okay, so with that structure, on the Parent edit page, I created the option for someone to add a Child to it and populate it with FooChilds with the Symfony prototype mechanism seen here. When I attempt to submit the rather large form, I get the following exception:
Entity of type MyBundle\Entity\FooChild has identity through a foreign entity MyBundle\Entity\Child, however this entity has no identity itself. You have to call EntityManager#persist() on the related entity and make sure that an identifier was generated before trying to persist 'MyBundle\Entity\FooChild'. In case of Post Insert ID Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you have to call EntityManager#flush() between both persist operations.
The thing is, I've attempted to persist the various parts of this graph in different orders, and the exception still remains. My current attempt is:
$form = $this->createForm(new ParentType(), $parent);
if ($request->getMethod() == 'POST') {
$form->handleRequest($request);
if ($form->has('child')) {
$data = $form->getData();
$child = $data->getChild();
$fooChildren = $child->getFooChildren();
foreach ($fooChildren as $fc) {
$em->persist($fc);
$em->flush();
}
$em->persist($child);
$em->flush();
}
$em->persist($parent);
$em->flush();
}
The exception is thrown at the first attempt to persist, in the foreach. Like I said before, I've swapped the order of what gets persisted when several times, but it hasn't made a difference. I'm not sure what else to try.

I had an initial solution of removing the Child's form type and having the Parent's form type handle unmapped (very important) FooChild entries. Then, in the controller, I had:
$em->persist($parent);
$em->flush();
if ($form->has('fooChildren')) {
$child = new Child();
$child->setParent($parent);
$em->persist($child);
$em->flush();
// run through the FooChild entites and add them to the child
}
It worked, but I ran into some other non-related issues, so I'm currently reorganizing my schema.

Related

How to be warned a many-to-many relation gonna be update?

I want to do a specific treatment when a specific field is updated.
The obvious way is to do it with event preUpdate, and see what fields are updated. It works fine ... except for a many-to-many field. It triggers the event, but the ChangeSet is empty.
/**
* #ORM\PreUpdate
*/
public function updateDate(PreUpdateEventArgs $event){
$changeSet = $event->getEntityChangeSet();
$res = "";
foreach($changeSet as $key => $change){
$line = $key." : ".$event->getOldValue($key)." || ".$event->getNewValue($key);
$res .= $line;
}
}
In $res all my fields are modified except for the many-to-many field.
Also, I'm trying to do it in a listener, but I can't find how to extract the fields which are updated from the entityManager.
Thank you.
More informations :
Relation from the update entity :
/**
* #var Status
*
* #ORM\ManyToMany(targetEntity="User", inversedBy="projectsSupervisor", cascade={"persist"})
* #ORM\JoinTable(name="projects_supervisors")
*/
protected $supervisors;
From the other side :
/**
* #var Project
*
* #ORM\ManyToMany(targetEntity="Task", mappedBy="users")
*/
protected *tasks
Symfony version : 3.1.10
It is not possible to track changes made to an many-to-many-association. See here :
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine’s point of view)
Additionally, ::getEntityChangeSet() is only useful for regular fields, not associations. For One-To-Many-Associations, you can use $unitOfWork->getScheduledCollectionUpdates() :
foreach ($uow->getScheduledCollectionUpdates() as $collectionUpdate) {
/** #var $collectionUpdate \Doctrine\ORM\PersistentCollection */
if ($collectionUpdate->getOwner() === $entity) {
// This entity has an association mapping which contains updates.
$collectionMapping = $collectionUpdate->getMapping();
print_r($collectionMapping); // Investigate this further
}
}
A practical example is viewable in my github repository "DoctrineWatcher" which does exactly the same (line 196+).

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.

Doctrine OneToOne identity through foreign entity exception on flush

I have User and UserProfile OneToOne–related Doctrine ORM entities. They should always exist as a pair, there should be no User without UserProfile.
User should get its id from autoincrement, while UserProfile should have User's id. So they both should have the same id and there is no other column to set up the relationship (Doctrine docs: Identity through foreign Entities).
UserProfile's id is both a primary key (PK) and foreign key (FK) at the same time.
I managed to set it up, but it requires that User is saved first and only later UserProfile is created and saved in a separate step.
What I want is that UserProfile is always created with User, in the constructor, but if I do that, I get this exception:
Doctrine\ORM\ORMInvalidArgumentException: The given entity of type 'AppBundle\Entity\UserProfile' (AppBundle\Entity\UserProfile#0000000052e1b1eb00000000409c6f2c) has no identity/no id values set. It cannot be added to the identity map.
Please see code below – it works, but not the way I want. The php comments show what I want to achieve.
Test.php:
/**
* It works, both saving and loading.
* BUT, it requires that I create and save UserProfile
* in a separate step than saving User step.
*/
// create and save User
$user = new User();
$objectManager->persist($user);
$objectManager->flush();
// create and save UserProfile (this should be unnecessary)
$user->createProfile()
$objectManager->flush();
User.php:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\UserRepository")
* #ORM\Table(name="users")
*/
class User
{
/**
* #var int
*
* #ORM\Column(name="uid", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* It's NULL at first, I create it later (after saving User).
*
* #var UserProfile|null
*
* #ORM\OneToOne(targetEntity="UserProfile", mappedBy="user", cascade="persist")
*/
private $profile = null;
public function __construct()
{
// I want to create UserProfile inside User's constructor,
// so that it is always present (never NULL):
//$this->profile = new UserProfile($this);
// but this would give me error:
//
// Doctrine\ORM\ORMInvalidArgumentException:
// The given entity of type 'AppBundle\Entity\UserProfile'
// (AppBundle\Entity\UserProfile#0000000058af220a0000000079dc875a)
// has no identity/no id values set. It cannot be added to the identity map.
}
public function createProfile()
{
$this->profile = new UserProfile($this);
}
}
UserProfile.php:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="profiles")
*/
class UserProfile
{
/**
* – UserProfile's "uid" column points to User's "uid" column
* – it is PK (primary key)
* - it is FK (foreign key) as well
* – "owning side"
*
* #var User
*
* #ORM\Id
* #ORM\OneToOne(targetEntity="User", inversedBy="profile")
* #ORM\JoinColumn(name="uid", referencedColumnName="uid", nullable=false)
*/
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
Test app: https://github.com/MacDada/DoctrineOneToOneTest
Please keep in mind that the actual object needs to be saved by the EntityManager.
Just giving the class as reference to the other class does not make the entityManager aware of the fact both classes exists.
You should persist the actual userProfile to the EntityManager to be able to save the relation.
UPDATE because of negative comment:
Please read the Doctrine docs... You should persist!
The following example is an extension to the User-Comment example of this chapter. Suppose in our application a user is created whenever he writes his first comment. In this case we would use the following code:
<?php
$user = new User();
$myFirstComment = new Comment();
$user->addComment($myFirstComment);
$em->persist($user);
$em->persist($myFirstComment);
$em->flush();
Even if you persist a new User that contains our new Comment this code would fail if you removed the call to EntityManager#persist($myFirstComment). Doctrine 2 does not cascade the persist operation to all nested entities that are new as well.
Update2:
I understand what it is you wish to accomplish, but by design you should not move this logic within your entities. Entities should represent as less logic as possible, since they represent your modal.
Have that said, I believe you could accomplish what you are trying to do like this:
$user = new User();
$profile = $user->getProfile();
$objectManager->persist($user);
$objectManager->persist($profile);
$objectManager->flush();
You should however consider creating a userService containing the entitymanager and make that responsible for creating, linking and persisting the user + userProfile entity.

Symfony Doctrine One to Many does not insert foreign key

I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

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"})
*/

Categories