Doctrine 2 how to properly map multi-level inheritance - php

I have multi-level inheritance doctrine entity like this:
/**
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"customer" = "CustomerUser",
* "admin" = "AdminUser", "stock" = "StockUser"})
*/
abstract class User { ... }
/** #ORM\Entity */
abstract class EmployeeUser extends User { ... }
/** #ORM\Entity */
class AdminUser extends EmployeeUser { ... }
/** #ORM\Entity */
class StockUser extends EmployeeUser { ... }
However it does not work this way, fields of EmployeeUser are neither read from database nor persisted.
I've found out that it works when I specify the discriminator map this way:
* #ORM\DiscriminatorMap({"customer" = "CustomerUser",
* "admin" = "AdminUser", "stock" = "StockUser", "EmployeeUser"})
it starts to work this way (there is no need to specify discriminator key for EmployeeUser - as it is abstract and will never be instantiated), but
I don't like when magic happen that I don't understand enough :) so my question is: is this a proper solution? To just let Doctrine know this way that this class is somehow included in inheritance hierarchy? Or should it be done anyhow else?
I haven't found any mention about how to do multiple level entity class inheritance in Doctrine docs.

I have same situation. I need more than one level of inheritance. That in your case is expected behavior, you need to list all of mapped classes in DiscriminatorMap.
My case don't have that map because I am using native transformation of ClassName to type key and it's working for classes on all inheritance level.
abstract ClassA
abstract ClassB extends ClassA
- protected someName
ClassC extends ClassB
When I save ClassC obj I have that property someName saved. You can try yourself without discriminatory map and see that all classes are mapped and save.
EDIT :
One more thing if you want to avoid multilevel inheritance you can always use trait for that. Just group the props to trait and add it to entity. There are good example of trait usage in DoctrineBehaviors bundle. There are using it to import additional capabilities to entities like blamable, loggable etc.

Related

Add column mappings to an extended class in Doctrine

I have a PHP entity that inherits from a base class contained in a library which I cannot change. I would like to map properties that are inherited from the base class to database columns. Since the #Column annotation must be used on a field of method, I can not define the inherited columns that way. Using #AttributeOverrides also will not work as then I would need to have access to the base class to make it a MappedSuperclass.
Example of classes:
class LibraryClass
{
protected $someProperty;
}
/**
* #Entity
* #Table(name="child")
*/
class Child extends LibraryClass
{
/**
* #Column(name="some_property", type="string")
* Somehow target $this->someProperty
*/
}
Is there any other way to map parent properties without transferring the properties between classes and not using inheritance?
You could use mapping with XML or YML instead of annotations. It's documented here.
https://symfony.com/doc/current/doctrine.html#add-mapping-information

persist parent and child entities using FOSUserBundle and "JOINED" Class Table Inheritance Doctrine 2/Symfony2.7

I am using the FOSUserBundle in order to manage my users into my application.
But in fact, I have multiple user entities: ParticularConsumer.php and ProfessionnalConsumer.php. So I create a ParentUser.php entity who as an abstract class who extends BaseUser of FOSUserBundle. See the code here:
/**
* ParentUser
*
* #ORM\Table(name="parent_users")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"particular_consumer" = "ParticularConsumer", "professionnal_consumer" = "ProfessionnalConsumer"})
* #ORM\Entity(repositoryClass="MyBundle\EntityBundle\Repository\ParentUserRepository")
*
*/
abstract class ParentUser extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(name="pusr_id", type="integer", nullable=false)
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
// ...
So there are the two other entities who extend the Parentuser.php, following the Class Table Inheritance of Doctrine behavior and documentation:
First ParticularConsumer.php
/**
* ParticularConsumer
*
* #ORM\Table(name="particular_consumer")
* #ORM\Entity(repositoryClass="MyBundle\EntityBundle\Repository\ParticularConsumerRepository")
*
*/
class ParticularConsumer extends ParentUser
{
public function __construct()
{
parent::__construct();
// your own logic
}
}
Second ProfessionnalConsumer.php
/**
* ProfessionnalConsumer
*
* #ORM\Table(name="professionnal_consumer")
* #ORM\Entity(repositoryClass="MyBundle\EntityBundle\Repository\ProfessionnalConsumerRepository")
*
*/
class ProfessionnalConsumer extends ParentUser
{
public function __construct()
{
parent::__construct();
// your own logic
}
}
Now, that I would like to do and to know is how to persist the parent and child entities.
Indeed, as I use the FOSUserBundle, all the routes (register,login, etc...) are managed and generated by this bundle. Now I need to persist datas in Child entities, normally it persists datas in parent entity, that's right ? How can I proceed exactly ?
This how I need to proceed to register a consumer:
There is a question on a page, whish ask users if there are
professionals or particulars.
A drop down list is here for them to make the choice.
Following the choice, I need to register the user in the right entity.
If the user choose particular in the drop down list, I need to
load/display a form to persist data in ParticularConsumer.php and not only in ParentUser.php.
But as I use the FOSUSerBundle, I don't really know how to proceed exactly. As you can understand, using the FOS is a practical way in order to manage users and manage the security, I would like to keep the logic of the bundle. And I want to use the good practices.
Finally, following all the doc of the FOSUserBundle in order to install it, if I want to register a user (localhost/web/app_dev.php/register) I have this error:
Error: Cannot instantiate abstract class MyBundle\EntityBundle\Entity\ParentUser
If your ParentUser is an abstract class then it cannot be an Entity. In such case you have to make it to a MappedSuperClass. But as you can read in the documentation:
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.
If you want to be able to query your ParentUser and you want this class to have its own entity repository then you will have to remove abstract and add a value for ParentUser to your #ORM\DiscriminatorMap definition:
#ORM\DiscriminatorMap({
"parent_user" = "ParentUser"
"particular_consumer" = "ParticularConsumer",
"professionnal_consumer" = "ProfessionnalConsumer"
})

