Молодогвардейцев 454015 Россия, Челябинская область, город Челябинск 89085842764
MindHalls logo

Zend Framework 2 + Doctrine. Сущности и связи

Приветствую всех, кто заскочил! В прошлой статье я дал обзорную характеристику 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);
        }
    }
}

Связи работают

Нам больше не нужно обращаться к базе данных, мы дали доктрине исчерпывающую информацию о нашем проекте и о структуре базы данных. Теперь мы работаем в чистом ООП, создаем объекты классов, и изменяем состояние базы данных с помощью их методов.

Заключение

Самое сложное для меня было научиться определять связь и не путаться. Нужно понять, что первое слово в связи относится к той сущность в которой ты «находишься». Много городов  в одной области, и одна область содержит множество городов. Тогда проблем с определением типа связи возникнуть не должно.

В следующий раз я затрону тему оставшихся двух элементов системы — это представление и контроллер. А потом начнем знакомится с обширным количеством возможностей фреймворка, которые позволяют создать приложение практически любой сложности. А на сегодня у меня все, спасибо за внимание!