Single Table Inheritance (STI) with a ManyToOne relationship - php

I'm pretty new to Doctrine, so any general advice is welcome. I'm trying to achieve the following:
A page can have both videos and photos. Both are media and share properties, so I thought Single Table Inheritance makes sense. Here are the relevant parts of my setup:
Page
/**
* #ORM\Entity
*/
class Page {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Media", mappedBy="page", cascade={"persist"})
* #var ArrayCollection|Media[]
*/
protected $media;
/**
* #return Media[]|ArrayCollection
*/
public function getMedia()
{
return $this->media;
}
}
Media
/**
* #ORM\Entity
* #ORM\Table(name="media")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="media_type", type="string")
* #ORM\DiscriminatorMap({"video" = "Video", "photo" = "Photo"})
*/
abstract class Media {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $url;
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="media")
*/
protected $page;
}
Photo
/**
* #ORM\Entity
*/
class Photo extends Media {
/**
* #ORM\Column(type="string")
*/
protected $exif;
}
Video
/**
* #ORM\Entity
*/
class Video extends Media {
/**
* #ORM\Column(type="integer")
*/
protected $length;
}
This works perfectly fine, but -and this is my question- how do I fetch all Photos or Videos of a page. I've tried adding things like
/**
* #ORM\OneToMany(targetEntity="Photo", mappedBy="page", cascade={"persist"})
* #var ArrayCollection|Photo[]
*/
protected $photos;
to Page, but this results in a schema error, since it doesn't have an inverse defined on Photo. Any help is greatly appreciated!

You can apply filter to the media collection
class Page {
.....
public function getPhotos()
{
return $this->getMedia()->filter(
function($media) {
return $media instanceof Photo;
}
);
}
public function getVideos()
{
return $this->getMedia()->filter(
function($media) {
return $media instanceof Video;
}
);
}
}
Bear in mind, it will fetch all medias in a single select query, instantiate both Photos and Videos, and then filter the result. It may affect performance, but you may benefit from doctrine cache, depending on your scenario.
If performance is a key, you may need to implement a custom methods in PageRepository to actually fetch only subset of medias using instance of dql as described in Query the Type.

I think you can solve this by using a join on the specific subclass:
$queryBuilder->select('m')
->from('Media', 'm')
->join('Photo', 'p', 'WITH', 'm.id = p.id')
->where('m.page = :page_id')
->setParameter('page_id', $pageId);

You won't be able to have a single $media property on your Page class which maps to a superclass. You will have to have a property per class. See the note in the Doctrine docs:
A mapped superclass cannot be an entity, it is not query-able and
persistent relationships defined by a mapped superclass must be
unidirectional (with an owning side only). This means that One-To-Many
associations are not possible on a mapped superclass at all.
Furthermore Many-To-Many associations are only possible if the mapped
superclass is only used in exactly one entity at the moment. For
further support of inheritance, the single or joined table inheritance
features have to be used.

Related

Doctrine criteria on subclasses of abstract entity

I have an abstract entity which is inherited by two subclasses. I'm using it in association with other entity and now I want to create criteria to filter out objects with the property set to some value.
My superclass:
/**
* #ORM\MappedSuperclass
* #ORM\Entity(repositoryClass="Foo\Bar\AddressRepository")
* #ORM\InheritanceType(value="SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discriminator_type", type="string")
* #ORM\DiscriminatorMap({"homeaddress" = "HomeAddress", "companyaddress" = "CompanyAddress"})
*/
abstract class AbstractAddress
{
/**
* #var bool
* #ORM\Column(type="boolean", options={"default": 0})
*/
protected $active;
}
Subclasses:
/**
* #ORM\Entity(repositoryClass="Foo\Bar\AddressRepository")
*/
class HomeAddress extends AbstractAddress
{
}
/**
* #ORM\Entity(repositoryClass="Foo\Bar\AddressRepository")
*/
class CompanyAddress extends AbstractAddress
{
}
Entity with association:
/**
* #ORM\Entity(repositoryClass="Foo\Bar\CustomerRepository")
*/
class Customer
{
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="Foo\Bar\AbstractAddress", cascade={"all"}, orphanRemoval=true)
*/
private $addresses;
/**
* #return Collection|null
*/
public function getAddresses(): ?Collection
{
return $this->addresses->matching(
Criteria::create()->where(Criteria::expr()->eq('active', true))
);
}
}
This setup produces exception:
ResultSetMapping builder does not currently support your inheritance scheme.
The problem is that it tries to instantiate AbstractAddress instead of one of the subclasses respectively.
Is there any way to achieve this filtering using Criteria API?
I know that I can use filter() instead, but this function doesn't impact SQL query, so I would rather have this solved used criteria.

Symfony2 - extending entities, abstract entity

I have 2 entities with the same fields - parent, children, order. Actually it is a mechanism and this 3 fields not applicable to content of this entity - like name, title, category etc.
I want to set this fields to one place, one class and I'm considering where should I put it. Should it be an abstract class? Or should I make a trait?
I also can use ORM\Discriminator mechanism, but I think this is for something else, not for that what I want to do.
i would make an abstract class with doctrines #MappedSuperClass annotation and shared fields and the entities extend them
here is an example with a shared created_at field
namespace Your\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\MappedSuperclass;
/**
* Abstract base class to be extended by my entity classes with same fields
*
* #MappedSuperclass
*/
abstract class AbstractEntity {
/**
* #var integer
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* Get id
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Get createdAt
* #return \DateTime
*/
public function getCreatedAt() {
return $this->createdAt;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return AbstractEntity
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
}
your entities both extend this class like:
class YourEntityClass extends AbstractEntity
{
in YourEntityClass the $id property must be "protected"
I had that issue some time ago. I also wanted to have nice abstraction for entities (or value objects, whatever) just to be a little lazy. But It is not the good way IMHO.
What if name from one entity has to have other length than the other? What if you want to add new field, add it to one entity, and some time later you wanted it in the other entity, but you forget to move it to abstraction?
I think entity is such autonomic thing that it is pointless and confusing to move some part of it to the abstraction.
Remember about KISS principle.

Doctrine 2 many-to-many with MappedSuperclass in Zend framework 2

I am new to Doctrine2 and trying to create entities for the following DB structure:
I want to have all machine parts as an array in one attribute of the machine class. I tried this:
class Machine {
....
/**
* #var array
* #ORM\OneToMany(targetEntity="MachineHasPart", mappedBy="machine", cascade={"persist", "remove"}, orphanRemoval=TRUE)
*/
private $parts;
....
public function getParts () {
return array_map(
function ($machineHasPart) {
return $machineHasPart->getPart();
},
$this->parts->toArray()
);
}
}
Where MachineHasPart is a #MappedSuperclass for the intermediate entities/tables (like machineHasCylinder etc), but it failed with:
An exception occurred while executing 'SELECT FROM machineHasPart t0'.
Should I restructure my database to use ORM here? Or there is a solution for my case?
You cannot query a #MappedSuperClass. This is also mentioned in the Doctrine2 documentation in chapter 6.1. Mapped Superclasses:
A mapped superclass cannot be an entity, it is not query-able and persistent
This means you have to either change the target entity to something queryable or you have to make MachineHasPart to a entity and change to single table inheritance.
When I look at your database structure I would suggest changing your Machine entity to have three independent relationships for the parts. One for Belt, one for Cylinder and one for Gear.
Then instead of a generic getParts you will have three methods getBelts, getCylinders and getGears.
If that is really not what you want then you can leave a comment.
UPDATE
You can solve it also with class inheritance. First make a base class Part that is also an entity and use it in the other classes Belt, Cylinder and Gear:
Part:
<?php
namespace Machine\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Part
*
* #ORM\Entity
* #ORM\Table("part")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discriminator", type="string")
* #ORM\DiscriminatorMap({
* "part" = "Part",
* "gear" = "Gear",
* "cylinder" = "Cylinder",
* "belt" = "Belt",
* })
* #property int $id
*/
class Part
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var Machine
* #ORM\ManyToOne(targetEntity="Machine\Entity\Machine", inversedBy="parts")
* #ORM\JoinColumn(name="machine_id", referencedColumnName="id", nullable=true)
*/
protected $machine;
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set id.
*
* #param int $id
* #return self
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
//... add setters and getters for machine as normal ...
}
Extend this class in your other parts:
Belt:
<?php
namespace Machine\Entity;
/**
* Belt
*
* #ORM\Entity
*/
class Belt extends Part
{
}
Cylinder:
<?php
namespace Machine\Entity;
/**
* Cylinder
*
* #ORM\Entity
*/
class Cylinder extends Part
{
}
Gear:
<?php
namespace Machine\Entity;
/**
* Gear
*
* #ORM\Entity
*/
class Gear extends Part
{
}
Now in your machine relate to the parts like as follows.
Machine:
<?php
namespace Machine\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Machine
*
* #ORM\Entity
* #ORM\Table("machine")
* #property int $id
*/
class Machine
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set id.
*
* #param int $id
* #return self
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* #var Collection
* #ORM\OneToMany(targetEntity="Machine\Entity\Part", mappedBy="machine")
*/
protected $parts;
public function __constuct()
{
$parts = new ArrayCollection();
}
/**
*
* #return Collection
*/
public function getParts()
{
return $this->parts;
}
//... add setters and getters for parts as normal ...
}
Extend this class in your other parts:
Reading further in the Doctrine2 documentation in chapter 6.1. Mapped Superclasses (referred to by #Wilt):
... Furthermore Many-To-Many associations are only possible if the mapped superclass is only used in exactly one entity at the moment...
This means in this case the ORM mapping doesn't help. I cannot gather the data of all three entities MachineHasCylinder, MachineHasBelt and MachineHasGear through a MappedSupperclass at the same time.
I think using DQL or Native SQL is the only solution for this problem.

Bidirectional association with one column on the inversed side and multiple on the owning side

I am using Doctrine 2. Let's say we have two entities: User and Bug. Is it possible to have a bidirectional association with one column on the inversed side (User) and multiple columns on the owning side (Bug)?
If I define columns in the Bug entity like this:
/** #Entity */
class Bug {
/** #ManyToOne(targetEntity="User", inversedBy="associated_bugs") */
protected $reported_by;
/** #ManyToOne(targetEntity="User", inversedBy="associated_bugs) */
protected $assigned_to;
}
then I don't know what to write in the User entity...
/** #Entity */
class User {
/**
* #OneToMany(targetEntity="Bug", mappedBy="???")
* #var Bug[]
**/
protected $associated_bugs;
}
No this is something you can not do with mapping. Lets say you would set a list of bugs to User::associated_bugs. How would you expect it to store that when calling persist?
You should map the 2 types of bugs separately and next combine them in a method.
/** #Entity */
class User {
/**
* #OneToMany(targetEntity="Bug", mappedBy="reported_by")
* #var Bug[]
**/
protected $reported_bugs;
/**
* #OneToMany(targetEntity="Bug", mappedBy="assigned_to")
* #var Bug[]
**/
protected $assigned_bugs;
protected function getAssociatedBugs()
{
return array_merge($this->reported_bugs, $this->assigned_bugs);
}
}
Something like this:
/** #Entity */
class User {
/**
* #OneToMany(targetEntity="Bug", mappedBy="assigned_to")
**/
protected $associated_bugs;
/**
* #OneToMany(targetEntity="Bug", mappedBy="reported_by")
**/
protected $reported_bugs;
}
In Bug entity, you have to add these annotations:
For assigned bugs:
#JoinColumn(name="assignee_id", referencedColumnName="id", onDelete="cascade")
and
#JoinColumn(name="reporter_id", referencedColumnName="id", onDelete="cascade")
for reported bugs
This should do the job

How to relate entities in Symfony 2 and Doctrine?

How can i create a relationship between entities with Symfony 2 and Doctrine? I'm only able to create standalone entities. Maybe someone can help me figure this out using the entity generator? I want to:
Create two entities: Post and Category. A Post is part of a Category.
Create a Tag entity: A Post can have many Tags.
A practical example is covered in Symfony2 docs here:
http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations
To elaborate, taking the first example, you need to create a OneToMany relationship between your Category object and your Post object:
Category.php:
<?php
namespace Your\CustomBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table(name="category")
* #ORM\Entity()
*/
class Category
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Post", mappedBy="category")
*/
public $posts;
/**
* Constructor
*/
public function __construct()
{
$this->posts = new ArrayCollection();
}
/**
* #return integer
*/
public function getId()
{
return $this->id;
}
}
Post.php
<?php
namespace Your\CustomBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="post")
* #ORM\Entity()
*/
class Post
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="posts")
*/
public $category;
/**
* #return integer
*/
public function getId()
{
return $this->id;
}
}
This should get you started. I've just written this so there might be errors :s
I'm making properties $posts and $category public here for brevity; however you'd probably be advised to make these private and add setters/getters to your classes.
Also note that $posts is an array-like Doctrine ArrayObject class especially for arrgregating entities, with methods like $category->posts->add($post) etc.
For more detail look into association mapping in the Doctrine documentation. You'll probably need to set up a ManyToMany relationship between Posts and Tags.
Hope this helps :)
You don't create the relationships with the entity generator itself.
Once the entity classes themselves exist (created either with the entity generator or written by hand), you then edit them to add the relationships.
For example, with your Post having many Tags example
namespace Your\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Your\Bundle\Entity\Post
*
* #ORM\Table(name="post")
* #ORM\Entity
*/
class Post
{
/**
* #var \Doctrine\ORM\PersistentCollection
*
* #ORM\OneToMany(targetEntity="Tag", mappedBy="post", cascade={"persist"})
*/
private $tags;
}
See Doctrine's Documentation for more information about specifying relationships.

Categories