Map subclass as its extended parent

I have created the following abstract class, which use single table inheritance and maps subclasses on the DiscriminatorColumn model.
/**
* #Entity
* #Table(name="entity")
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="model", type="string")
* #DiscriminatorMap({
* "green" = "model\GreenEntity",
* "blue" = "model\BlueEntity"
* })
*/
abstract class AbstractEntity
{
/** #Id #Column(type="string") */
protected $entity_id;
}
Let's say I extend the abstract class AbstractEntity by some classes:
class GreenEntity extends AbstractEntity {}
class BlueEntity extends AbstractEntity {}
And extend these by some more subclasses
class GreenEntityChildOne extends GreenEntity {}
class GreenEntityChildTwo extends GreenEntity {}
class BlueEntityChildOne extends BlueEntity {}
class BlueEntityChildTwo extends BlueEntity {}
Now, for example, when I instantiate GreenEntityChildOne and persist it to the database, it will throw an exception that I don't have a mapping for it.
What I'm trying to do is get GreenEntityChildOne to be mapped as GreenEntity (or rather, every class which extends a class below AbstractEntity to be mapped as the class which extends the upper abstract class).
Is this at all possible?
It's not possible with pure annotations
Yes, the mapping you are trying to achieve is possible. However, not with pure annotations. The important thing is that Doctrine needs to know all sub classes at runtime. If you do not want to state them explicitly in the annotations of the mapped superclass, you will need to dynamically provide them.
Doctrine event system to the rescue
There is a great blog post on dynamic mapping with Doctrine, which explains how you can use Doctrine event listeners to programmatically change the loaded ClassMetadata.
To dynamically add subclasses to the discriminator map you can implement a Doctrine event listener like the following:
class DynamicDiscriminatorMapSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(Events::loadClassMetadata);
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$metadata->addDiscriminatorMapClass("GreenEntityChildOne", GreenEntityChildOne::class);
}
}
Register your subscriber
Now you only need to register the event subscriber with Doctrine. Ideally, you inject the classes you want to add based on your configuration to the event subscriber.
// create dynamic subscriber based on your config which contains the classes to be mapped
$subscriber = new DynamicDiscriminatorMapSubscriber($config);
$entityManager->getEventManager()->addEventSubscriber($subscriber);
Further reading
Also, have a look at the PHP mapping section in the Doctrine manual and the more informative API docs for the ClassMetadataBuilder.
Answer is possibly on the Doctrine Docs:
"All entity classes that is part of the mapped entity hierarchy (including the topmost class) should be specified in the #DiscriminatorMap"
http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html
You've only specified GreenEntity and BlueEntity.
I don't know what I'm talking about. This is the first thing I've ever read about Doctrine...

Inheritance on doctrine's embeddables

