Doctrine2 Class Table Inheritance - php

I am a little bit confused by Doctrine's documentation so maybe you can help me. I have the following class inheritance:
<?php
class User
{
/**
* #var int
*/
private $_id;
/**
* #var Role
*/
private $_role;
}
class Company extends User
{
}
class Customer extends User
{
...
}
class Role
{
/**
* #var int
*/
private $_id;
}
?>
I want to store each class in a separate table. The role defines the type of user by an id. How would I solve this problem? I tried this:
<?php
/**
* #Entity
* #Table(name="user")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="role_id", type="integer")
* #DiscriminatorMap({"1" = "User", "2" = "Customer"})
*/
class User
{
...
}
?>
I am not sure how to handle the role class in this scenario.
Thank you for your answer. Now I tried this and got following error:
[Doctrine\DBAL\Schema\SchemaException]
There is no column with name '_id' on table 'customer'.
I have following code:
<?php
/**
* Class Sb_User
*
* #Entity
* #Table(name="user")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="role_id", type="integer")
* #DiscriminatorMap({"2" = "Sb_Customer", "8" = "Sb_Pos"})
*/
class Sb_User implements Sb_User_Interface
{
/**
* #Id
* #GeneratedValue
* #Column(name="id", type="integer")
* #var int
*/
protected $_id;
...
}
/**
* Class Sb_Customer
*
* #Entity
* #EntityResult(discriminatorColumn="role_id")
* #Table(name="customer")
*
*/
class Sb_Customer extends Sb_User implements Sb_Customer_Interface
{
....
}
I do not know what I am doing wrong. Can you help me?
?>

Your question is a bit confusing to me.
You want to use joined table inheritance to define the role of a user, but you also want to add a role attribute to your user that, as I understand, does exactly the same. Seems like you're trying to do the same thing twice in a different way.
Anyway, I will try to give you an answer.
If you want to use separate tables for each type (Customer, Company, etc) you should have a look at mapped superclasses:
http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html#mapped-superclasses
That way you can define basic class attributes and relations that will be used by all the entities that extend it. All data will be saved in separate tables for each entity.
If you want to define the role of a user using the Role entity you should define a many-to-one relation between user and role.
Good luck!

Related

How to override Doctrine's field association mappings in subclasses when using PHP 8 attributes?

How can I define a Doctrine property in a parent class and override the association in a class which extends the parent class? When using annotation, this was implemented by using AssociationOverride, however, I don't think they are available when using PHP 8 attributes
Why I want to:
I have a class AbstractTenantEntity whose purpose is to restrict access to data to a given Tenant (i.e. account, owner, etc) that owns the data, and any entity which extends this class will have tenant_id inserted into the database when created and all other requests will add the tenant_id to the WHERE clause. Tenant typically does not have collections of the various entities which extend AbstractTenantEntity, but a few do. When using annotations, I handled it by applying Doctrine's AssociationOverride annotation to the extended classes which should have a collection in Tenant, but I don't know how to accomplish this when using PHP 8 attributes?
My attempt described below was unsuccessful as I incorrectly thought that the annotation class would magically work with attributes if modified appropriately, but now I see other code must be able to apply the appropriate logic based on the attributes. As such, I abandoned this approach and just made the properties protected and duplicated them in the concrete class.
My attempt:
Tenant entity
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[Entity()]
class Tenant
{
#[Id, Column(type: "integer")]
#[GeneratedValue]
private ?int $id = null;
#[OneToMany(targetEntity: Asset::class, mappedBy: 'tenant')]
private array|Collection|ArrayCollection $assets;
// Other properties and typical getters and setters
}
AbstractTenantEntity entity
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\JoinColumn;
abstract class AbstractTenantEntity implements TenantInterface
{
/**
* inversedBy performed in child where required
*/
#[ManyToOne(targetEntity: Tenant::class)]
#[JoinColumn(nullable: false)]
protected ?Tenant $tenant = null;
// Typical getters and setters
}
This is the part which has me stuck. When using annotation, my code would be as follows:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\AssociationOverrides({
* #ORM\AssociationOverride(name="tenant", inversedBy="assets")
* })
*/
class Asset extends AbstractTenantEntity
{
// Various properties and typical getters and setters
}
But AssociationOverrides hasn't been modified to work with attributes, so based on the official class, I created my own class similar to the others which Doctrine has updated:
namespace App\Mapping;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\ORM\Mapping\Annotation;
/**
* This annotation is used to override association mapping of property for an entity relationship.
*
* #Annotation
* #NamedArgumentConstructor()
* #Target("ANNOTATION")
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class AssociationOverride implements Annotation
{
/**
* The name of the relationship property whose mapping is being overridden.
*
* #var string
*/
public $name;
/**
* The join column that is being mapped to the persistent attribute.
*
* #var array<\Doctrine\ORM\Mapping\JoinColumn>
*/
public $joinColumns;
/**
* The join table that maps the relationship.
*
* #var \Doctrine\ORM\Mapping\JoinTable
*/
public $joinTable;
/**
* The name of the association-field on the inverse-side.
*
* #var string
*/
public $inversedBy;
/**
* The fetching strategy to use for the association.
*
* #var string
* #Enum({"LAZY", "EAGER", "EXTRA_LAZY"})
*/
public $fetch;
public function __construct(
?string $name = null,
?array $joinColumns = null,
?string $joinTable = null,
?string $inversedBy = null,
?string $fetch = null
) {
$this->name = $name;
$this->joinColumns = $joinColumns;
$this->joinTable = $joinTable;
$this->inversedBy = $inversedBy;
$this->fetch = $fetch;
//$this->debug('__construct',);
}
private function debug(string $message, string $file='test.json', ?int $options = null)
{
$content = file_exists($file)?json_decode(file_get_contents($file), true):[];
$content[] = ['message'=>$message, 'object_vars'=>get_object_vars($this), 'debug_backtrace'=>debug_backtrace($options)];
file_put_contents($file, json_encode($content, JSON_PRETTY_PRINT));
}
}
When validating the mapping, Doctrine complains that target-entity does not contain the required inversedBy. I've spent some time going through the Doctrine source code but have not made much progress.
Does my current approach have merit and if so please fill in the gaps. If not, however, how would you recommend meeting this need?
It has been resolved by this pr: https://github.com/doctrine/orm/pull/9241
ps: PHP 8.1 is required
#[AttributeOverrides([
new AttributeOverride(
name: "id",
column: new Column(name: "guest_id", type: "integer", length: 140)
),
new AttributeOverride(
name: "name",
column: new Column(name: "guest_name", nullable: false, unique: true, length: 240)
)]
)]
Override Field Association Mappings In Subclasses
Sometimes there is a need to persist entities but override all or part of the mapping metadata. Sometimes also the mapping to override comes from entities using traits where the traits have mapping metadata. This tutorial explains how to override mapping metadata, i.e. attributes and associations metadata in particular. The example here shows the overriding of a class that uses a trait but is similar when extending a base class as shown at the end of this tutorial.
Suppose we have a class ExampleEntityWithOverride. This class uses trait ExampleTrait:
<?php
/**
* #Entity
*
* #AttributeOverrides({
* #AttributeOverride(name="foo",
* column=#Column(
* name = "foo_overridden",
* type = "integer",
* length = 140,
* nullable = false,
* unique = false
* )
* )
* })
*
* #AssociationOverrides({
* #AssociationOverride(name="bar",
* joinColumns=#JoinColumn(
* name="example_entity_overridden_bar_id", referencedColumnName="id"
* )
* )
* })
*/
class ExampleEntityWithOverride
{
use ExampleTrait;
}
/**
* #Entity
*/
class Bar
{
/** #Id #Column(type="string") */
private $id;
}
The docblock is showing metadata override of the attribute and association type. It basically changes the names of the columns mapped for a property foo and for the association bar which relates to Bar class shown above. Here is the trait which has mapping metadata that is overridden by the annotation above:
<?php
/**
* Trait class
*/
trait ExampleTrait
{
/** #Id #Column(type="string") */
private $id;
/**
* #Column(name="trait_foo", type="integer", length=100, nullable=true, unique=true)
*/
protected $foo;
/**
* #OneToOne(targetEntity="Bar", cascade={"persist", "merge"})
* #JoinColumn(name="example_trait_bar_id", referencedColumnName="id")
*/
protected $bar;
}
The case for just extending a class would be just the same but:
<?php
class ExampleEntityWithOverride extends BaseEntityWithSomeMapping
{
// ...
}
Overriding is also supported via XML and YAML (examples).

Doctrine: How to create entity-related tables on demand? (if i want to keep one SQL schema source)

Is there a way to tell Doctrine the name of a number of entities and it creates their related tables (incl. foreign keys etc.)?
My scenario:
I want to have annotations at my Doctrine entities as the only source for my database schema. Which means, that for instance for tests, i don't want to maintain a copy of these information in a SQL file or something.
To be clear, i mean annotations in entity classes like the following:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"email"}, message="There is already an account with this email")
*
* #ORM\Table(
* uniqueConstraints={
* #ORM\UniqueConstraint(name="email", columns={"email"})
* }
* )
*/
class User
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, nullable=false)
*/
private $email;
// ...
}
What i would like to do:
In my tests i would like to create the table for, lets say User, like:
<?php
namespace App\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class SomeTestCase extends KernelTestCase
{
public function setUp()
{
// ...
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function test1()
{
// Is there a function available which has this functionality?
$this->entityManager->createTableForEntity('App\Entity\User'); // <---------
// ...
}
}
Is that possible? If not, even creating all tables at once is fine for me.
Is there another way to achieve it?
I use the following to create all the tables in my tests:
use Doctrine\ORM\Tools\SchemaTool;
$metadatas = $this->entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadatas);
There is a method getMetadataFactory() on the MetadataFactory class so I guess the following should work as well if you want to create just one table.
$metadata = $this->entityManager->getMetadataFactory()->getMetadataFor('App\Entity\User');
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadata);

Doctrine 2 OneToOne, load owning side only when neccessary

I have two entities - User and UserSettings. In User entity, I want to have UserSettings as an attribute. That would be OK, I would add a OneToOne relation but there's a problem - because UserSettings is an owning side of the relation, every time I load User entity, Doctrine has to load the UserSettings entity too.
Is there a way how to load User but not UserSettings?
I made maybe a weird solution - there's no relation between these entities and the settings are loaded by method of Facade. For example:
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $name;
/** #var UserSettings */
private $settings;
public function __construct()
{
$this->settings = new UserSettings();
}
public function setSettings(UserSettings $settings)
{
$this->settings = $settings;
}
}
/**
* #ORM\Entity
*/
class UserSettings
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(name="user_id", type="integer")
*/
private $userId;
}
class UserFacade
{
/**
* #var EntityManager
*/
private $em; // is injected automatically by DI
public function loadSettings(User $user)
{
$settings = $this->em->getRepository("UserSettings")->findOneBy(array("userId" => $user->id));
$user->setSettings($settings);
}
}
$user = $em->find("User", 1);
// if I want user's settings
$userFacade->loadSettings($user); // now I can use $user->getSettings()->something;
Side note: UserFacade is a service class that manipulates with users' data like adding new user, editing, deleting etc. In my MVC application, controller classes communicate with Facades, not with EntityManager directly.
That's OK - settings are loaded only when I want to. However, there are two possible problems:
a) I don't think this is a clear way
b) When I want a list of users, I cannot JOIN a table where settings are, because entities are not associated, so I have to make an extra SQL for each user.
My question is - how to solve the problem with OneToOne relation? I don't have much experience with Doctrine, so it may be a stupid question - sorry for that.
Thanks!

Multiple level of different kind of inheritance

For my project, I'm trying to use the inheritance feature of Doctrine. I need to represent medias (through different tables : one table for uploaded documents, one for linked videos, ... and so on).
But, the videos can vary from provider to provider (such as Youtube, Dailymotion, you name it). So, I was thinking of doing another inheritance, proper to the Video table, through a SINGLE_TABLE inheritance.
But, when I declare my entities, it seems that if I add the SINGLE_TABLE inheritance annotation on the AbstractVideo entity, which extends the AbstractMedia Entity, the Video table is never created (nor detected). Here is a snippet of these two entities :
<?php
namespace Acme\Demo\Entity;
use Datetime;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Media")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
*/
abstract class AbstractMedia
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// some other fields
}
/**
* #ORM\Entity
* #ORM\Table(name="Video")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="provider", type="string")
* #ORM\DiscriminatorMap({})
*/
abstract class AbstractVideo extends AbstractMedia
{
/** #ORM\Column(type="string") */
private $name;
// some other fields
}
I already tried to have a mapped entity to a Foo entity, extending the AbstractVideo, but then when I try to persist something, it says that it is not a valid entity.
Any ideas, or should I really avoid such deep inheritance ? Thanks
Not really sure if this is exactly what you need, but this is from a production code I use.
We inherit the file, with other entities, and those are also inherited.
The important part is to add the inheriting(extending) entities to disciminator map.
/**
* File
*
* #ORM\Table(name = "file")
* #ORM\Entity(repositoryClass="Living\ApiBundle\Entity\File\FileRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string", length=64)
* #ORM\DiscriminatorMap({
* "file" = "Something\Entity\File\File",
* "image" = "Something\Entity\Image\Image",
* "specialImage" = "Something\Entity\Image\SpecialImage",
* })
*/
class File implements FileEntityInterface
.....
/**
* ImageFile
*
* #ORM\Table(name="image")
* #ORM\Entity(repositoryClass="Living\ApiBundle\Entity\Image\ImageRepository")
*/
class Image extends File implements ImageEntityInterface
As #OCramius said in a comment to my question, this is not supported by Doctrine ORM. So to do what I wanted to do, I will store a value object in the data property of my object, storing the property of "child classes" instead of having deep different kind of inheritance.
<?php
class Video extends AbstractMedia
{
// returns the value object youtube, dailymotion, ... etc
public function getData();
}
class Youtube
{
//public function ...
}
class Dailymotion
{
// public funciton ...
}

Doctrine 2 association overwrite

I have a question about Doctrine 2 and the ability (or not?) to extend an association between to classes.
Best explained with an example :)
Let's say I have this model (code is not complete):
/**
* #Entity
*/
class User {
/**
* #ManyToMany(targetEntity="Group")
* #var Group[]
*/
protected $groups;
}
/**
* #Entity
*/
class Group {
/**
* #ManyToMany(targetEntity="Role")
* #var Role[]
*/
protected $roles;
}
/**
* #Entity
*/
class Role {
/**
* #ManyToOne(targetEntity="RoleType")
* #var RoleType
*/
protected $type;
}
/**
* #Entity
*/
class RoleType {
public function setCustomDatas(array $params) {
// do some stuff. Default to nothing
}
}
Now I use this model in some projects. Suddenly, in a new project, I need to have a RoleType slightly different, with some other fields in DB and other methods. Of course, it was totally unexpected.
What I do in the "view-controller-but-not-model" code is using services:
// basic configuration
$services['RoleType'] = function() {
return new RoleType();
};
// and then in the script that create a role
$role_type = $services['RoleType'];
$role_type->setCustomDatas($some_params);
During application initialization, I simply add this line to overwrite the default RoleType
$services['RoleType'] = function() {
return new GreatRoleType();
};
Ok, great! I can customize the RoleType call and then load some custom classes that do custom things.
But... now I have my model. The model says that a Role targets a RoleType. And this is hard-written. Right now, to have my custom changes working, I need to extend the Role class this way:
/**
* #Entity
*/
class GreatRole extends Role {
/**
* Changing the targetEntity to load my custom type for the role
* #ManyToOne(targetEntity="GreatRoleType")
* #var RoleType
*/
protected $type;
}
But then, I need to extend the Group class to target GreatRole instead of Role.
And in the end, I need to extend User to target GreatGroup (which targets GreatRole, which targets GreatRoleType).
Is there a way to avoid this cascade of extends? Or is there a best practice out there that is totally different from what I did?
Do I need to use MappedSuperClasses? The doc isn't very explicit...
Thanks for your help!
--------------------------- EDIT ---------------------------
If I try to fetch all the hierarchy from User, that's when I encounter problems:
$query
->from('User', 'u')
->leftJoin('u.groups', 'g')
->leftJoin('g.roles', 'r')
->leftJoin('r.type', 't');
If I want to have a "r.type" = GreatRoleType, I need to redefine each classes.

Categories