I am using Symfony 4 and with Doctrine where I have entities which have the same common attributes such as createdWhen, editedWhen, ...
What i would like to do is this:
Defining a kind of base entity that holds these common attributes and implements the setter and getter. And many entities which inherit from that base entity. The database fields should all be defined in the table of the respective sub entity (no super table or the like should be created in the db).
Example:
/**
* #ORM\Entity(repositoryClass="App\Repository\BaseRepository")
*/
class Base
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=127, nullable=true)
*/
private $createdWhen;
// Getter and setter
...
}
/**
* #ORM\Entity(repositoryClass="App\Repository\PersonRepository")
*/
class Person extends Base
{
/**
* #ORM\Column(type="string", length=127, nullable=true)
*/
private $name;
// Getter and setter
...
}
/**
* #ORM\Entity(repositoryClass="App\Repository\CarRepository")
*/
class Car extends Base
{
/**
* #ORM\Column(type="string", length=127, nullable=true)
*/
private $brand;
// Setter and getter
...
}
This should create the tables "person" and "car" (each with id, created_when) but no table base.
I would still like to be able to use the bin/console make:migration for updating the database schema.
Is this kind of approach possible with Symfony 4? If yes how would I define the entities and what do I have to change in terms of configuration, etc.?
You are looking for entity inheritance
Rewrite your code like so
/** #MappedSuperclass */
class Base
{
...
}
In fact, this is a part of Doctrine, here is what an official documentation says
A mapped superclass is an abstract or concrete class that provides
persistent entity state and mapping information for its subclasses,
but which is not itself an entity. Typically, the purpose of such a
mapped superclass is to define state and mapping information that is
common to multiple entity classes.
Related
I have an entity User with lots of feature built for it.
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
}
This entity is good as is for most of my User.
However, a few of them will have a special ROLE, ROLE_TEACHER.
With this role, I need to store a lot of new variables specially for them.
If I create a new entity Teacher, doctrine creates a new table with every User's data + the Teacher's data.
/**
* #ORM\Entity(repositoryClass="App\Repository\TeacherRepository")
* #Table(name="teachers")
*/
class Teacher extends User
{
/**
* #ORM\Column(type="string", length=64, nullable=true)
*/
protected $test;
public function __construct()
{
parent::__construct();
}
}
What I want, is for Teacher & User to share the users table and have the teachers table only store the extra data. How could I achieve that ?
This is more of system design problem than implementation problem. as #Gary suggested you can make use of Inheritance Mapping which can have Performance issues, I'd rather suggest re think your schema and make use of database normalization techniques to break up your data into more manageable entities.
You can have User entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
/**
* One user has many attibute data. This is the inverse side.
* #OneToMany(targetEntity="UserData", mappedBy="data")
*/
private $data;
}
With other UserData Entity with OneToMany relationship :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserDataRepository")
* #Table(name="user_data")
*/
class UserData
{
/* variables + getter & setter */
#ORM\Id()
private $id;
/**
* Many features have one product. This is the owning side.
* #ManyToOne(targetEntity="User", inversedBy="data")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #ORM\Column(type="string")
*/
private $attribute;
/*
* #ORM\Column(name="value", type="object")
*/
private $value;
}
Now you can have list of user attributes without requiring specific structure to each role. It's scalable and arbitrary.
You can also define same Relation with TeacherData, StudentData or UserProfile Entities with foreign keys and branch your application logic according to the roles. Key is to break data into their separate domains and keep common data in one table. Load related data by querying related entity, this increases readability and makes it easy to break complex structure into manageable codebase.
I get unnecessary queries then entity has ManyToOne relationship with abstract class.
My classes structure:
/**
* #ORM\Entity
* #ORM\Table(name="tb_payment_info")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\DiscriminatorMap({
* "0" = "PaymentInfoPaypal",
* "1" = "PaymentInfoSkrill",
* })
*/
abstract class AbstractPaymentInfo
{
/**
* #ORM\Column(name="payment_info_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
}
/**
* #ORM\Entity
* #ORM\Table(name="tb_payment_info_paypal")
*/
class PaymentInfoPaypal extends AbstractPaymentInfo
{
}
/**
* #ORM\Entity
* #ORM\Table(name="tb_payment_info_skrill")
*/
class PaymentInfoSkrill extends AbstractPaymentInfo
{
}
My Payout class contains payment_info_id column from tb_payment_info table.
/**
* #ORM\Entity
* #ORM\Table(name="tb_payout")
*/
class Payout
{
/**
* #var AbstractPaymentInfo
*
* #ORM\ManyToOne(targetEntity="AbstractPaymentInfo")
* #ORM\JoinColumn(name="payment_info_id", referencedColumnName="payment_info_id")
*/
private $paymentInfo;
}
When I try to get any Payout entity, its paymentInfo initialize automatically. So:
$this->getEntityManager()->getRepository('TuoPayBundle:Payout')->find(255);
got 2 queries: first for Payout and second for its paymentInfo
$this->getEntityManager()->getRepository('TuoPayBundle:Payout')->findBy(['id'=>[255,256]]);
got 3 queries: first for Payout and second, third separate queries to init paymentInfo
How to achieve lazy load?
You cannot declare an abstract class in Doctrine 2 with #ORM\Entity notation. If you want to use abstract classes in your object model I suggest you check the documentation on Mapped Superclasses on how to do that correctly.
Most importantly you should declare the class with a special #ORM\MappedSuperClass annotation.
Keep in mind that Mapped superclasses come with restrictions. I quote:
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.
I have many ads entities (MotorAds, RealestateAds, ElectronicsAds, ...) that share some attributes like title and description. In order to avoid redefining these attributes for each Ads entity, one can use the mapped superclass methods as follows:
<?php
/** #MappedSuperclass */
class MappedSuperclassAds{
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=false)
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=false)
*/
private $description;
}
Then, the inheritance will do the job.
Now, what is the problem? The problem is that each Ads entity is related to its entity that defines the list of users that added the ads to their favorites. To do that (the MotorsAds entity for example),
1.linking the MotorsAds entity to its MotorsFavorite entity through that code:
/**
* #ORM\OneToMany(targetEntity="Minn\AdsBundle\Entity\MotorsFavorite",
* mappedBy="motors",cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=true)
*/
private $favorites;
2.Defining the MotorsFavorite entity as fellows:
<?php
namespace Minn\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* MotorsFavorite
*
* #ORM\Table(
* uniqueConstraints={#ORM\UniqueConstraint(name="unique_fav_motors",
* columns={"user_id", "motors_id"})})
* #ORM\Entity(repositoryClass="Minn\AdsBundle\Entity\MotorsFavoriteRepository")
* #ORM\HasLifecycleCallbacks()
*/
class MotorsFavorite {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Minn\UserBundle\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity="Minn\AdsBundle\Entity\MotorsAds", inversedBy="favorites")
* #ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $motors;
//...
}
As you can see, the linkage between the MotorAds and MotorFavorite is a hard linkage, which means that I have to create a Favorite entity for each Ads entity I create (FavoriteMotors, FavoriteRealestate, FavoriteElectronics, ...). This is a long and repetitive work.
So my question is:
1.Creating a super mapped class called SuperMappedFavorite which will only include the $id and $user attributes will reduce the repetitive work. But what about the the attribute $motors? $motors is hardly linked to the entity MotorsAds as you see here:#ORM\ManyToOne(targetEntity="Minn\AdsBundle\Entity\MotorsAds", inversedBy="favorites"). All the burden of the work is in the setters and getters of $motors.
2.Is it possible to make the target entity an interface like this:
<?php
// SuperMappedFavorite.php
// ...
#ORM\ManyToOne(targetEntity="Minn\AdsBundle\Favorite\FavoriteAwareInterface", inversedBy="favorites")
private $object;
// ...
and the MotorsAds entity will be implementing in this the FavoriteAwareInterface
If anyone has a good link/article regarding this kind of issue, I will be happy to have it.
Thanks.
Yes, you can set an interface as target entity, as described in the Symfony documentation.
The process is basically:
defining the interface (your Minn\AdsBundle\Favorite\FavoriteAwareInterface),
setting the interface in the parent entity (as you already did),
implementing the interface in a different entity (would be class MotorsFavorite implements FavoriteAwareInterface) – and yes, it can also be derived from a mapped superclass,
and then telling Doctrine to use your implementation through the doctrine.orm.resolve_target_entities config parameter.
See the documentation for details and a code example.
What is the best way to handle a File entity where you have multiple ManyToOne relationships.
Let's say I have 5 entities that has a OneToMany relationship with the File entity.
File.php
/**
* #ORM\ManyToOne(targetEntity="Entity1", inversedBy="files")
* #ORM\JoinColumn(name="entity1_id", referencedColumnName="id", nullable=true, onDelete="CASCADE")
*/
private $entity1;
/**
* #ORM\ManyToOne(targetEntity="Entity2", inversedBy="files")
* #ORM\JoinColumn(name="entity2_id", referencedColumnName="id", nullable=true, onDelete="CASCADE")
*/
private $entity2;
and so one....
Entity1.php
/**
* #ORM\OneToMany(targetEntity="File", mappedBy="entity1" , cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $images;
The great thing about the above is the getter and setter are set, I can persist and save to the database automatically. The relationship is set and I can load the files by just calling $entity1->getFiles().
What I don't like is every time I want to add another entity that has a OneToMany with File it creates a new column in the database so potential I could have 10 columns referencing Ids from other entities.
What I would like to achieve is saving the class of the entity in the class field and saving the id of the record in an id field but also somehow still allowing the persist and collection saving to work.
entity_id | class
------------------------------------------
2 | ProjectBundle/Entity/Entity1
3 | ProjectBundle/Entity/Entity2
You don't need the class field at all.
Use Doctrine's inheritance mapping by creating a base class for all entities you want to refer from File:
/**
* #ORM\Entity()
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="entityType", type="string")
* #ORM\DiscriminatorMap({
* "entity1" = "Entity1",
* "entity2" = "Entity2"
* })
*/
abstract class BaseEntity
{
/**
* #ORM\ManyToMany(targetEntity="File", mappedBy="entities" , cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $images;
}
/**
* #ORM\Entity
*/
class Entity1 extends BaseEntity
{
...
}
/**
* #ORM\Entity
*/
class Entity2 extends BaseEntity
{
...
}
This way you can refer to both Entity1 and Entity2 by their base class from File. When calling getEntities, Doctrine creates instances of the proper class "automatically", based on the discriminator value of each entity.
File
/**
* #ORM\ManyToMany(targetEntity="Entity", inversedBy="images")
* #ORM\JoinColumn(name="entity_id", referencedColumnName="id", nullable=true, onDelete="CASCADE")
*/
protected $entities;
OneToMany, ManyToOne become ManyToMany because now the file may have many entities.
I have a User Entity. This is considered the primary entity in this case and the mere fact it is being used means it is present.
The User entity, has a Store entity. But not all Users will necessarily have a Store entity.
It is worth noting that this is an existing database we are working with, and the id for the User table is the same as the id for the Store table. Name (id) and Value. It's just that in some cases, Store does not have a record for a given User id.
User:
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $store;
...
}
Store:
class Store extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
...
}
This causes problems in the controllers. If a User entity does not have a Store record, it fails with a "Entity not found" exception. This can be dealt with using a try catch easy enough (I haven't been able to find a way to check if an Entity object exists or is just a proxy). If the User does have a store record, all is fine here.
But the big issue I have is especially the Fixtures:
protected function createUser($id)
{
$user = new User();
$user->setId($id);
$user->setEmail($id.'#example.com');
$user->setUserName($id.'_name');
$user->setArea($this->manager->find('Area', 156)); // Global
$this->manager->persist($user);
return $user;
}
When I run Fixtures, this fails. Giving me the error "Integrity constraint violation: 1048 Column 'id' cannot be null". This message disappears if I remove the store entity from User. So in a nutshell, I cannot add a user if it doesn't have a store.
Anyone know what's happening? I've done some looking around and I can't find anything, including doctrine docs, on having optional relationships between Entities. Which I thought would have been a common situation.
Found the solution to this on this doc page:
http://docs.doctrine-project.org/en/latest/tutorials/composite-primary-keys.html#use-case-3-join-table-with-metadata
In my case, rather than the User entity being associated with the Store entity using the id field, the store property in the User entity would be associated to the Store entity by user (an entity object). In return, the Store object will hold a User entity, which is annotated as the entity's id.
I'm sure that's as confusing as hell, so just look at the sample above. Below are my adjusted Entity classes:
User
class User extends Entity
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Store", mappedBy="user")
*/
protected $store;
...
}
Store
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}
Now, if there is no Store record present for a given User, the store property in the User entity will be null. Fixtures runs as expected too.
In addition to the answer above, I also needed to add an inversedBy attribute. Otherwise, an invalid Entity mapping error will be thrown.
Using the entities above, the Store object would need to look like this:
class Store extends Entity
{
/**
* #ORM\Column(type="string", length=36)
*/
protected $id;
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User", inversedBy="store")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $user;
...
}