
Особенности использования Doctrine2 для реализации наследования
Часто некоторые сущности обладают общими свойствами, которые имеет смысл оставить в базовой сущности (таблице). Хорошим примером этого может служить база продуктов с сервиса sql-ex.ru, с которым многие сталкивались при изучении SQL. Попробуем реализовать структуру предлагаемой базы данных с помощью Symfony и Doctrine2.
База эта интересна тем, что служит хорошим примером наследования. В её основе лежит сущность "продукт", у которой есть поля Производитель, Модель и Тип. В оригинальной базе данных в качестве внешнего ключа использовалось поле Модель, но в Doctrine дублирование этого поля в таблицах будет излишним, кроме того имеет смысл создать отдельное поле Id для некоторой систематизированности и расширяемости. Для описанного выше создадим Entity Product.

namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"product" = "Product", "pc" = "PC"})
*/
class Product
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $maker;
/**
* @ORM\Column(type="string", length=255)
*/
private $model;
public function getId(): ?int {
return $this->id;
}
public function getMaker(): ?string {
return $this->maker;
}
public function setMaker(string $maker): self {
$this->maker = $maker;
return $this;
}
public function getModel(): ?string {
return $this->model;
}
public function setModel(string $model): self {
$this->model = $model;
return $this;
}
}
Кроме того создадим дочерний класс PC
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class PC extends Product
{
/**
* @ORM\Column(type="string", length=255)
*/
private $code;
/**
* @ORM\Column(type="integer")
*/
private $speed;
/**
* @ORM\Column(type="integer")
*/
private $ram;
/**
* @ORM\Column(type="integer")
*/
private $hd;
/**
* @ORM\Column(type="integer")
*/
private $cd;
/**
* @ORM\Column(type="float", scale=10, precision=2)
*/
private $price;
public function getCode() {
return $this->code;
}
public function getSpeed() {
return $this->speed;
}
public function getRam() {
return $this->ram;
}
public function getHd() {
return $this->hd;
}
public function getCd() {
return $this->cd;
}
public function getPrice() {
return $this->price;
}
public function setCode($value) {
$this->code = $value;
return $this;
}
public function setSpeed($value) {
$this->speed = $value;
return $this;
}
public function setRam($value) {
$this->ram = $value;
return $this;
}
public function setHd($value) {
$this->hd = $value;
return $this;
}
public function setCd($value) {
$this->cd = $value;
return $this;
}
public function setPrice($value) {
$this->price = $value;
return $this;
}
}
Теперь обратим внимание на три последних аннотации для класса Product
@ORM\InheritanceType("JOINED") - здесь мы указываем тип наследования JOINED, тем самым указывая, что хранить каждый Entity нужно в отдельной таблице со своими колонками, не перегружая, например, Laptop колонками Printer и т.д.
@ORM\DiscriminatorColumn(name="type", type="string") - здесь мы указываем, что именно поле type будет использоваться для определения нужной таблицы, а значения для колонки type берутся из последующей аннотации.
@ORM\DiscriminatorMap({"product" = "Product", "pc" = "PC"}) - здесь видно, что для класса Product в свойстве type будет храниться значение product, а для PC - pc. Позднее для Laptop и Printer мы добавим их значения.
После настройки подключения к базе данных можно средствами Doctrine создать миграцию следующей командой php bin/console doctrine:migrations:diff и, если всё будет хорошо ;), то мигрировать, - php bin/console doctrine:migrations:migrate
В целом основа для последующего расширения проекта уже сделана, кроме этого можно добавить аннотации геттерам и сеттерам и типы принимаемых/возвращаемых значений..
Для реализации остальных таблиц достаточно добавить классы:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class Laptop extends Product
{
/**
* @ORM\Column(type="string", length=255)
*/
private $code;
/**
* @ORM\Column(type="integer")
*/
private $speed;
/**
* @ORM\Column(type="integer")
*/
private $ram;
/**
* @ORM\Column(type="integer")
*/
private $hd;
/**
* @ORM\Column(type="integer")
*/
private $screen;
/**
* @ORM\Column(type="float", scale=10, precision=2)
*/
private $price;
public function getCode() {
return $this->code;
}
public function getSpeed() {
return $this->speed;
}
public function getRam() {
return $this->ram;
}
public function getHd() {
return $this->hd;
}
public function getScreen() {
return $this->screen;
}
public function getPrice() {
return $this->price;
}
public function setCode($value) {
$this->code = $value;
return $this;
}
public function setSpeed($value) {
$this->speed = $value;
return $this;
}
public function setRam($value) {
$this->ram = $value;
return $this;
}
public function setHd($value) {
$this->hd = $value;
return $this;
}
public function setScreen($value) {
$this->screen = $value;
return $this;
}
public function setPrice($value) {
$this->price = $value;
return $this;
}
}
and
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class Printer extends Product
{
/**
* @ORM\Column(type="boolean")
*/
private $color;
/**
* @ORM\Column(type="string")
*/
private $printerType;
/**
* @ORM\Column(type="float", scale=10, precision=2)
*/
private $price;
public function getColor() {
return $this->color;
}
public function setColor($value) {
$this->color = $value;
return $this;
}
public function getPrinterType() {
return $this->printerType;
}
public function setPrinterType($value) {
$this->printerType = $value;
return $this;
}
public function getPrice() {
return $this->price;
}
public function setPrice($value) {
$this->price = $value;
return $this;
}
}
Остается изменить строчку
@ORM\DiscriminatorMap({"product" = "Product", "pc" = "PC"})
на
@ORM\DiscriminatorMap({"product" = "Product", "pc" = "PC", "laptop" = "Laptop", "printer" = "Printer"})
После этого снова мигрируем и получаем четыре таблицы: product - базовая, и три производных, объединение которых с базовой происходит по внешнему ключу Id.
Если мы сделаем запрос с помощью QueryBuilder в одном из методов контроллера, то получим объекты нужного класса:
* @Route("/printer/list")
*/
public function printerListAction() {
$em = $this->getDoctrine()->getManager();
/** @var QueryBuilder $qb */
$qb = $em
->getRepository(Printer::class)
->createQueryBuilder('p')
;
$results = $qb
->setMaxResults(20)
->getQuery()
->getArrayResult()
;
return new JsonResponse(['status' => 'success', 'data' => [$results]]);
}
где все необходимые классы уже импортированы:
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
Теперь расширение проекта на прочной основе с использованием встроенного функционала Doctrine2 будет происходить быстрее и легче, и вы можете попробовать добавить свои классы и таблицы.


Украина, Житомир
ул. Витрука 9в
Пн-Пт 9.00 - 19.00