Как связать Yii Framework и Doctrine 2 ORM?

от автора

Мне очень нравится Yii Framework. Он быстрый, удобный, гибкий. Мне нравится, как реализован в нём паттерн ActiveRecord. Но бывают случаи, когда бизнес-логика, а, если быть точным, доменная логика, очень сложная и постоянно растёт и модифицируется. В таких случаях удобнее пользоваться паттерном DataMapper.

В тоже время мне нравится Doctrine 2 ORM. Это пожалуй самая мощная ORM для PHP, имеющая широчайший функционал. Да, возможно, она «тяжеловата» и замедляет работу приложения. Но начиная разработку, прежде всего стоит думать об архитектуре приложения, так как «преждевременная оптимизация корень всех бед»

Таким образом, однажды мне пришла в голову мысль связать 2 этих интересных мне инструмента. Как это было сделано, описано ниже.

Установка необходимых библиотек

Связать Doctrine и Yii было решено с помощью создания соответствующего компонента DoctrineComponent, который бы и предоставлял доступ к функциям Doctrine.

Первым делом, в папке protected фреймворка была создана папка vendor, куда и был загружен код Doctrine 2 ORM. Установить Doctrine можно с помощью Composer либо просто скачав/склонировав исходники из GitHub проекта Doctrine.
Также, для корректной работы ORM понадобятся Doctrine Database Abstraction Layer и Doctrine Common (при установке Doctrine 2 ORM с помощью Composer данные зависимости подтягиваются автоматически).

Кроме того, советую для того, чтобы была возможность работать с Doctrine 2 ORM через консоль установить в туже папку vendor 2 компонента Symfony — это Console (для работы с Doctrine через консоль) и Yaml (при желании описания сущностей на Yaml)

Таким образом, на данном этапе должна быть получена следующая структура проекта:

Создание компонента DoctrineComponent

Теперь можно перейти непосредственно к созданию компонента DoctrineComponent. Ниже я приведу целиком код компонента, благо он достаточно небольшой. Данный код должен находится в папке protected/components в файле DoctrineComponent.php.

use Doctrine\ORM\EntityManager; use Doctrine\ORM\Configuration; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry;  class DoctrineComponent extends CComponent {     private $em = null;     private $basePath;     private $proxyPath;     private $entityPath;     private $driver;     private $user;     private $password;     private $host;     private $dbname;            public function init()     {         $this->initDoctrine();     }      public function initDoctrine()     {         Yii::setPathOfAlias('Doctrine', $this->getBasePath() . '/vendor/Doctrine');          $cache = new Doctrine\Common\Cache\FilesystemCache($this->getBasePath() . '/cache');         $config = new Configuration();         //  $config->setMetadataCacheImpl($cache);           $driverImpl = new AnnotationDriver(new AnnotationReader(), $this->getEntityPath());         AnnotationRegistry::registerAutoloadNamespace('Doctrine\ORM\Mapping', $this->getBasePath() . '/vendor');          $config->setMetadataDriverImpl($driverImpl);         $config->setQueryCacheImpl($cache);         $config->setProxyDir($this->getProxyPath());         $config->setProxyNamespace('Proxies');         $config->setAutoGenerateProxyClasses(true);         $connectionOptions = array(             'driver' => $this->getDriver(),             'user' => $this->getUser(),             'password' => $this->getPassword(),             'host' => $this->getHost(),             'dbname' => $this->getDbname()         );          $this->em = EntityManager::create($connectionOptions, $config);     }        public function setBasePath($basePath)     {         $this->basePath = $basePath;     }      public function getBasePath()     {         return $this->basePath;     }      public function setEntityPath($entityPath)     {         $this->entityPath = $entityPath;     }      public function getEntityPath()     {         return $this->entityPath;     }      public function setProxyPath($proxyPath)     {         $this->proxyPath = $proxyPath;     }      public function getProxyPath()     {         return $this->proxyPath;     }      public function setDbname($dbname)     {         $this->dbname = $dbname;     }      public function getDbname()     {         return $this->dbname;     }      public function setDriver($driver)     {         $this->driver = $driver;     }      public function getDriver()     {         return $this->driver;     }      public function setHost($host)     {         $this->host = $host;     }      public function getHost()     {         return $this->host;     }      public function setPassword($password)     {         $this->password = $password;     }      public function getPassword()     {         return $this->password;     }      public function setUser($user)     {         $this->user = $user;     }      public function getUser()     {         return $this->user;     }       /**      * @return EntityManager      */     public function getEntityManager()     {         return $this->em;     } } 

Основная часть компонента заключена в методе initDoctrine. Разберём подробнее код.

$cache = new Doctrine\Common\Cache\FilesystemCache($this->getBasePath() . '/cache'); $config = new Configuration(); $config->setMetadataCacheImpl($cache); 

Данным кодом мы устанавливаем метод кеширования метаданных сущностей из Doctrine. По-хорошему, тип кеширования (в данном случае FilesystemCache) следовало бы лучше вынести в параметры компонента, который мы могли бы менять при конфигурировании компонента.

$driverImpl = new AnnotationDriver(new AnnotationReader(), $this->getEntityPath()); AnnotationRegistry::registerAutoloadNamespace('Doctrine\ORM\Mapping', $this->getBasePath() . '/vendor'); $config->setMetadataDriverImpl($driverImpl); 

С помощью кода выше устанавливается драйвер для чтения метаданных сущностей.

 $config->setQueryCacheImpl($cache);  $config->setProxyDir($this->getProxyPath());  $config->setProxyNamespace('Proxies');  $config->setAutoGenerateProxyClasses(true); 

Кодом выше мы устанавливаем метод кеширования для запросов (первая строчка), остальные строки — настройка Proxy для Doctrine (путь, пространство имён, установка автоматического генерирования Proxy-классов)

$connectionOptions = array(     'driver' => $this->getDriver(),     'user' => $this->getUser(),     'password' => $this->getPassword(),     'host' => $this->getHost(),     'dbname' => $this->getDbname() ); $this->em = EntityManager::create($connectionOptions, $config); 

Код выше определяет опции соединения с БД. Данные параметры задаются при подключении компонента (будет показано далее, как подключить компонент).
И в конце создаётся EntityManager с определёнными раннее $connectionOptions и $config, с помощью которого и можно работать с нашими сущностями.

Как подключить DoctrineComponent к проекту?

Перейдём к подключению DoctrineComponent к проекту.
Сделать этого довольно просто — необходимо просто внести изменения в конфигурационный файл проекта (обычно это main.php)

return array(     'components' => array(         'doctrine'=>array(             'class' => 'DoctrineComponent',             'basePath' => __DIR__ . '/../',             'proxyPath' => __DIR__ . '/../proxies',             'entityPath' => array(                 __DIR__ . '/../entities'             ),             'driver' => 'pdo_mysql',             'user' => 'dbuser',             'password' => 'dbpassword',             'host' => 'localhost',             'dbname' => 'somedb'         ),         // ... ); 

Теперь наш компонент будет доступен через Yii::app()->doctrine, а получить EntityManager мы можем через Yii::app()->doctrine->getEntityManager()

Но при таком использовании компонента возникает проблема в подсказках методов для объекта EntityManager. Для этого было придумано следующее решение:

сlass MainController extends Controller {     private $entityManager = null;      /**      * @return Doctrine\ORM\EntityManager      */     public function getEntityManager()     {         if(is_null($this->entityManager)){             $this->entityManager = Yii::app()->doctrine->getEntityManager();         }         return $this->entityManager;     }     // ... } 

Каждый контроллер теперь наследуется от MainController и таким образом, в каждом контроллере можно вызвать метод $this->getEntityManager() для получения менеджера сущностей, причём в IDE теперь будут работать подсказки методов для EntityManager, что несомненно является плюсом.

Настройка консоли Doctrine

С Doctrine очень удобно работать через её консоль. Но для этого необходимо написать код для её запуска. Этот код приведён ниже. Я положил файл для запуска консоли в папку protected/commands. Очень хорошо также было бы реализовать команду doctrine для ещё более простого запуска консоли, но мной пока этого сделано не было.

Пример файл doctrine.php для работы с консолью Doctrine.

// change the following paths if necessary $yii = __DIR__ .'path/to/yii.php'; $config = __DIR__ . 'path/to/config/console.php';  require_once($yii); Yii::createWebApplication($config); Yii::setPathOfAlias('Symfony', Yii::getPathOfAlias('application.vendor.Symfony'));  $em = Yii::app()->doctrine->getEntityManager(); $helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(     'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),     'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em) ));  \Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet); 

Чтобы запустить консоль Doctrine достаточно перейти в папку commands и выполнить php doctrine.php.

Валидация моделей и использование моделей в виджете GridView.

Те, кто работал с Doctrine 2 ORM, знают, что фактически моделей в общепринятом их понятии (с методами валидации, получения данных из БД, включённой бизнес-логикой и т.д.) нет, а функциональность эта фактически разбита на 2 части — Entity и Repository. В Entity обычно включают бизнес-логику, а в Repository — методы получения данных из БД с использование DBAL Doctrine (либо менеджер сущностей, либо другим иным способам).

Валидация моделей

Таким образом, на мой взгляд, логично было бы включить валидацию данных в класс конкретной сущности.
Рассмотрим на примере сущности User.
Чтобы не изобретать велосипед, было решено, что неплохо было бы использовать уже встроенную валидацию моделей из Yii, а конкретно из класса CModel.

Для этого просто-напросто можно наследовать сущность User от класса CModel. Пример такой сущности с описанными правилами валидации ниже:

use Doctrine\ORM\Mapping as ORM;  /**  * User  *  * @ORM\Table(name="user")  * @ORM\Entity(repositoryClass="UserRepository")  * @ORM\HasLifecycleCallbacks  */ class User extends CModel {     /**      * @var integer      *      * @ORM\Column(name="id", type="integer", nullable=false)      * @ORM\Id      * @ORM\GeneratedValue(strategy="IDENTITY")      */     private $id;      /**      * @var string      *      * @ORM\Column(name="name", type="string", length=255, nullable=false)      */     private $name;      /**      * @var string      *      * @ORM\Column(name="password", type="string", length=255, nullable=false)      */     private $password;      /**      * @var string      *      * @ORM\Column(name="email", type="string", length=255, nullable=false)      */     private $email;      /**      * @var string      *      * @ORM\Column(name="role", type="string", length=255, nullable=false)      */     private $role;      /**      * @var \DateTime      *      * @ORM\Column(name="created", type="datetime", nullable=false)      */     private $created;      /**      * @var \DateTime      *      * @ORM\Column(name="modified", type="datetime", nullable=false)      */     private $modified;       public function rules(){         return array(             array('name, password', 'required'),             // ...         );     }      public function attributeNames()     {         return array(             'id'=>'id',             'name'=>'name',             'email'=>'email',             'created'=>'created',             'updated'=>'updated'         );     }      public function attributeLabels()     {         return array(             'description' => 'Description',             'createdString' => 'Creation Date'         );     }     // ...  } 

Теперь приведу пример как с этой валидацией работать (пример создания нового пользователя ниже):

    /**      * Creates a new model.      * If creation is successful, the browser will be redirected to the 'view' page.      */     public function actionCreate()     {         $user = new User();          $userData = $this->getRequest()->get('User');         $course->setAttributes($userData);         if(!is_null($userData) && $user->validate())         {             $user->setName($userData['name']);             // ... и так далее все поля              $this->getEntityManager()->persist($user);             $this->getEntityManager()->flush();             $this->redirect(array('view','id'=>$user->getId()));         }          $this->render('create',array(             'model'=>$user,         ));     } 
Использование моделей в виджете GridView

Одной из самых главных прелестей Yii являются, по-моему, виджеты, а в особенности различные Grid, которые идут в Yii из коробки.
Но единственный нюанс — они работают с ActiveRecord (я имею ввиду виджет GridView). А лично мне бы хотелось заставить их работать с Doctrine и сущностями. Для этого можно использовать Repository.

При использовании GridView есть 2 узких места — свойства dataProvider и filter. И здесь я пою оды разработчикам Yii — для того, чтобы GridView работал с какими-то данными, отличными от полученных из ActiveRecord, достаточно, чтобы объект, переданный в GridView в качестве dataProvider правильно реализовывал интерфейс IDataProvider (этот интерфейс и следует реализовать в нашем UserRepository), а объект, переданный в filter, — должен наследоваться от CModel (наша сущность User уже отлично подходит для этого).

Всю реализацию UserRepository приводить не буду, обрисую только общую схему.

use Doctrine\ORM\EntityRepository;  abstract class BaseRepository extends EntityRepository implements IDataProvider {     protected $_id;     private $_data;     private $_keys;     private $_totalItemCount;     private $_sort;     private $_pagination;      public $modelClass;     public $model;     public $keyAttribute;      private $_criteria;     private $_countCriteria;      public $data;      abstract protected function fetchData();     abstract protected function calculateTotalItemCount();       public function getId(){ //... }      public function getPagination($className='CPagination'){ //... }      public function setPagination($value){ //... }      public function setSort($value){ //... }      public function getData($refresh=false){ //... }      public function setData($value){ //... }      public function getKeys($refresh=false){ //... }      public function setKeys($value){ //... }      public function getItemCount($refresh=false){ //... }      public function getTotalItemCount($refresh=false){ //... }      public function setTotalItemCount($value){ //... }      public function getCriteria(){ //... }      public function setCriteria($value){ //... }      public function getCountCriteria(){ //... }      public function setCountCriteria($value){ //... }      public function getSort($className='CSort'){ //... }      protected function fetchKeys(){ //... }      private function _getSort($className){ //... }  } 

Выше пример реализации базового репозитория. Фактически реализацию многих методов можно подсмотреть в Yii классе CActiveDataProvider который и реализует интерфейс IDataProvider. В UserRepository нам придётся определить лишь 2 метода(пример кода ниже):

<?php  class UserRepository extends BaseRepository {     protected $_id = 'UserRepository';      /**      * Fetches the data from the persistent data storage.      * @return array list of data items      */     protected function fetchData()     {        //...     }      /**      * Calculates the total number of data items.      * @return integer the total number of data items.      */     protected function calculateTotalItemCount()     {         //...     } } 

Резюме

Выше я привёл один их способов того, как можно работать в связке Yii + Doctrine 2 ORM. Многие могут сказать, что из-за Doctrine 2 ORM Yii потеряет свои преимущества, но не стоит забывать, что Doctrine имеет огромное количество средств для оптимизации и кеширования, да и никто не запрещает переписать слишком медленные либо интенсивные запросы на Plain SQL.
Зато в такой связке мы выигрываем в архитектурном решении и на мой взгляд, код становится от этого чище.

Был бы очень признателен, если бы в комментариях Вы поделились своими вариантами решения по внедрению паттерна DataMapper, каких-то других ORM в Yii, о своих способах решения разрастания бизнес логики в моделях ActiveRecord в Yii, о предметно-ориентированном программировании с использовании Yii.

Спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/208182/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *