Is it possible (and recommended) to use multiple models for one table?
I have multiple types of "people" (teacher, students, employees, firms) and would like to store them in the same table. They have all personal data in common, but have other relations (for ex. students -> firms, teacher -> rooms and more), additional information (for ex. firms: firm name, teachers: education) and more.
It would be nice to get some best practices for such cases.
It's really simple! You have to make entities like this:
/**
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"teacher" = "Teacher"})
*/
class Person
{
/**
* #ORM/Id
**/
private $id;
/**
* #ORM/Column
**/
private $name;
//setters getters and all the stuff
}
And in Teacher.php file:
/**
* #ORM\Table()
* #ORM\Entity()
*/
class Teacher extends Person
{
private $salary;
}
The key to success are those two annotation:
#ORM\DiscriminatorColumn(name="discr", type="string")
#ORM\DiscriminatorMap({"teacher" = "Teacher"})
First is telling ORM what column will be used to verify if person is Teacher or not.
Second is telling what classes are extending base Person class and what to put in column i mentioned before. When you do that you will have two tables, but in Teacher you will have only data you add to Teacher entity :)
Generally speaking when using ORM you have to think on the object abstraction level and don't care about DB (well, that's not entirely true, but it's general idea) :)
It's not recommended. Here are several reasons I can think of why not to do this:
If Teachers and Students share the same set of IDs, what is to stop someone from loading a Teacher using a Student's ID, and adding a value to the "salary" field? Limiting this access can be done, but it may require custom repositories or other modifications.
Let's say that Teachers are required to have a salary. If Students and Teachers share the same table, then the database won't be able to enforce that constraint without giving Students a salary too.
Even though Students will never have a salary, the database may need to allocate space for that field anyway.
IBM has a great post about mapping inheritance in relational databases, and I've referred to it several times when implementing this type of model. They make reference to three methods: (1) using a single table to represent all classes (the method you've proposed), (2) using separate tables for children classes, or (3) using separate tables for all classes. In the end, it comes down to personal preference, but the way I normally do it is a blend between #2 and #3, where I create the model for all classes, but I limit the access to the parent class from the child, and instead write shortcut methods to access the parent data. Consider this:
class Person
{
private $id;
private $name;
public function getId()
{ return $this->id; }
public function getName()
{ return $this->name; }
public function setName($name)
{ return $this->name; }
}
class Teacher
{
private $id;
private $person; // reference to Person table
private $salary;
public function getId()
{ return $this->id; }
private function getPerson()
{ return $this->person; }
public function getSalary()
{ return $this->salary; }
public function setSalary($salary)
{ $this->salary = $salary; }
public function getName()
{ return $this->getPerson()->getName(); }
public function setName($name)
{ $this->getPerson()->setName($name); }
}
I usually choose this method because I can treat a Teacher as simply a Teacher, or as a Person, depending on what the situation requires. So let's say that I have two pages: one that prints out everyone's name (Teachers and Students), and one that prints out just the Teachers and their salaries:
// Assume:
// $people = $this->getDoctrine()->getEntityManager()->getRepository("MyBundle:Person")->findAll()
{% for person in people %}
{{ person.name }}
{% endfor %}
// Assume:
// $teachers = $this->getDoctrine()->getEntityManager()->getRepository("MyBundle:Teacher")->findAll()
{% for teacher in teachers %}
{{ teacher.name }} makes ${{ teacher.salary }}
{% endfor %}
Related
I am building an API with Symfony and JMS Serializer (via FOSRestundle) that exposes trees. I've created an Tree entity that contains an id, a title and the root node of the tree. I've created a Node entity too, containg the chaining between nodes.
My API contains a public part and an admin part, and I want trees to be exposed differently according if the controller belongs to one or the other:
in the public api, I want the id and the title of my tree only to be visible
in the admin api, I want all the properties of the tree to be visible, including the root node.
I've come to the following code:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
/**
* #ORM\Entity(repositoryClass="App\Repository\TreeRepository")
*/
class Tree {
/**
* Unique ID to identify the tree
*
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Serializer\Groups({"ADMIN", "API"})
*/
private $id;
/**
* Name of the tree (example = a failure to diagnose)
*
* #ORM\Column(type="string", length=255)
* #Serializer\Groups({"ADMIN", "API"})
*/
private $title;
/**
* #ORM\OneToOne(targetEntity="App\Entity\Node")
* #ORM\JoinColumn(referencedColumnName="id")
*
* #Serializer\Groups({"ADMIN"})
*/
private $firstNode;
public function getId()
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getFirstNode(): ?Node
{
return $this->firstNode;
}
public function setFirstNode(?Node $firstNode): self
{
$this->firstNode = $firstNode;
return $this;
}
}
As you see, I've created two exclusion groups so that I can hide or expose the properties I want. That works great!
But for the properties inside the node to be visible, I have to add the #Serializer\Groups annotations for all the properties, and propagate it to the classes of the properties all down along the dependencies.
I would like not to have to copy the #Serializer\Groups annotations in all of my entity classes. So I tried with JMS Exclusion policies (#Serializer\ExclusionPolicy()), but this does not seem to work.
Is there a way to expose/exclude a class, inconditionnaly of the current JMS exclusion group ?
Thanks.
After 24 hours, I realised that I misused the concept of exclusion groups for jms serializer.
Before: I had 2 groups : "API" and "ADMIN". With this organisation, I needed to declare every property in the groups, and this, all along my dependency tree (like for a white list).
Now: I only have one group "ADMIN". With this organisation, I need:
to declare in the group "ADMIN", only the properties that are not publicly visible
to declare my controllers for the group "Default" for the public requests, and for the groups "ADMIN" and "Defaults" for the requests that are admin-only. I don't need to propagate any exclusion groups down the dependency tree.
This resolves my problem nicely. But maybe my initial demand is still legit.
Im wondering where should I store and how to retrieve a tax rate that Im going to use in multiple classes. I would like to set this value as a parameter just one time. Is parameters.yml a good place to set it?
/**
* #ORM\Column(type="string")
*/
protected $taxRate;
I will have the code above in multiple classes as I said.
Bear in mind that I want to use also the value on templates.
Well if you just want to set the value at a single point then the easiest solution would be using a trait.
The benefit is that you can use multiple traits which feels like multiple inheritance instead of a big basic class that is unflexible and does not care about the single responsibility principle.
The trait might look like this:
namespace Your\Namespace;
use Doctrine\ORM;
trait TaxRate {
/**
* #ORM\Column(type="string")
*/
protected $taxRate = 'your_default_value';
// set get methods here
}
You can then use it multiple times:
namespace Your\Namespace;
use Your\Namespace\TaxRate;
class Entity {
use TaxRate;
// more methods here
}
Adding parameters from parameters.yml to an entity is quite a mess in Symfony. An idea is to use the entity as a service which is not working when using Doctrine's find(). So I don't recommend this.
This code looks like it belongs to an Entity. You can share this parameter by creating an abstract class and make all the entities that need and have this property to extend form it:
abstract class TaxableEntity {
/**
* #ORM\Column(type="string")
*/
protected $taxRate;
}
class Entity extends TaxableEntity {
//here you have access to taxRate
}
This approach has its drawbacks though, which are the usual drawbacks of inheritance. Once you get many consumers of this API (protected) you'll have a very hard time re-factoring, plus you're enforcing all your Taxable entities to have that field mapped into the DB.
Another approach, would be to wrap the concept of a TaxRate into a ValueObject. This way you'd be able to use it more as a regular attribute but with a stronger type:
class TaxRate {
private function __construct($rate) {
$this->rate = $rate;
}
public static function fromFloat($value) {
if(!is_float($value)) {
throw new \InvalidArgumentException();
}
return self($value);
}
public static function fromInteger($value) {
if(!is_int($value)) {
throw new \InvalidArgumentException();
}
return self((float)$value);
}
}
Anyway, the "parameters.yml" has nothing to do with your object's properties, but more about your project's property. For instance, the necessary parameters to connect to your DB would be well located in the parameters.yml:
database_host: localhost
database_port: 3306
database_user: user
database_password: password
database_name: db_name
This way they can be injected/used in your services by means of the DIC.
I think I should consider to define it as a constant: http://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options
An option where you could handle varying tax rates for when they are changed would be to store your tax rates in a separate table and reference that from your entities, possibly using a trait for ease of use and DRY code.
I will be using annotations as it seems that's what you are using.
AppBundle\Entity\TaxRate
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="app_tax_rate")
* #ORM\Entity(repositoryClass="AppBundle\Entity\TaxRateRepository")
*/
class TaxRate
{
/**
* #var int
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var float
*
* #ORM\Column(type="decimal", scale=2, column="tax_rate")
*/
private $rate;
/**
* #var \DateTime
*
* #ORM\Column(type="datetime", column="active_from")
*/
private $activeFrom;
.. getter & setters
}
In this you would store the tax rate and the date the rate became/becomes active. If, for example, you knew that in 3 months the rate was going to change you could add that to your database in preparation but it wouldn't take effect until that date. You would also be able to set an entities tax rates from the past or the future, should the need arise.
AppBundle\Entity\TaxRateRepository
use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class TaxRateRepository extends EntityRepository
{
/**
* Get current tax rate if available
*
* #return TaxRate
* #throws NotFoundHttpException
*/
public function getCurrentTaxRateForDate(\DateTime $date)
{
return $this->getTaxRateForDate(new \DateTime());
}
/**
* Get tax rate for a specified date if available
*
* #param \DateTime $date
* #return TaxRate
* #throws NotFoundHttpException
*/
public function getTaxRateForDate(\DateTime $date)
{
$queryBuilder = $this->createQueryBuilder('r');
$rate = $queryBuilder
->where($queryBuilder->expr()->lte('r.activeFrom', ':date'))
->setParameter('date', $date)
->orderBy('r.activeFrom', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult();
if (null === $rate) {
throw new NotFoundHttpException(sprintf(
'No tax rate available for "%s"',
$date->format('Y-m-d')
));
}
return $rate;
}
}
This "getTaxRateForDate" method will find all tax rates that became active on or before today and order them by the date they became active, in reverse, then return the first in the list.. so the last tax rate to become active before the given date. The "getCurrentTaxRate" will do the above but automatically set the date to "today".
You can then create a trait that you can drop into any entity that uses a tax rate, with the trait containing the necessary annotations. The association will be unidirectional meaning you would be able to get the tax rate from the order/invoice/etc but not the other way around.
AppBundle\Entity\Traits\TaxableTrait
use Doctrine\ORM\Mapping as ORM;
trait TaxableTrait
{
/**
* #ORM\ManyToOne(targetEntity="TaxRate")
* #ORM\JoinColumn(name="tax_rate_id", referencedColumnName="id")
*/
private $taxRate;
.. getters & setters
}
Then in each of your entities you can just add the trait which will add the getter, setter and annotation for the field.
use AppBundle\Entity\Traits\TaxableTrait;
class Order
{
use TaxableTrait;
// the rest of your entity
}
To use this you would then do something like..
$order = new Order();
// This, as stated in your custom repository, would cause an exception
// to be thrown if no tax rate was available for "now"
$order->setTaxRate($taxRateRepository->getCurrentTaxRate());
.. set the rest of the order details
$order->getTaxRate(); // would get the tax rate object
$order->getTaxRate()->getRate(); // would get the actual rate
{{ order.taxRate.rate }} // would get the rate in a twig template
Create service that returns taxrate from database .
For Performance use Doctrine Second Level Cache ( there will be no difference if you access tax one or hundred times - after first time value would be store in SLC - till end of request )
Lets say, I have a class Movie with a Orm\OneToMany relation ship to the class Actors.
I have already a working example of an getter for $movie->getActors(); which will return all actors of that movie.
But how to dynamically modify the query for that? For example, I show a list of all actors of the movie, and allow the user to sort by name, age, gender, whatever.
===== EDIT ======
After learning, that such things belongs to the repository class (thanks to Yoshi, scoolnico), here is the adapted question:
Lets say, I have got a Movie ID 4711. I will fetch the movie:
$movie = $this->getDoctrine()
->getRepository("Movie")
->find(4711);
And now, I need to get all Actors from this movie sorted by name (as an example).
$actorsOfMovie = $this->getDoctrine()
->getRepository("Actor")
->findBy(array("movie_id" => 4711), array('name'=>'asc'));
Is this really the correct way?
With this version, I need to know in the controller, how the relationship between movie and actors work! Thats a thing, doctrine should handle for me!
And how to use it with multiple movies?
// Controller
$movies = $this->getDoctrine()
->getRepository("Movie")
->findfindBy(array());
return $this->render("xyz.html.twig", array("movies": $movies));
// Twig: xyz.html.twig
{% for movie in movies %}
<h1>{% movie.getName() %}</h1>
<p>Actors: {% for actor in movie.getActorsOrderByName() %}{{ actor.getName() }},{% endfor %}
{% endfor %}
You just have to create a specific function in your class Repository:
class MovieRepository extends EntityRepository
{
public function getActoryByGender($gender)
{
/.../
}
}
And in your controller:
/.../
$em = $this->getDoctrine()>getManager();
$repository = $em->getRepository('YourBundle:Movie');
$actors = $repository->getActorByGender('male');
/.../
I think the best solution for this is to use doctrine's Criteria class (http://docs.doctrine-project.org/en/latest/reference/working-with-associations.html#filtering-collections).
Have a look at https://www.boxuk.com/insight/blog-posts/filtering-associations-with-doctrine-2
Based on this, I can do the following:
// In the Movie Class
/**
* Get actors
*
* #param array $options
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getActors($options = array())
{
if(!$options) {
return $this->actors;
}
$criteria = Criteria::create();
if(isset($options["order_by"])) {
$criteria->orderBy($options["order_by"]);
}
if(isset($options["limit"])) {
$criteria->setMaxResults($options["limit"]);
}
if(isset($options["offset"])) {
$criteria->setFirstResult($options["offset"]);
}
// Or I can define other filters or sorting stuff
if(..) {
...
}
return $this->actors->matching($criteria);
}
I have two Entities:
User and
Comment
Every comment can only have one user but a user can have multiple comments.
I'm not sure if I want to use biderectional, Unidirectional with Join Table, or Self-referencing.
I only want this relationship to apply when calling the comment object. If I call a user object somewhere I do not want a bunch of comment objects flooding the user object. Which approach should I take?
Symfony's documentation explains the process pretty well : http://symfony.com/doc/current/book/doctrine.html#fetching-related-objects
What's important is the fact that you have easy access to the
product's related category, but the category data isn't actually
retrieved until you ask for the category (i.e. it's "lazily loaded").
Just make a ManyToOne relationship, and only fetch a user's comments when you need to.
User entity:
<?php
class User
{
// ...
/**
* #ORM\OneToMany(targetEntity="Comment", mappedBy="author")
*/
protected $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
}
Comment entity:
<?php
class Comment
{
// ...
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="comments")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $author;
}
Let say I have a Company for which I manage Employees, Cars, Contracts, Buildings, Sites, Products, etc. As you can guess, these are quite independant things, so no inheritance is possible.
For each of these elements (i.e. Entities), I want to be able to attach one or several Documents (click on a button, form opens, select one/several Document or upload a new one).
Linking Document to one kind of entity is not a problem, my problem is that there are many kinds of entities. How should I manage that? I have 2 ideas which have their own problems...:
Create a ManyToMany relationship between Document and Employee, another between Document and Car, etc.
Problem: I have to duplicate the Controller code to attach Document, duplicate the forms, etc.
Create a single join table containing the Document's ID, the related entity's ID and the related entity's class name.
Problem: it doesn't look really clean to me, I didn't really dig in this way but I feel I'll have a lot of "entity mapping" problems.
Any suggestion?
[EDIT]
In fact I have to do the same for Event as well: I need to link some Events to some Employees and/or to some Cars, etc. And in my real case, I have more than 10 Entities to be linked to Event and/or Document, which means duplicating more tha 20 times the code if I go with the solution 1!
Assuming you're using Doctrine ORM, i think you're searching for the Mapped Superclasses inheritance.
The docs are better than words :
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
So finally I managed to solve my problem, following #Rpg600 idea about Mapped Superclasses.
This is probably not the best and cleanest solution ever, I'm not really proud of it but it does the job and it is still better than my first ideas.
I create a BaseEntity which is my a mapped superclass (Employee, Car, etc. Entities have to extend this Class):
/**
* BaseEntity
* #ORM\MappedSuperclass
*/
class BaseEntity {
/**
* #ORM\OneToOne(targetEntity="MyProject\MediaBundle\Entity\Folder")
*/
private $folder;
/**
* Set folder
* #param \Webobs\MediaBundle\Entity\Folder $folder
* #return BaseEntity
*/
public function setFolder(\Webobs\MediaBundle\Entity\Folder $folder = null){
$this->folder = $folder;
return $this;
}
/**
* Get folder
* #return \Webobs\MediaBundle\Entity\Folder
*/
public function getFolder(){
return $this->folder;
}
}
As it is not possible to have a Many-to-Many relationship in a superclass, I use a Folder which will contain one or several Document. This is the dirty part of the solution ; the folder table basically contain only one field which is the id...
class Folder
{
private $id;
/**
* Note : Proprietary side
* #ORM\ManyToMany(targetEntity="MyProject\MediaBundle\Entity\Document", inversedBy="folders", cascade={"persist"})
* #ORM\JoinTable(name="document_in_folder")
*/
private $documents;
// setters and getters
Then I create a helper class (which is declared as a service) to manage the link between any Entity and the Document:
class DocumentHelper extends Controller
{
protected $container;
/** ************************************************************************
* Constructor
* #param type $container
**************************************************************************/
public function __construct($container = null)
{
$this->container = $container;
}
/** ************************************************************************
* Attach Document(s) to an $entity according to the information given in the
* form.
* #param Entity $entity
* #param string $redirectRouteName Name of the route for the redirection after successfull atachment
* #param string $redirectParameters Parameters for the redirect route
* #return Response
**************************************************************************/
public function attachToEntity($entity, $redirectRouteName, $redirectParameters)
{
$folder = $entity->getFolder();
if($folder == NULL){
$folder = new Folder();
$entity->setFolder($folder);
}
$form = $this->createForm(new FolderType(), $folder);
// ------------- Request Management ------------------------------------
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request); // Link Request and Form
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($folder);
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl($redirectRouteName, $redirectParameters));
}
}
return $this->render('MyProjectMediaBundle:Folder:addDocument.html.twig', array(
'form' => $form->createView(),
'entity' => $entity,
));
}
Doing that way, I just have to add one small action in each relevant controller, let say EmployeeController.php:
public function addDocumentAction(Employee $employee)
{
$redirectRouteName = 'MyProjectCore_Employee_see';
$redirectParameters = array('employee_id' => $employee->getId());
return $this->get('myprojectmedia.documenthelper')->attachToEntity($employee,$redirectRouteName,$redirectParameters);
}
Same principle for the display, in the helper I have the common function which I call in my already-existing seeAction() and in the TWIG file I import the common "Document list" display.
That's all folks!
I hope this can help :)