Multiple JoinColumns in Symfony2 using Doctrine annotations? - symfony

Here is the problem:
Class Routing with attributes objectId and objectType. objectId is an int, and objectType is a string. The reason for this was to allow the same table to hold data for different kind of routings. For instance for the routing of Products, Department and Brand. So, the combination of the objectType and the objectId is my JoinColumn.
How do I create such a bidirectional relationship with Doctrine2? I looked at inherited relationships, but none of the concepts seemed to be what I'm looking for.
I could create some views in the database and just have a couple of different Routing entities, but this does not seem the best route.
Here are my entities Department, Product and Brand.
../Entity/Department.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table(name="departments")
*/
class Department implements DescribableInterface
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer", length=11)
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="status", type="string", length=1)
*/
private $status;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
...
../Entity/Product.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="products")
*/
class Product implements DescribableInterface
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer", length=11)
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="status", type="string", length=1)
*/
private $status;
/**
* #ORM\Column(name="product_code", type="string", length=100, nullable=true)
*/
private $productCode = '';
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
...
../Entity/Brand.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="brands")
*/
class Brand
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer", length=11)
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="status", type="string", length=1)
*/
private $status = 'a';
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
...
Each product, brand and department has its own URL that is held in the routing table set by the object_type and object_id where the object_type is simply department, product or brand and the object_id is the unique id of the corresponding product, brand or department.
../Entity/Routing.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="routing")
*/
class Routing
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer", length=11)
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="object_id", type="integer", length=11)
*/
private $objectId;
/**
* #ORM\Column(name="object_type", type="string", length=100)
*/
private $objectType;
/**
* #ORM\Column(name="url", type="text")
*/
private $url;
...
What I am really struggling with is how do I setup the relationship, so the departments, products and brands can access their URL from the single routing entity.
I have tried adding the relationships to the $objectId, but it doesn't seem to like that. Is it possible to set this up like this?
What I am basically trying to achieve is to get the data object and have the ability to get the URL of an object, for example:
$departments = $em->getRepository("AdamStaceySiteBundle:Department")->findAll();
foreach ($departments as $department)
{
echo ''.$department->getMenuTitle().';
}
Can anyone help?

After further researching I found a man (Dirk Olbertz) in the know who had the same problem.
Information can be found at: Google Groups: Multiple JoinColumns?
I have now implemented this and I will explain how I did it incase it might help anyone else.
The answer to my problem was the use of single table inheritance.
The first thing I needed to do was update the routing entity to use single table inheritance:
../Entity/Routing.php
/**
* #ORM\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="object_type", type="string")
* #ORM\DiscriminatorMap({"product" = "ProductRouting", "department" = "DepartmentRouting", "brand" = "BrandRouting"})
* #ORM\Table(name="routing")
*/
class Routing
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer", length=11)
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
The DiscriminatorColumn allowed me to specify what column would be used to link, which in this case was the object_type field.
The DiscriminatorMap allowed me to specify what object_type will link with what entities.
These entities then had to be created that extended the Routing entity.
../Entity/ProductRouting.php
/**
* #ORM\Entity
*/
class ProductRouting extends Routing
{
/**
* #ORM\ManyToOne(targetEntity="Product")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id")
*/
protected $product;
...
../Entity/DepartmentRouting.php
/**
* #ORM\Entity
*/
class DepartmentRouting extends Routing
{
/**
* #ORM\ManyToOne(targetEntity="Department")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id")
*/
protected $department;
...
../Entity/BrandRouting.php
/**
* #ORM\Entity
*/
class BrandRouting extends Routing
{
/**
* #ORM\ManyToOne(targetEntity="Brand")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id")
*/
protected $brand;
...
Then in each of the Product, Department and Brand entities I needed to add the new $routings.
../Entity/Product.php
...
class Product
{
...
/**
* #ORM\OneToMany(targetEntity="ProductRouting", mappedBy="product", cascade={"all"})
*/
private $routings;
...
../Entity/Department.php
...
class Department
{
...
/**
* #ORM\OneToMany(targetEntity="DepartmentRouting", mappedBy="department", cascade={"all"})
*/
private $routings;
...
../Entity/Brand.php
...
class Brand
{
...
/**
* #ORM\OneToMany(targetEntity="BrandRouting", mappedBy="brand", cascade={"all"})
*/
private $routings;
...
Hope that helps...

Related

Add ArrayCollection of Entities in the same entity type

Is it possible to keep an array of entities A in the entity A ? How to do this with Doctrine ?
I have :
class A {
/**
* #var \Doctrine\Common\Collections\ArrayCollection
*/
private $sisters;
}
But I don't know what to add to have Doctrine do what I need.
A can have many sisters, and many sisters can be sister of A (Many-To-Many, Self-referencing):
/**
* #ORM\Entity()
* #ORM\Table()
*/
class A
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var \Doctrine\Common\Collections\ArrayCollection
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\A")
*/
private $sisters;
}
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#many-to-many-self-referencing

How to remove usage of cascade={"remove","persist"} from entities in Symfony2?

I am working on a project where there I am forced to use cascade={"remove","persist"} because of the problem described here.
Reading through the documentation, to quote:
Even though automatic cascading is convenient it should be used with care. Do not blindly apply cascade=all to all associations as it will unnecessarily degrade the performance of your application. For each cascade operation that gets activated Doctrine also applies that operation to the association, be it single or collection valued.
And I see that the same can be fixed if I use
$em->persist($entity);
In my persistence services, which I am already calling. However, doctrine doesn't work as expected. Here are my entities.
Entity/Employee.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping AS ORM;
/**
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(
* name="UNIQ_EMPLOYEE_ID_NAME_ADDRESS_STATE_CITY_COUNTRY",
* columns={"id","name","city","state","country"}
* )})
*/
class Employee
{
/**
* #ORM\Id
* #ORM\Column(type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $name;
/**
* #ORM\Column(type="text", nullable=false)
*/
private $address;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $city;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $state;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $country;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Department", inversedBy="employee", cascade={"remove","persist"})
* #ORM\JoinColumn(name="department_id", referencedColumnName="id", nullable=false)
*/
private $department;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Transfer", mappedBy="employee", cascade={"remove","persist"})
*/
private $transfer;
}
?>
Entity/Department.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping AS ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Selectable;
/**
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(
* name="UNIQ_DEPARTMENT_ID_NAME",
* columns={"id","name"}
* )})
*/
class Department
{
/**
* #ORM\Id
* #ORM\Column(type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Employee", mappedBy="department", cascade={"remove","persist"})
*/
private $employee;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Transfer", mappedBy="department", cascade={"remove","persist"})
*/
private $transfer;
}
?>
Entity/Transfer.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping AS ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Selectable;
/**
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(
* name="UNIQ_TRANSFER_ID_DEPARTMENT_EMPLOYEE_START_END",
* columns={"id","name"}
* )})
*/
class Transfer
{
/**
* #ORM\Id
* #ORM\Column(type="integer", options={"unsigned":true})
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="date", length=255, nullable=false)
*/
private $start;
/**
* #ORM\Column(type="date", length=255, nullable=false)
*/
private $end;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Employee", inversedBy="attendance", cascade={"persist","remove"})
* #ORM\JoinColumn(name="employee_id", referencedColumnName="id", nullable=false)
*/
private $employee;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Department", inversedBy="attendance", cascade={"persist","remove"})
* #ORM\JoinColumn(name="department_id", referencedColumnName="id", nullable=false)
*/
private $department;
}
?>
UPDATE 1:
Now, I have another problem. Since my GeneratorValue strategy for Employee and Department are NONE, I have problem with duplicate record error. I am trying to use PreFlushEventArgs to remove the entity before persisting if the record exists in database. But I wonder if it should be that complex?
Thanks for your help in advance.
It seems you cascade persist operation from Employee to Department and Transfer. But they do cascade the persist operation to the Employee Entity too.
It means when you do
$em->persist($an_employee);
You are stuck in a persist loop.
In my opinion, the cascade persist should be only in one way, i.e. only on the Employee entity.
Also if you choose to do this, add your Department to the Employee, and not the opposite :
$an_employee->addDepartment($a_department);
$an_employee->addTransfer($a_transfer);
That way, when you persist an employee, its Departments and Transfer should be persisted too

How can I access a global list within a entity without relation mappings?

I have two doctrine entities defined as below:
/**
* #ORM\Table(name="users")
* #ORM\Entity
*/
class User {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
*/
private $name;
}
/**
* #ORM\Table(name="holidays")
* #ORM\Entity
*/
class Holiday {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="date")
*/
private $date;
}
Holiday is a global list of holidays. There is no relation (Doctrine Mapping) between User and Holiday but I need to access holidays in user class.
How can I access holidays within user class without any mappings? I don't want to inject Entity Manager into my user class.
Update: Let's assume I have a method in User like below:
function getWorkedDaysExcludingHolidays() {
// calculation $days here based on holidays
return $days;
}

Doctrine2 relation for non Id column

I'm building a simple web-service using Symfony 3, Doctrine 2.5 and stuck at ORM relations described below in simplified structure.
I have an Action entity containing many actions with ManyToOne relation...
class Action
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="\AppBundle\Entity\Status")
* #ORM\JoinColumn(referencedColumnName="code", nullable=false)
*/
private $status;
and the Status Entity with a few statuses.
class Status
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="integer", unique=true)
*/
private $code;
I cannot get proper way to set referencedColumnName="code" column (not 'Id' as usual) for Action entity.
Configured this way repo throws wxception at persist moment with "Notice: Undefined index: code";
I guess that it is mappedBy or inversedBy annotation parameter... but can't figure out "how".
Unfortunately it's not supported in Doctrine (reference).
You may edit your Status entity like this (ensure that code is set before persist):
class Status
{
/**
* #ORM\Column(name="code", type="integer", unique=true)
* #ORM\Id
*/
private $code;
}
If autoincremented field is your requirement you can take a look on this answer for possible solutions.
Just thought I'd add you can still use the non-primary keys as many to many, by using the entity itself as the join table. This will work but you still need to set your relationship keys correctly.
Example:
/**
* #ORM\Entity
*/
class Car {
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #ORM\Column(name="registration_code", type="text", length=128, nullable=false)
* #var string
*/
public $registrationCode;
/**
* #var \Doctrine\Common\Collections\Collection
* #ORM\ManyToMany(targetEntity="Registration", mappedBy="Cars")
* #ORM\JoinTable(name="car",
* joinColumns={#ORM\JoinColumn(name="id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="registration_code", referencedColumnName="registration_code")}
* )
*/
public $Registrations;
public function __construct() {
$this->Cars = new ArrayCollection();
}
}
/**
* #ORM\Entity
*/
class Registration {
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #ORM\Column(name="registration_code", type="text", length=128, nullable=false)
* #var string
*/
public $registrationCode;
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="Car", mappedBy="Registrations")
* #ORM\JoinTable(name="car",
* joinColumns={#ORM\JoinColumn(name="registration_code", referencedColumnName="registration_code")},
* inverseJoinColumns={#ORM\JoinColumn(name="id", referencedColumnName="id")}
* )
*/
public $Cars;
public function __construct() {
$this->Cars = new ArrayCollection();
}
}
The upside is that it works fine as a workaround.
Keep in mind a few things:
it's a collection not a single instance;
column has to be managed manually on your end;
you must set up constraints correctly (indexes, keys, etc);
check your queries still perform!

Symfony2 doctrine ManyToMany associations mapping invalid?

I have two entities relating to this issue, and I want a join table to be able to cross reference values in each table.
Here is an explanation:
Entity ContainerType.php:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\Containers;
/**
* ContainerType
*
* #ORM\Table(name="container_type")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class ContainerType
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="type", type="string", length=255)
*/
private $type;
/**
* #var \DateTime
*
* #ORM\Column(name="date_added", type="datetime")
*/
private $dateAdded;
/**
* #var \DateTime
*
* #ORM\Column(name="date_modified", type="datetime", nullable=true)
*/
private $dateModified;
/**
* #ORM\ManyToMany(targetEntity="Containers", inversedBy="type")
* #ORM\JoinTable(name="container_type_containers")
**/
private $container;
public function __construct()
{
$this->container = new ArrayCollection();
}
And entity Containers.php:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\ContainerType;
/**
* Containers
*
* #ORM\Table(name="containers")
* #ORM\Entity
*/
class Containers
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="number", type="string", length=255)
*/
private $number;
/**
* #ORM\ManyToMany(targetEntity="Containers", mappedBy="container")
*/
private $type;
public function __construct()
{
$this->type = new ArrayCollection();
}
And although the schema update works without problems, when I do a doctrine:schema:validate I get the following fail:
[Mapping] FAIL - The entity-class 'AppBundle\Entity\Containers' mapping is invalid:
* The association AppBundle\Entity\Containers#type refers to the owning side field AppBundle\Entity\Containers#container which does not exist.
But the $container field DOES exist in ContainerType so I do not understand why it's trying to reference a field called container in the Containers entity?
Can anyone shed any light on this?
Thank you
Michael
I think this code should works fine for you :)
ContainerType.php
/**
* #ORM\ManyToMany(targetEntity="Containers", inversedBy="containersType")
* #ORM\JoinTable(name="container_type_containers",
* joinColumns={#ORM\JoinColumn(name="container_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="container_type_id", referencedColumnName="id")})
*/
protected $containers;
Containers.php
/**
* #ORM\ManyToMany(targetEntity="ContainerType", mappedBy="containers")
*/
protected $containersType;
I have just realized my error!
I had used the wrong target Entity in the Containers.php file, I should have used ContainerType as the target, instead I had Containers which is why it was trying to find the field in the wrong table!

Resources