Inheritence in Symfony3 with discriminator - symfony

I want to create this tables description in symfony:
GeneralAssignment:
id (int)
name (text)
course (Course)
type (1|2)
assignment (Assignment1 if type==1|Assignment2 if type==2)
Course:
id (int)
name (text)
Assignment1:
id (int)
general_assignment (GeneralAssignment)
Assignment2:
id (int)
general_assignment (GeneralAssignment)
So the best solution is to use this relation schema:
[Simple relation ManyToOne] (Course) : Course.id <------> GeneralAssignment.course (GeneralAssignment)
And
[JOINED Ineritence type] (GeneralAssignment) : GeneralAssignment.type [1=Assignment1|2=Assignment2]
So I will get this class model (sorry for this bad photo):
Problem
I used to create this model because I want to firstly, create the Course, then create the GeneralAssignment using the course id and finally, create the specific assignment using the general assignment's id
The problem is, I can't create a relation doctrine relation between Assignment1 and GeneralAssignment using Assignment1.general_assignment : "Cannot instantiate abstract class AssignmentGeneral". (the same thing between Assignment2 and AssignmentGeneral).
I need the Assignment1.assignment_general attribute because i'm going to use Assignment1 and Assignment2 classes later and if I can't know the general_assignment id, I can't even know the assignment general informations because of the inheritence from AssignmentGeneral to the specific Assignment classes (Assignment1, Assignment2)
The AssignmentGeneral is an abstract class because of the DiscriminatorMap requires that the class should be abstract.
So what to should I do ?
I can't set the AssignmentGeneral from "abstract class" to "class" and I can't create a relation between Assignment1 (or Assignment2) and GeneralAssignment in Assignment1
PS : These class names are not the real ones, but they are similiar to my real problem. Just to get it simple, I exposed this simple problem.
Thank you very much for each answer
Best regards
[Edit]
I'm adding the AssignmentGeneral declaration
/**
*
* #ORM\Table(name="assignment_general")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\MappedSuperclass
* #ORM\DiscriminatorMap({
1 = "Assignment1",
* 2 = "Assignment2"
* })
*/
abstract class StatisticsGeneralAbstract
{
//...
}
And also:
/**
*
* #ORM\Table(name="assignment1")
*
*/
class Assignment1 extends AssignmentGeneral
{
//...
}
/**
*
* #ORM\Table(name="assignment2")
*
*/
class Assignment2 extends AssignmentGeneral
{
//...
}

Class table inheritance ('JOINED') doesn't require the parent class to be abstract.
Besides, one of the benefits of inheritance is to get the parent's properties when you instantiate a child class. So you don't have to instantiate both, just instantiate Assignment1 or Assignment2.
Add relations to both of them in the Course entity and an assignmentType property so that you know which one it's linked to.

Related

Is it possible to use a custom naming strategy for auto generated Doctrine DiscriminatorMaps?

I am working a custom bundle which should be used in different Symfony 5+ projects. The bundle includes some entities which inherit from the same Mapped Superclass:
/**
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\Table(name="vehicles")
* #ORM\DiscriminatorColumn(name="vehicle_type", type="string")
* #ORM\DiscriminatorMap({
* "race_car" = "RaceCar",
* "sailboat" = "Sailboat",
* ...})
*/
abstract class Vehicle {
/** #Column(type="string") */
protected $color;
// ... more fields and methods
}
/**
* #ORM\Entity
*/
class RaceCar extends Vehicle {
...
}
/**
* #ORM\Entity
*/
class SailBoat extends Vehicle {
...
}
This works fine and it is no problem to use a custom naming strategy (or completely custom type names) in the DiscriminatorMap.
Now I would like to add more Vehicle sub classes to a project which uses the bundle. Since it seems not to be possible to extend the DiscriminatorMap outside its original definition (is it?), I switched to not specifying a map manually but letting Doctrine build it automatically:
If no discriminator map is provided, then the map is generated
automatically. The automatically generated discriminator map contains
the lowercase short name of each class as key.
While this works in a new project, all existing entries in the database of an existing project already use the custom naming. This would conflict with the auto generated lowercase naming: e.g. race_car vs. racecar.
Is it possible to provide a custom naming strategy which is than be used by Doctrine to build the map?
Or do I have to manually update the database by replacing the old mapping names with the new (lowercase) ones?