Is it possible to use inheritance on value objects embedded in doctrine entities?
The situation I'm thinking about is:
I have an entity that has and embedded value object. That value object has the following hierarchy:
class myEntity {
/** #Embedded(class = "baseValueObject") */
private $value_object;
...
}
class baseValueObject {...}
class valueObject1 extends baseValueObject{...}
class valueObject2 extends baseValueObject2{...}
If I define my entity to have baseValueObject as an embeddable, nothing happens when I use the schema-tool to update my db schema, so I guess that's not the way to do it.
Another option that I'm thinking about is to use single-table inheritance on the entity to create a child entity that use one of the value objects, and another child entity for the other one. Like this:
class myEntity {
/** #Embedded(class = "baseValueObject") */
private $value_object;
...
}
class myEntityA extends myEntity {
/** #Embedded(class = "valueObject1") */
private $value_object;
...
}
class myEntityB extends myEntity {
/** #Embedded(class = "valueObject2") */
private $value_object;
...
}
class baseValueObject {...}
class valueObject1 extends baseValueObject{...}
class valueObject2 extends baseValueObject2{...}
What's the proper approach? Is it even possible to do it this way?
If you want to extend one embeddable from another you need to set the parents properties as protected not private.
https://github.com/doctrine/doctrine2/issues/4097
If you want to use your Value Object in field then you should define new type in doctrine http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html
If you want inherit properties from base then you should use #MappedSuperclass annotation http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html
You should use #Embeddable when you want split entity by specific properties by creating specific entities. So you can't use Value Object as target. According to documentation (I'm not able to share third link).

Doctrine2, Mapping "inherited" tables

as pointed out here: Doctrine 2.1 - Map entity to multiple tables Doctrine2 does not allow mapping of one object to multiple tables.
I currently have a Mysql db setup similar to this:
base_entity: id, some_basic_data_columns
state: id, state, entity_id (FK to base_entity.id), start_time, end_time, ...
entity_one: id (FK to base_entity.id), some_specific_data
entity_two: id (FK to base_entity.id), some_specific_data
and so on...
In a way, entity_x is "extending" base_entity, and all these entities can have multiple states. To have proper foreign keys I would have to either have separate state tables (which I don't want to do because they will structurally be the same ), or do it like this.
The base entity by itself is useless, id could even be boiled down to just the id field to allow to join with each child entity to multiple states.
I do not need a BaseEntity class, but I do need for each child Entity to have a getStates() method. Of course I may actually have an abstract entity class, but concrete entities will extend it, not have it as a property like they would if I would map them as one would map other one-to-one relationships
Since Doctrine will not allow me to map EntityOne to both entity_one and base_entity table I have to ask:
Is this bad design? Am I overlooking some other way to solve this elegantly? I know other DMBSs have inheritance, but for instance PostgreSql would still not allow me to join the base_entity to state if no physical base_entity exists for a child.
I could do something like this on the code side:
class EntityOne {
// baseEntity as a property
private $_baseEntity;
// private getter for the base table
private getBaseEntity();
// and getters like this for properties in the base table
public getStates(){
return $this->getBaseEntity()->getStates();
}
}
This way the entity would behave like a single entity (not combined from base and child) to the outside world, but it would still require that I write a separate BaseEntity class and all the config info to connect it to other entity classes
Basically, what I'm asking is: is this a Db design issue, and I got it completely wrong from the start (and if I did, which is the "best" approach), or is this a code issue, and I should work around it with code (if so, is my approach in 2. ok, or are there better ways to deal with this), and are there ORMs which allow for multiple table mapping?
Many thanks in advance.
You could use Class Table Inheritance (see Doctrine documentation about that), defining a BaseEntity entity class, and and create EntityOne and EntityTwo extending that.
You could define the relationship between the BaseEntity class and the State entity class as one-to-many association - if I understood right what you wanted, providing the needed getState() method in the BaseEntity class.
Something like this:
/**
* #Entity
* #Table(name="base_entity")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="entity_type", type="string")
* #DiscriminatorMap({"entity_one"="EntityOne", "entity_two"="EntityTwo"})
*/
class BaseEntity {
/**
* #Id
* #Column(type="integer")
*/
protected $id;
/**
* #OneToMany(targetEntity="State", mappedBy="entity)
**/
protected $states;
public function getStates() {
return $this->states;
}
...
}
/**
* #Entity
* #Table(name="entity_one")
*/
class EntityOne extends BaseEntity {
...
}
/**
* #Entity
* #Table(name="entity_two")
*/
class EntityTwo extends BaseEntity {
...
}
/**
* #Entity
* #Table(name="state")
*/
class State {
/**
* #ManyToOne(targetEntity="BaseEntity", inversedBy="states")
* #JoinColum(name="entity_id", referencedColumnName="id")
*/
protected $entity;
public function getEntity() {
return $this->entity;
}
...
}

Categories