...

понедельник, 6 января 2014 г.

[Из песочницы] Как связать 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.


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий