
Особливості використання Doctrine2 для реалізації успадкування
Часто деякі сутності володіють загальними властивостями, які слід залишити в базовій сутності (таблиці). Хорошим прикладом цього може служити база продуктів з сервісу sql-ex.ru, з яким багато хто стикався при вивченні SQL. Спробуємо реалізувати структуру пропонованої бази даних за допомогою Symfony і Doctrine2.
Ця база цікава тим, що служить добрим прикладом успадкування. В її основі лежить сутність "продукт", у якій є поля Виробник, Модель і Тип. В оригінальній базі даних як зовнішній ключ використовувалося поле Модель, але в Doctrine дублювання цього поля в таблицях буде зайвим. Крім того, має сенс створити окреме поле Id для деякої систематизированности і розширюваності. Для описаного вище створимо Entity Product.
<?php
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
<?php
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.
В цілому основа для подальшого розширення проекту вже зроблена, крім цього можна додати анотації геттерам і сеттерам і типи прийнятих/повернутих значень.
Для реалізації інших таблиць досить додати класи:
<?php
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;
}
}
i
<?php
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 App\Entity\Printer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
Тепер розширення проекту на міцній основі з використанням вбудованого функціоналу Doctrine2 відбуватиметься швидше і легше, і ви можете спробувати додати свої класи і таблиці.