Приветствую всех, кто заскочил! В прошлой статье я дал обзорную характеристику ZendFramework и ORM Doctrine, рассказал, как это все установить.
Пришло время посмотреть реализацию!
Что такое Entity
В соответствии с принципом MVC, первым и самым фундаментальным элементом системы является модель. В случае с нашей ORM модель носит название Entity, или сущность. У нас, как и у любого серьезного проекта, есть база данных со множеством таблиц. Так вот, сущность олицетворяет собой программное «отображение» таблицы БД.
Одна таблица — одна сущность.
В простейшем примере, о котором я упомянул в прошлый раз, сущностью является город(City). И ее реализацией послужит ООП класс с аналогичным названием — City. Один класс — одна сущность — один PHP файл. Напомню, что сущности в проекте находятся по адресу: /ZendFramework2/module/Application/src/Application/Entity.
У нашего города всего два поля: id и name. Создание таблицы в базе данных не относится к обсуждаемой теме, поэтому будем считать, что она уже создана и в ней существует по крайней мере одна запись.
Вот, как будет выглядеть файл City.php
<?php
//Первое, что нужно сделать - это определить пространство имен для файла. Мы указываем, что это сущность
namespace Application\Entity;
//Подключаем необходимые библиотеки доктрины:
//Маппинг - это отображение таблицы БД на этот класс и обратно
use Doctrine\ORM\Mapping as ORM;
//Аннотации, которые помогут построить это отображение
use Doctrine\Common\Annotations\AnnotationRegistry;
use Zend\InputFilter\Factory as InputFactory;
//Первый пример аннотации. Мы указываем имя таблицы, с которой связываем сущность
//И указываем непосредственно, что описанный ниже класс является этой сущностью
/**
* @ORM\Table(name="City");
* @ORM\Entity();
*/
class City {
//Первое поле в таблице - это идентификатор.
//Сообщаем доктрине информацию: тип поля, идентификатор, автоинкремент
/**
* @ORM\Column(type="integer");
* @ORM\Id;
* @ORM\GeneratedValue(strategy="AUTO");
*/
protected $id;
//Поле имя имеет строковый тип
/**
* @ORM\Column(type="string");
*/
protected $name;
//Очень важно написать "сетеры" и "гетеры" для всех полей,
//чтобы дать доктрине возможность заполнять и редактировать сущность
public function setId($id) {
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function setName($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
}
?>
Я подробно закомментировал наиболее важные моменты и повторяться не буду, скажу лишь, что следует различать мои однострочные комментарии (//…) и специальные многострочные комментарии-аннотации вида * @ORM/…, которые предназначены для общения с Doctrine.
Связи между сущностями
Мы описали одну сущность, это отлично! Но где же видано, чтобы проект состоял только из одной? Сейчас начнется самое интересное, я постараюсь дать краткое и понятное описание связей между сущностями так, как понял это я сам.
Связь один ко многим OneToMany
Введем в игру новую сущность под названием Область(Area). Имеется ввиду, например, Челябинская область, которая содержит множество городов, в том числе и Челябинск. В базе данных таблица Area будет содержать поля: id, name. В свою очередь город обзаведется новым полем — area_id. Сразу же создаем новый файл Area.php с описанием класса. Вы узнаете в нем город, за исключением одного момента. Появилось новое поле класса — $cities. Это массив, который будет хранить в себе список городов, относящихся к этой области. Благодаря аннотациям и двум дополнительным методам, доктрина самостоятельно будет инициализировать сущность и работать с ней.
Файл Area.php
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Zend\InputFilter\Factory as InputFactory;
/**
* @ORM\Table(name="Area");
* @ORM\Entity();
*/
class Area {
/**
* @ORM\Column(type="integer");
* @ORM\Id;
* @ORM\GeneratedValue(strategy="AUTO");
*/
protected $id;
/**
* @ORM\Column(type="string");
*/
protected $name;
//Аннотация гласит о том, что область связана с городом по принципу
//один ко многим(одна область содержит множество городов)
//в targetEntity указывается путь до сущности "город", mappedBy - это название поля в
//City, которое будет содержать его область
/**
* @ORM\OneToMany(targetEntity="Application\Entity\City", mappedBy="area")
*/
private $cities;
//Городов много, поэтому $cities инициализируем, как массив
function __construct() {
$this->cities = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setId($id) {
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function setName($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
//Сетеры и гетеры нужно писать для всех полей
public function getCities() {
return $this->cities;
}
public function setCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
$this->cities = $cities;
}
//Добавление городов в область. Этот метод позволит связать город и область
public function addCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
foreach($cities as $city) {
//Поставить область каждому городу
$city->setArea($this);
//Добавить город в область
$this->cities->add($city);
}
}
//Если есть добавление, то должно быть и удаление
public function removeCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
foreach($cities as $city) {
$this->cities->removeElement($city);
}
}
}
?>
Связь многие к одному ManyToOne
Связь многие к одному появляется в сущности City. Я уже упомянул, что в таблице добавится поле area_id. А в классе аж два новых поля: $area_id и $area. Первое полностью соответствует полю в таблице, а второе будет содержать саму область(объект класса Area), которой принадлежит город.
Обновление файла City.php
//...
class City {
//...
//Описываем связь многие к одному. cascade={"persist"} говорит нам о том, что
//при сохранении города в базу данных, сущность тоже будет сохранена(если ее еще там нет)
//inversedBy содежит имя поля в области, которое хранит список городов
/**
* @ORM\ManyToOne(targetEntity="Application\Entity\Area", cascade={"persist"}, inversedBy="cities")
*/
private $area;
//...
//Опять же, не забываем сетеры и гетеры
public function setAreaId($id) {
$this->area_id = $id;
}
public function getAreaId() {
return $this->area_id;
}
public function setArea($area) {
$this->area = $area;
}
public function getArea() {
return $this->area;
}
}
Связь многие ко многим ManyToMany
Продолжим нашу концепцию и введем улицу(Street). А для того, чтобы «подогнать» ее под ManyToMany скажем, что одна улица может находиться в разных городах. Действительно, почти во всех городах нашей страны есть улица Ленина. Так вот, со стороны города появится массив улиц, а улица, в свою очередь, содержит массив городов.
В базе данных связь между улицей и городом реализована дополнительной таблицей, под названием city_street, которая содержит три поля: id, city_id, street_id. Оба идентификатора являются внешними ключами MySQL для таблиц City и Street соответственно.
Подробнее о том, как настроить внешние ключи для такой таблицы я писал в этой статье.
Файл Street.php
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Zend\InputFilter\Factory as InputFactory;
/**
* @ORM\Table(name="Street");
* @ORM\Entity();
*/
class Street {
/**
* @ORM\Column(type="integer");
* @ORM\Id;
* @ORM\GeneratedValue(strategy="AUTO");
*/
protected $id;
/**
* @ORM\Column(type="string");
*/
protected $name;
//Описываем связь многие ко многим.
//В JoinTable описывается таблица связей.
//joinColumns - поле в таблице связей, которое отвечает за текущую сущность
//name - имф поля в таблице связей, referencedColumnName - имя поля в таблице сущности
//inverseJoinColumns - поле -//- за связанную сущность
/**
* @ORM\ManyToMany(targetEntity="City", cascade={"persist"})
* @ORM\JoinTable(name="city_street",
* joinColumns={@ORM\JoinColumn(name="street_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="city_id", referencedColumnName="id")}
* )
*/
private $cities;
function __construct() {
$this->cities = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setId($id) {
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function setName($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
public function setCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
$this->cities = $cities;
}
public function getCities() {
return $this->cities;
}
public function addCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
foreach ($cities as $city) {
$this->cities->add($city);
}
}
public function removeCities(\Doctrine\Common\Collections\ArrayCollection $cities) {
foreach ($cities as $city) {
$city->getStreets()->removeElement($this);
$this->cities->removeElement($city);
}
}
}
?>
Класс Street строится по такому же принципу, что и другие. Всю основную информацию о связи многие ко многим мы описали в аннотациях к полю $cities. Осталось только добавить в город массив улиц и все заработает.
Обновление City.php
//...
class City {
//...
//С другой стороны описание попроще, достаточно указать
//связанную сущность, в которой описана основная информация
/**
*@ORM\ManyToMany(targetEntity="Street")
*/
private $streets;
function __construct() {
$this->streets = new \Doctrine\Common\Collections\ArrayCollection();
}
//...
public function setStreets(\Doctrine\Common\Collections\ArrayCollection $streets) {
$this->streets = $streets;
}
public function getStreets() {
return $this->streets;
}
public function addStreets(\Doctrine\Common\Collections\ArrayCollection $streets) {
foreach ($streets as $street) {
$this->streets->add($street);
}
}
public function removeStreets(\Doctrine\Common\Collections\ArrayCollection $streets) {
foreach ($streets as $street) {
$street->getCities()->removeElement($this);
$this->streets->removeElement($street);
}
}
}
Связи работают
Нам больше не нужно обращаться к базе данных, мы дали доктрине исчерпывающую информацию о нашем проекте и о структуре базы данных. Теперь мы работаем в чистом ООП, создаем объекты классов, и изменяем состояние базы данных с помощью их методов.
Заключение
Самое сложное для меня было научиться определять связь и не путаться. Нужно понять, что первое слово в связи относится к той сущность в которой ты «находишься». Много городов в одной области, и одна область содержит множество городов. Тогда проблем с определением типа связи возникнуть не должно.
В следующий раз я затрону тему оставшихся двух элементов системы — это представление и контроллер. А потом начнем знакомится с обширным количеством возможностей фреймворка, которые позволяют создать приложение практически любой сложности. А на сегодня у меня все, спасибо за внимание!