How to make relation to MappedSuperclass works?

I have problem with relation Many to One to MappedSuperclass in Symfony 5. I have entities Employee, Employer which extends MappedSuperclass abstract class Person. And I want create raports entity which will be in relation with Person (both Employee and Employer) like below:
/**
* #ORM\ManyToOne(targetEntity=Person::class)
*/
private $person;
But when i try to push migrations I get following error message:
Column name id referenced for relation from App\Entity\Raport towards App\Entity\Person does not exist.
but I have id properties in these classes:
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
protected $id;
I found on the Symfony page example with creating interface to do it but it doesn't work for me too. Maybe someone had that problem before and know how to resolve it. Thanks a lot for any reply.
EDIT
My Person class:
**
* Abstract base class to be extended by my entity classes with same fields
*
* #MappedSuperclass
*/
abstract class Person
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
protected $id; //This property exist in Employee and Employer too (like doctrine documentation said)
Now when I change it from superclass to 'JOINED' inheritance when I try create now Employee or Employer I get following error:
An exception occurred while executing 'INSERT INTO person (discr) VALUES
(?)' with params ["Employee"]:
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error i
n your SQL syntax; check the manual that corresponds to your MySQL server v
ersion for the right syntax to use near 'person (discr) VALUES ('Employee')'
at line 1
There isn't any other way to just make relation in one property to few entities whose implements one interface or extends class?? Sometimes I hate doctrine... And my person entity maybe it helps configure what is wrong:
* #ORM\Entity(repositoryClass=PersonRepository::class)
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"person" = "Person", "employee" = "Employee", "employer" = "Employer"})
*/
class Person
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
protected $id;
public function getId(): ?int
{
return $this->id;
}
}
You can't have a relation target a MappedSuperClass, since it's NOT an entity itself. It only can itself define an owning one-to-one relationship (or very limited many-to-many):
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.
source: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/inheritance-mapping.html#mapped-superclasses
What you might want is a single table inheritance or a class table inheritance (see link above, it shows those as well). It can get quite annoying though. You've been warned.
It's possible to have relations into MappedSuperClass (in Symfony). What you want is dynamic mapping, which is not well documented. You have to use Doctrine Event loadClassMetadata. The doc example is very (very) simple, but you can do such more.
In your case, you must check if the entity is the one you want and is not a MappedSuperClass :
<?php
namespace App\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
class LoadClassMetadataListener implements EventSubscriber
{
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata,
];
}
public function loadClassMetadata(\Doctrine\ORM\Event\LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$class = new \ReflectionClass($metadata->getName());
if ($class->implementsInterface('Your\Interface') && !$metadata->isMappedSuperclass) {
$metadata->mapManyToOne(array(
'targetEntity' => Person::class,
'fieldName' => 'person',
'inversedBy' => 'whatYouWant',
'joinColumns' => [[
'name' => 'xxx_id',
'referencedColumnName' => 'id',
'nullable' => false,
]]
));
}
}
}
services:
App\Doctrine\LoadClassMetadataListener
tags:
- { name: doctrine.event_subscriber }
$metadata gives access to all mapping possibilities and all mapping options, like with annotations, yaml or XML.
Try to remove the abstract. I do not have a MappedSuperclass as a relation. I have set the MappedSuperclass is abstract for a class in a project and I get an error. Abstract class seems not to be allowed. It also makes sense because it is not an abstract class in that sense.

Entity associated with non-entity

