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.


Configuration


config.yml


imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
- { resource: '@AppBundle/Resources/config/' }

...

services.yml


services:
_defaults:
autowire: true

AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

Entities


Employee


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;
}
}

Accountant


declare(strict_types=1);

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class Accountant extends Employee
{
}

Developer


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;
}
}

Marketer


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;
}
}

EmployeeRepository


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"

DefaultController


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;
}
}

Tests


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.

Database


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)