Мне очень нравится 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/
Добавить комментарий