I have a recursive entity containing parent/children item
namespace Vendor\StructureBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Events;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Vendor\StructureBundle\Entity\Structure
* #Gedmo\Tree(type="nested")
*
* #ORM\Table(name="lowbi_structure")
* #ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
* #ORM\HasLifecycleCallbacks()
* #CustomAssert\ganttDate
*/
class Structure {
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
/**
* #ORM\Column(name="title", type="string", length=64)
*/
private $title;
/**
* #Gedmo\TreeParent
* #ORM\ManyToOne(targetEntity="Structure", inversedBy="children",cascade={"persist"})
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $parent;
/*
* #ORM\OneToMany(targetEntity="Structure", mappedBy="parent",cascade={"persist","remove"})
* #ORM\OrderBy({"lft" = "ASC"})
*/
private $children;
...
}
and i want to update the "parent" when i persit this entity.
/**
* Set prePersist
*
* #ORM\PrePersist()
* #ORM\PreUpdate()
*
*/
public function prePersist()
{
$this->getParent()->setTitle('Foo');
}
The probleme is that my current entity is persisted but the parent entity is not. The title is not saved. How can i save the parent's properties?
PS : I simplified the code. In real world, i need to update parent start dates/end dates to fit the children (project management tree)
What you're trying to do here is not possible with a Lifecycle callback (i.e. #PrePersist) if you save only the child afterwards.
Doctrine only tracks and saves changes to the owning side of a relation.
When a bidirectional assocation is updated, Doctrine only checks on
one of both sides for these changes. This is called the owning side of
the association.
Therefore you can't persist an update of the parent from the child by persisting the child only.
You can find more information about the concepts of the inverse and owning side in this answer and in the documentation chapter Working with Associations.
To solve this issue ... you can either persist the parent instead of the child ... or (instead of using the lifecycle callback #PrePersist) create an event listener that will automatically persist the parent. This is a better practice in general because it keeps application logic out of your model.
The documentation chapter How to Register Event Listeners and Subscribers provides all the necessary information to get you started.
I don't know why, but if i return $this in the prePersist function, IT WORKS.
**
* Set prePersist
*
* #ORM\PrePersist()
* #ORM\PreUpdate()
*
*/
public function prePersist()
{
//$this->getParent()->setTitle('Foo');
return $this->getParent()->setTitle('Foo'); //solved this problem!!!!
}
Related
I have read the official docs and many questions here. I also tried blindly few things without results. Please do not focus on "why I need it" but whether this is possible:
I have these 2 entities:
/**
* #ORM\Entity
*/
class Payout
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\BalanceLog")
* #ORM\JoinTable(name="payouts_balance_logs",
* joinColumns={#ORM\JoinColumn(name="payout_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="balance_log_id", referencedColumnName="id", unique=true)}
* )
*/
private Collection $balance_logs;
}
/**
* #ORM\Entity
*/
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Payout")
*/
private ?Payout $payout;
}
One BalanceLog CAN (but does not have to) point to a Payout. And every Payout has at least one BalanceLog. This all is achieved with payouts_balance_logs table, where balance_log_id is unique (meaning a single balance log can point to only one payout)
I can easily make a bi-directional ManyToMany relationship, but then I have BalanceLog::$payouts collection, not a single object.
I can also make it bi-directional OneToMany/ManyToOne without using JoinTable, but then EVERY BalanceLog will need its nullable payout_id column.
I think that what I want has to be common and achievable. Please help, thanks!
Symfony 3.1 - Doctrine: I am trying to add relationships between entities and thereby want to add foreign key to link tables in my example. I tried several settings, utilizing various combination of arguments, but none of them were able to ALTER the tables with Foreign key.
My entities are namely "Trips" and "Airlines" with the content attached below. I would like to use airline_id as a foreign key inside trips table. Here are the extract from my entity scripts:
Entity#1: Trips
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="trips")
*/
class Trips
{
/**
* #ORM\Column(name="trip_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $trip_id;
/**
* #ORM\Column(name="airline_id", type="integer")
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Airlines")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="airline_id", referencedColumnName="airline_id")
* })
*/
private $airline_id;
}
Entity#2: Airlines
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="airlines")
*/
class Airlines
{
/**
* #ORM\Column(name="airline_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $airline_id;
/**
* #ORM\Column(type="string", length=80)
*/
private $name;
}
I tried updating the schema from console , but all it says is
Nothing to update - your database is already in sync with the current entity metadata.
Also tried deleting the table and re-creating them with console; the result did not change much and the foreign key is still missing. :/
Any thoughts, or perhaps you know the solution?
I have a few suggestions, as I've gone through the same process of design.
First off, should I presume that "a trip has one airline" and also "an airline has many trips"? This seems logical, and since you show #ORM\ManyToOne annotation for the Trips class, that would equate to my same presumptions.
If this is so, then these are the suggestions:
First, don't call the classes "Trips" and "Airlines", but rather in the singular, that is "Trip" and "Airline". This is object oriented thinking. So when you create a "Trip" Doctrine object, it represents a trip for example. This also makes you code more readable and also supportable from a future standpoint; as because when people read your code, they will understand that a single instance of a Trip (from the Trip class), represents a trip (that a person takes). This is important when designing code.
Given the above, make these code changes.
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="trip")
*/
class Trip
{
/**
* #ORM\Id
* #ORM\Column(name="trip_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $trip_id;
/**
* #ORM\ManyToOne(targetEntity="Airline", inversedBy="trips")
* #ORM\JoinColumn(name="trip_airline_id", referencedColumnName="airline_id")
*/
private $airline;
...
/**
* Set airline - Adds this trip to the passed in airline and sets the airline
* property to resultant airline.
*
* #param \AppBundle\Entity\Airline
*/
public function setAirline($airline){
$airline->addTrip($this);
$this->airline = $airline;
}
}
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="airlines")
*/
class Airlines
{
/**
* #ORM\Column(name="airline_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $airline_id;
/**
* #ORM\Column(type="string", length=80)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="Trip", mappedBy="airline")
*/
protected $trips = null;
...
public function __construct(){
// This is an array of trips.
$this->trips = new ArrayCollection();
}
...
/**
* Add trip - Adds a course to the array.
*/
public function addTrip($trip){
$this->trips[] = $trip;
}
}
Now you can get trips from the airline, and also you can do the other way around, get the airline from the trip.
When #ORM\Column is specified along with #ORM\JoinColumn on same column, then JoinColumn's association gets ignored and Foreign Key isn't created on table. so dont use both #ORM\Column and #ORM\JoinColumn in same column. There is no need to specify data type with #ORM\Column for associations, doctrine handles it and assign data type based on database engine. For MySql, it is int.
I currently have to Entities in my application:
Page
Block
A Page can have many Blocks, which are shared across many Pages, so it is quite obvious that the relation is a ManyToMany. However, I need to be able to add the same Block twice (or more) to the same Page. Doctrine creates the "page_block" join table automatically, but with page_id and block_id both as Primary Keys, therefore adding a duplicate throws an error.
Is it possible, without adding an additional Entity, to tell doctrine to allow duplicates on the Page--Block relation ?
Well, I'm not sure about that behavior in doctrine, but if that is the case, then you can do something that I almost always do. Represent the ManyToMany relation as two OneToMany-ManyToOne. You must create your own PageBlock entity and configure it's foreign keys.
class Page{
/**
* #var array
*
* #ORM\OneToMany(targetEntity="PageBlock", mappedBy="page", cascade={"all"})
*/
private $pageBlocks;
}
class Block{
/**
* #var array
*
* #ORM\OneToMany(targetEntity="PageBlock", mappedBy="block", cascade={"all"})
*/
private $pageBlocks;
}
class PageBlock{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \stdClass
*
* #ORM\ManyToOne(targetEntity="Page", inversedBy="pageBlocks")
* #ORM\JoinColumn(name="id_page", referencedColumnName="id")
*/
private $page;
/**
* #var \stdClass
*
* #ORM\ManyToOne(targetEntity="Block", inversedBy="pageBlocks")
* #ORM\JoinColumn(name="id_block", referencedColumnName="id")
*/
private $block;
}
As you can see the primary key remains as ID, so problem resolved. I say almost always do because this is how I do it if I need an extra attribute in the relation(almost always it happens). I suspect that could be a way of do it with the ManyToMany annotation, but there is no difference with this approach.
Hope this help you.
I have an Entity Employee
class Employee
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="WorkHour", mappedBy="employee", cascade={"persist", "remove"})
*/
private $workHours;
}
and WorkHour
class WorkHour
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Profile
*
* #ORM\ManyToOne(targetEntity="Employee", inversedBy="workHours")
* #ORM\JoinColumn(name="employee_id", referencedColumnName="id")
*/
protected $profile;
/**
* #var integer
*
* #ORM\Column(name="weekday", type="smallint")
*/
private $weekday;
/**
* #var \DateTime
*
* #ORM\Column(name="hour_from", type="time")
*/
private $hourFrom;
/**
* #var \DateTime
*
* #ORM\Column(name="hour_to", type="time")
*/
private $hourTo;
}
Now I'm confused when I'm going to add addWorkHour(), removeWorkHour() methods.
Usually one-to-many relation you can add as many relations as you want, but in my case, one employee can have only up-to-7 workHours, and for a specified weekday (from 0 to 6) can have only one (or no) record.
So I think what I need is something methods like,
public function setWorkHourByWeekday($hour_from, $hour_to, $weekday);
public function getWorkHourByWeekday($weekday);
And after set workhours for an employee, when you persist that employee,
I want doctrine delete those workhours that are no longer exist, update those workhours that are changed, and create new workhours that not exist before.
How can I implement this? Should I write these logic in class Employee or its Repository, or a WorkHourManager class?
I think WorkDay is a probably better name for your entity, so i'll use that :).
$workdays= $employee->getWorkDays();
$workdays->clear(); // Clear existing workdays
// Add new workdays
foreach(...) {
$workday = new WorkDay();
$workday ->setWeekday(0); // You could use a constant like `WorkDay::MONDAY`
$workday ->setStart('09:00');
$workday ->setEnd('17:00');
$workdays->add($workday);
}
Set orphanRemoval=true on $workHours to remove WorkHours without an Employee.
The setWeekday method in your Workday entity should throw an exception when an invalid weekday is supplied (other than 0-6). You could also use a Weekday value object in combination with Doctrine embeddables.
I'd go for a service or manager class in this case.
My advice is not to drop old workhours, maybe you don't needed now, but this data could be useful in the future. So, will be better just add workHours to the Employee and make a report the get the last workHours for today. About validations, there is a lot of ways of doing that. If you are working with forms and the rules are complex maybe you need read http://symfony.com/doc/current/cookbook/validation/custom_constraint.html , but maybe you can find alternatives in the action controller or the entity class itself.
I'm currently trying to map music related data using Doctrine2's POPO Annotations.
I haven't had problems mapping any other many-to-many relations, but one specific relation is giving me trouble. It does not throw an error, but the mapping does not get inserted into the mapping table (artist_album)
Artist:
<?php
/**
* #orm:Entity
* #orm:Table(name="artist")
*/
class Artist
{
...
/**
* #orm:ManyToMany(targetEntity="Company\MusicBundle\Entity\Album", inversedBy="artists", cascade={"persist"})
* #orm:JoinTable(name="artist_album",
* joinColumns={#orm:JoinColumn(name="artist_id", referencedColumnName="id")},
* inverseJoinColumns={#orm:JoinColumn(name="album_id", referencedColumnName="id")})
*
* #var ArrayCollection
*/
private $albums;
...
}
Album
....
/**
* #orm:ManyToMany(targetEntity="Company\MusicBundle\Entity\Artist", mappedBy="albums", cascade={"persist"})
*
* #var ArrayCollection
*/
private $artists;
...
}
I'm sure it's just something in I've done wrong in the mapping, but I just can't put my proverbial finger on it.
My problem was that I was setting the artist on the inverse side of the relationship. It appears you must set the relationship on the owning side (in this case, Artist).