Приветствую всех, кто заскочил! В прошлой статье я дал обзорную характеристику 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); } } }
Связи работают
Нам больше не нужно обращаться к базе данных, мы дали доктрине исчерпывающую информацию о нашем проекте и о структуре базы данных. Теперь мы работаем в чистом ООП, создаем объекты классов, и изменяем состояние базы данных с помощью их методов.
Заключение
Самое сложное для меня было научиться определять связь и не путаться. Нужно понять, что первое слово в связи относится к той сущность в которой ты «находишься». Много городов в одной области, и одна область содержит множество городов. Тогда проблем с определением типа связи возникнуть не должно.
В следующий раз я затрону тему оставшихся двух элементов системы — это представление и контроллер. А потом начнем знакомится с обширным количеством возможностей фреймворка, которые позволяют создать приложение практически любой сложности. А на сегодня у меня все, спасибо за внимание!