24/02/2018 - DOCTRINE, SYMFONY
Doctrine entity inheritance mapping comes handy when you have more than one entity which contain same properties. It helps us reduce duplications by keeping shared properties in abstract
class and unique properties in child classes. At the end, we end up with a single table that contains all the fields in database. For more information check Inheritence Mapping.
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
- { resource: '@AppBundle/Resources/config/' }
...
services:
_defaults:
autowire: true
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
declare(strict_types=1);
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="employee", indexes={@ORM\Index(name="type_idx", columns={"type"})})
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string", length=3)
* @ORM\DiscriminatorMap({
* "Acc"="Accountant",
* "Dev"="Developer",
* "Mar"="Marketer"
* })
*/
abstract class Employee
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id", type="integer")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="firstname", type="string", length=100)
*/
private $firstname;
/**
* @var string
*
* @ORM\Column(name="lastname", type="string", length=100)
*/
private $lastname;
/**
* @var string
*
* @ORM\Column(name="level", type="string", length=100)
*/
private $level;
public function getId(): int
{
return $this->id;
}
public function setFirstname(string $firstname): self
{
$this->firstname = $firstname;
return $this;
}
public function getFirstname(): string
{
return $this->firstname;
}
public function setLastname(string $lastname): self
{
$this->lastname = $lastname;
return $this;
}
public function getLastname(): string
{
return $this->lastname;
}
public function setLevel(string $level): self
{
$this->level = $level;
return $this;
}
public function getLevel(): string
{
return $this->level;
}
}
declare(strict_types=1);
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Accountant extends Employee
{
}
declare(strict_types=1);
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Developer extends Employee
{
/**
* @var string
*
* @ORM\Column(name="calibre", type="string", length=100)
*/
private $calibre;
public function setCalibre(string $calibre): self
{
$this->calibre = $calibre;
return $this;
}
public function getCalibre(): string
{
return $this->calibre;
}
}
declare(strict_types=1);
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Marketer extends Employee
{
/**
* @var bool
*
* @ORM\Column(name="is_internal", type="boolean")
*/
private $isInternal = true;
public function setIsInternal(bool $isInternal): self
{
$this->isInternal = $isInternal;
return $this;
}
public function getIsInternal(): bool
{
return $this->isInternal;
}
}
declare(strict_types=1);
namespace AppBundle\Repository;
use AppBundle\Entity\Employee;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
class EmployeeRepository
{
private $entityRepository;
private $entityManager;
public function __construct(
EntityRepository $entityRepository,
EntityManagerInterface $entityManager
) {
$this->entityRepository = $entityRepository;
$this->entityManager = $entityManager;
}
public function findOneById(int $id): ?Employee
{
return $this->entityRepository->createQueryBuilder('e')
->where('e.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
public function insert(Employee $employee): void
{
$this->entityManager->persist($employee);
$this->entityManager->flush();
}
}
services:
_defaults:
autowire: true
autoconfigure: true
public: false
app.entity_repository.customer:
class: Doctrine\ORM\EntityRepository
factory: [ "@doctrine.orm.entity_manager", getRepository ]
arguments:
- AppBundle\Entity\Employee
AppBundle\Repository\EmployeeRepository:
arguments:
$entityRepository: "@app.entity_repository.customer"
declare(strict_types=1);
namespace AppBundle\Controller;
use AppBundle\Entity\Accountant;
use AppBundle\Entity\Developer;
use AppBundle\Entity\Employee;
use AppBundle\Entity\Marketer;
use AppBundle\Repository\EmployeeRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class DefaultController
{
private $employeeRepository;
public function __construct(
EmployeeRepository $employeeRepository
) {
$this->employeeRepository = $employeeRepository;
}
/**
* @Method({"GET"})
* @Route("/{id}")
*
* @return Response
*/
public function getOneAction(int $id)
{
$employee = $this->employeeRepository->findOneById($id);
if (!$employee instanceof Employee) {
throw new BadRequestHttpException('Employee not found.');
}
return new Response(get_class($employee), Response::HTTP_OK);
}
/**
* @Method({"POST"})
* @Route("/accountants")
*
* @return Response
*/
public function createAccountantAction(Request $request)
{
$fields = json_decode($request->getContent(), true);
$accountant = $this->createEmployee(new Accountant(), $fields);
$this->employeeRepository->insert($accountant);
return new Response(null, Response::HTTP_CREATED);
}
/**
* @Method({"POST"})
* @Route("/developers")
*
* @return Response
*/
public function createDeveloperAction(Request $request)
{
$fields = json_decode($request->getContent(), true);
/** @var Developer $developer */
$developer = $this->createEmployee(new Developer(), $fields);
$developer->setCalibre($fields['calibre']);
$this->employeeRepository->insert($developer);
return new Response(null, Response::HTTP_CREATED);
}
/**
* @Method({"POST"})
* @Route("/marketers")
*
* @return Response
*/
public function createMarketerAction(Request $request)
{
$fields = json_decode($request->getContent(), true);
/** @var Marketer $marketer */
$marketer = $this->createEmployee(new Marketer(), $fields);
$marketer->setIsInternal($fields['is_internal']);
$this->employeeRepository->insert($marketer);
return new Response(null, Response::HTTP_CREATED);
}
private function createEmployee(Employee $employee, array $fields): Employee
{
$employee->setFirstname($fields['firstname']);
$employee->setLastname($fields['lastname']);
$employee->setLevel($fields['level']);
return $employee;
}
}
POST /accountants
{
"firstname": "John",
"lastname": "Travolta",
"level": "Senior"
}
201 Created
POST /developers
{
"firstname": "Robert",
"lastname": "De Niro",
"level": "Senior",
"calibre": "Backend"
}
201 Created
POST /marketers
{
"firstname": "Al",
"lastname": "Pacino",
"level": "Junior",
"is_internal": false
}
201 Created
GET /1
200 OK
AppBundle\Entity\Accountant
GET /2
200 OK
AppBundle\Entity\Developer
GET /3
200 OK
AppBundle\Entity\Marketer
GET /4
400 Bad Request
Employee not found.
As you can see below, all the fields are in one table. The separator field here is type
.
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`firstname` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`lastname` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`level` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`type` varchar(3) COLLATE utf8_unicode_ci NOT NULL,
`calibre` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
`is_internal` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `type_idx` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
mysql> SELECT * FROM employee;
+----+-----------+----------+--------+------+---------+-------------+
| id | firstname | lastname | level | type | calibre | is_internal |
+----+-----------+----------+--------+------+---------+-------------+
| 1 | John | Travolta | Senior | Acc | NULL | NULL |
| 2 | Robert | De Niro | Senior | Dev | Backend | NULL |
| 3 | Al | Pacino | Junior | Mar | NULL | 0 |
+----+-----------+----------+--------+------+---------+-------------+
3 rows in set (0.00 sec)