Doctrine entity inheritance mapping özelliği, birden fazla entity classın aynı alan isimlerini kullandığı zaman faydalı olur. Farklı alanları ait oldukları class içinde, aynı alanları ise abstract class içinde tutarak, kod tekrarlama probleminden kurtulmamızı sağlar. Sonuç olarak elimizde tüm alanların barındırıldığı tek bir tablomuz olur. Daha fazla bilgi için Inheritence Mapping sayfasını okuyabilirsiniz.


Konfigürasyon


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

Entityler


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

Testler


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.

Veritabanı


Aşağıda'da gördüğümüz gibi tüm alanların barındırıldığı tek bir tablomuz var ve type ayırıcı alanımızdır.


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)