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
Related
I have an entity called Cycle, with a OneToMany association to CycleActeur (see code below).
I'd like to be able to fetch all Cycle objets in database with no CycleActeur objects associated, using a simple doctrine findBy* method from my controller.
That is to say something like this :
$manager = $this->getContainer()->get('doctrine.orm.entity_manager');
$cycleManager = $manager->getRepository('ESI67Zen2Bundle:Cycle');
$cyclesWithNoCycleActeur = $cycleManager->findBy('acteurs', null);
Is there a way to do this without having to write a specific method in the CycleRepository ?
Extract from the Cycle class code
class Cycle {
/**
* #ORM\OneToMany(
* targetEntity="CycleActeur",
* mappedBy="cycle",
* orphanRemoval=true)
*/
private $acteurs;
}
Extract from the Cycle class code
class CycleActeur {
/**
* #var Cycle Le cycle concerné
*
* #ORM\ManyToOne(targetEntity="Cycle", inversedBy="acteurs")
* #ORM\JoinColumn(name="cycle_id", referencedColumnName="id")
*
*/
private $cycle;
}
Your Cycle entity is the inverse side of relationship and it's table in database has no 'acteurs' column, so you cannot use findBy(['acteurs'=>null]) or findByActeurs(null). But you can do something anyway:
$manager = $this->getContainer()->get('doctrine.orm.entity_manager');
$cycleManager = $manager->getRepository('ESI67Zen2Bundle:Cycle');
$allCycles = $cycleManager->findAll();
$cyclesWithNoCycleActeur = [];
foreach($allCycles as $cycle)
{
if($cycle->getActeurs()->isEmpty())
{
$cyclesWithNoCycleActeur[] = $cycle;
}
}
In this case (to my mind) the best way is to use DQL's condition IS EMPTY:
$manager
->createQueryBuilder()
->from(Cycle::class, 'cycle')
->select('cycle')
->andWhere('cycle.acteurs IS EMPTY')
->getQuery()
->getResult()
;
You can use this code in the EntityRepository or anywhere you have access to the EntityManager.
Source: Doctrine documentation.
There's a DQL function SIZE(), which according to Doctrine documentation:
SIZE(collection) - Return the number of elements in the specified collection
So you can use it as a condition like:
SIZE(acteurs) = 0
I'm not sure if it will work with a findBy method, but I would recommend to create a custom method in ESI67Zen2Bundle:Cycle's repository, to make it explicit what the code is doing. Its will work both for DQL Query and Query Builder.
$cyclesWithNoCycleActeur = $cycleManager->findBy(array('SIZE(acteurs)' => 0));
My 2 cents
I have defined the follow entity in doctrine2 (with symfony).
/**
*
* #ORM\Table(name="order")
* #ORM\Entity
*/
class Order
/**
* #var integer
*
* #ORM\Column(name="personid", type="integer", nullable=false)
*/
private $personid;
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
public function getPersonId()
{
return $this->personid;
}
public function getPerson()
{
return $this->person;
}
}
I realize that if I call $order->getPersonId() it return always an empty value and I have to call the getPerson()->getId() method to get the correct personid.
Could anyone explain me why the variable $personid is not filled?
Should I to delete the column id used for the join if I defined one?
Thanks
Gisella
You should remove private $personid;, it's better to work with objects only in an ORM.
It's not a problem if you get the ID with $order->getPerson()->getId(), because Doctrine won't load the complete entity. The People entity will only be loaded if you call an other field than the join key.
You can still have a getter shortcut like this :
public function getPersonId()
{
return $this->getPerson()->getId();
}
Edit :
You can also still work with "ID" if you use Doctrine references, like this :
$order->setPerson($em->getReference('YourBundle:People', $personId));
With this way, Doctrine won't perform a SELECT query to load data of the person.
You don't need to have the $personid field when you already have the $person field.
$people contains the People object (with all People's attributes including the id).
Moreover, when doctrine translate your object into sql tables, he knows that he have to join with th id so it will create a field (in database) named personid. (It's the name that you defined in your ORM)
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
Sorry for bad english :p
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.
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"})
*/
I'm using an embed Symfony form to add and remove Tag entities right from the article editor. Article is the owning side on the association:
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
*/
private $tags;
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
}
The condition doesn't help here, as it is always true, and if it wasn't, no new tags would be persisted to the database at all. Here is the Tag entity:
class Tag
{
/**
* #Column(unique=true)
*/
private $name
/**
* #ManyToMany(targetEntity="Articles", mappedBy="tags")
*/
private $articles;
public function addArticle(Article $articles)
{
$this->articles[] = $articles;
}
}
I've set $name to unique, because I want to use the same tag every time I enter the same name in the form. But it doesn't work this way, and I get the exception:
Integrity constraint violation: 1062 Duplicate entry
What do I need to change to use article_tag, the default join table when submitting a tag name, that's already in the Tag table?
I have been battling with a similar issue for months and finally found a solution that seems to be working very well in my application. It's a complex application with quite a few many-to-many associations and I need to handle them with maximum efficiency.
The solution is explained in part here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush
You were already halfway there with your code:
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
Basically what I have added to this is to set indexedBy="name" and fetch="EXTRA_LAZY" on the owning side of the relationship, which in your case is Article entity (you may need to scroll the code block horizontally to see the addition):
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
*/
private $tags;
You can read up about the fetch="EXTRA_LAZY" option here.
You can read up about indexBy="name" option here.
Next, I modified my versions of your addTag() method as follows:
public function addTag(Tag $tags)
{
// Check for an existing entity in the DB based on the given
// entity's PRIMARY KEY property value
if ($this->tags->contains($tags)) {
return $this; // or just return;
}
// This prevents adding duplicates of new tags that aren't in the
// DB already.
$tagKey = $tag->getName() ?? $tag->getHash();
$this->tags[$tagKey] = $tags;
}
NOTE: The ?? null coalesce operator requires PHP7+.
By setting the fetch strategy for tags to EXTRA_LAZY the following statement causes Doctrine to perform a SQL query to check if a Tag with the same name exists in the DB (see the related EXTRA_LAZY link above for more):
$this->tags->contains($tags)
NOTE: This can only return true if the PRIMARY KEY field of the entity passed to it is set. Doctrine can only query for existing entities in the database/entity map based on the PRIMARY KEY of that entity, when using methods like ArrayCollection::contains(). If the name property of the Tag entity is only a UNIQUE KEY, that's probably why it's always returning false. You will need a PRIMARY KEY to use methods like contains() effectively.
The rest of the code in the addTag() method after the if block creates a key for the ArrayCollection of Tags either by the value in the PRIMARY KEY property (preferred if not null) or by the Tag entity's hash (search Google for "PHP + spl_object_hash", used by Doctrine to index entities). So, you are creating an indexed association, so that if you add the same entity twice before a flush, it will just be re-added at the same key, but not duplicated.
Two main solutions
First
Use a data transformer
class TagsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* used to give a "form value"
*/
public function transform($tag)
{
if (null === $tag) {
//do proper actions
}
return $issue->getName();
}
/**
* used to give "a db value"
*/
public function reverseTransform($name)
{
if (!$name) {
//do proper actions
}
$issue = $this->om
->getRepository('YourBundleName:Tag')
->findOneBy(array('name' => $name))
;
if (null === $name) {
//create a new tag
}
return $tag;
}
}
Second
Use lifecycle callback. In particular you can use prePersist trigger onto your article entity? In that way you can check for pre-existing tags and let your entity manager manage them for you (so he don't need to try to persist causing errors).
You can learn more about prePersist here
HINT FOR SECOND SOLUTION
Make a custom repository method for search and fetch old tags (if any)