I have an interface SupplierInterface with 2 implementations: B2BSupplier (a Doctrine entity), RetailSupplier (a static object).
<?php
namespace MyBundle\Model;
interface SupplierInterface {
const B2B = 'B2B';
const RETAIL = 'Retail';
/**
* #return string
*/
public function getSupplierType();
/**
* #return string
*/
public function __toString();
}
Another entity, Supply has a many-to-one relationship with a Supplier. Normally this isn't problematic. But because RetailSupplier is not a Doctrine entity, I'm a bit flummoxed about how to proceed.
Supply looks like this:
<?php
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Blameable\Traits\BlameableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
/**
* Supply
*
* #ORM\Table(name="cir_supply")
* #ORM\Entity()
*/
class Supply
{
use BlameableEntity;
use TimestampableEntity;
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="B2BSupplier")
* #ORM\JoinColumn(name="supplier_id", referencedColumnName="id", nullable=true)
*/
protected $supplier; // <-- PROBLEM, since supplier could be B2BSupplier entity, or it could be vanilla object RetailSupplier
/**
* #ORM\ManyToOne(targetEntity="Chemical", inversedBy="supplies")
* #ORM\JoinColumn(name="chemical_id", referencedColumnName="id", nullable=false)
*/
protected $chemical;
/**
* #ORM\Column(name="external_id", type="string")
*/
protected $externalId;
//getters and setters ...
}
How do I specify a Doctrine relationship when that relationship might not always be valid?
From my experience I'm 99% sure you can't do what you want in your current setup. That being said, there are a few workarounds I can think of. Also before I go into the workarounds. You should think if you really want OneToOne relation on 'supplier' or will ManyToOne work better. OneToOne has some Lazy loading issues and also Workaround 3 work better with ManyToOne.
Workaround 1:
Remove the relation and make the supplier filed contain the id, without having a relation defined.
Extend SupplierRepository 'find' method to handle the cases where id is
2.1 'null' there is no relation in witch case it returns RetailSupplier
2.2 call parent::find for all other cases
2.3 Optional: if null relations are required change 2.1 to use '0' instead of null (adds con 3)
Pros:
fast to achieve from your current setup
keep database foreign key (if step 2.3 is ignored)
Cons:
hidden behavior of the 'find' method
you loose the your doctrine relation
not scalable for other types of Suppliers
source of the information is split between the app and the database
if step 2.3 is required, you loose database foraign key ('0' will not be a foraign key)
Workaround 2:
Modify getSupplier to return RetailSupplier if $this->supplier is null
Modify setSupplier to set null if $supplier is instance of RetailSupplyer
Optinal: Change the first 2 steps to handle '0' as RetailSupplyer and 'null' as no relation
Pros:
fast to achieve from your current setup
keep database foreign key (if step 3 is ignored)
keep doctrine relation
Cons:
hidden behavior of the setter and getter
not scalable for other types of Suppliers
if step 3 is required, you loose database foraign key ('0' will not be a foraign key)
source of the information is split between the app and the database
Workaround 3 (doctrine inheritance mapping):
Create an abstract (called Supplier) this will be inherited by RetailSupplyer and B2BSupplier
Add inheritance metadata to Supplier abstract something like this
Create an entity for RetailSupplyer and a database table with one single line to start (the first RetailSupplier)
Change your database to match your inheritance mapping settings (for more info http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html)
Change your relation to ManyToOne on $supplier and make it to point to Supplier
Pros:
source of the information is only the database
no hidden behavior in your code
scalable for other types of suppliers and other more retail suppliers
Cons:
harder to achieve from your current setup (database changes, new doctrine setup, possibly some refactor)
pros/cons: Depending on the selected inheritance type you can have full relation path in your database (with foraign key), or you can have no relations. This is up to you ;) after you read the documentation for inheritance mapping.
PS: If I had to choose i will go with Workaround 3. It is hardest to achieve, but solid do it.
Hope this helps and happy coding
Alexandru Cosoi

Use Doctrine to merge entity with different subclass

I have a mapped superclass AbstractQuestion with single-table-inheritance.
/**
* #ORM\Entity
* #ORM\MappedSuperclass
* #ORM\Table(name="Question")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="dtype", type="string")
* #ORM\DiscriminatorMap({
* "simple": "SimpleQuestion",
* "dropdown": "DropdownQuestion"
* })
*/
abstract class AbstractQuestion
SimpleQuestion and DropdownQuestion inherit from this superclass.
/**
* Class SimpleQuestion.
* #ORM\Entity()
*/
class SimpleQuestion extends AbstractQuestion
I want to modify an existing SimpleQuestion and make it a DropdownQuestion.
When saving a question, I deserialise and merge the question, which contains an ID and the 'dtype' and other properties.
$dquestion = $this->serial->fromJson($request->getContent(), AbstractQuestion::class);
$question = $this->em->merge($dquestion);
$this->em->flush();
So I submit something like:
{ id: 12, dtype: 'dropdown', 'text': 'What is my favourite animal?'}
After the deserialisation, $dquestion is a DropdownQuestion object as I desired, but after the merge $question is a SimpleQuestion object as it was in the database previously, so any unique properties of DropdownQuestion are lost and the question is saved as a SimpleQuestion. Is there any way to work around this?
You will first have to delete the existing record (SimpleQuestion) and then insert the new record (DropdownQuestion). Type casting is not supported in Doctrine 2.
Note.
You can probably change the discriminator column with a pure SQL query, but this is absolutely not recommended and will for sure give you problems...
Check also the answers here and here since they might be interesting for you.

Symfony2 - Share Entity Between Bundles with different relationships

How do you share an entity between multiple bundles with different relationships?
For example both the ZooAnimalBundle and FarmAnimalBundle need a User Entity. A third Bundle AccountUserBundle has the User Entity.
In both the Zoo and Farm AnimalBundles I create a User Entity like so:
use Account\UserBundle\Entity\User as BaseUser;
class User extends BaseUser
{
}
I then have a Hospital entity in Zoo:
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_zoo_animals")
*/
protected $users;
And a Room entity in Farm:
class Room {
/**
* #ORM\ManyToMany(targetEntity="Farm\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_farm_animals")
*/
protected $users;
Everything works so far in that I can call Zoo.Room->getUsers() or Farm.Hospital->getUsers()
However the problem is I'm not sure on how to set up the inverse relationship in their respective User entities.
If for example I update the FarmAnimal User Entity and run doctrine:generate:entities
/**
* #ORM\Entity
*/
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Room", mappedBy="users", cascade={"persist"})
*/
protected $rooms;
}
It will copy the protected $properties from BaseUser and create all the set and get methods which is not what I want. What is the correct way of setting up these relationships?
Update
If you don't setup the inverse relationship, how would you select all users where hospital.id = 1
$qb = $this->getEntityManager()->createQueryBuilder()
->select(
'u'
)
->from('Account\UserBundle\Entity\User','u')
->leftJoin('u.hospitals', 'h')
->andWhere('h.id = :hospital_id')
->setParameter('hospital_id',$hospital_id);
This gives the error:
Class Account\UserBundle\Entity\User has no association named hospitals
I know I could select from hospital and join user because that relationship does exist but I need to select users because I am using them with Doctrine\ORM\Tools\Pagination\Paginator
The query would be
$qb = $this->createQueryBuilder('a')
->select(
'h', 'u'
)
->leftJoin('h.users', 'u')
The problem with this is Paginator only sees one result Hospital because the Users are attached to it.
You can define abstract entity dependencies and implement them with other bundles.
First, each of the bundles depending on a user entity should define a User interface. For example:
namespace Foo\BarBundle\Entity;
interface UserInterface
{
public function getId();
public function getEmail();
// other getters
}
Then, in each entity depending on the user, define the relationship, e.g.:
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Something
{
/**
* #ORM\ManyToOne(targetEntity="UserInterface")
* #Assert\NotNull
*/
protected $User;
// add other fields as required
}
Now you need to register the User entity as an implementation of the UserInterfaces:
namespace Foo\UserBundle\Entity;
use Foo\BarBundle\Entity\UserInterface as BarUserInterface;
use Foo\FoobarBundle\Entity\UserInterface as FoobarUserInterface;
/**
* #ORM\Entity
*/
class User implements BarUserInterface, FoobarUserInterface
{
// implement the demanded methods
}
Then add the following to app/config/config.yml:
doctrine:
orm:
resolve_target_entities:
Foo\BarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
Foo\FooarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
(Heads up: there will usually already be a doctrine.orm node which you'll have to extend.)
This is not a perfect solution, because you cannot really say which fields the user entity should have. On the other hand, it's strictly OOP, as you don't have to know about internals of the User implementation – you just need it to return the right values.
Creating multiple definitions of the account is the wrong way to do it, unless you want to create 3 seperate user tables (even then it's better not to do it this way).
Really you want your other entities to map to the your user entity in the account bundle.
I.e.,
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AccountBundle\Entity\User")
*/
protected $users;
Now, there is no need to create the inverse relationship. In fact, this is a bad practice since you have a bi-directional dependency. Users don't know about hospitals, but hospital knows about it's users. Now, any bundle can map to the user entity and reuse it.

Resources