Скрещиваем двух «зверей»
В принципе, скрестить Zend Framework с Doctrine не так уж сложно. Но прежде поговорим о подготовительной работе. По мнению автора, предлагаемую по умолчанию структуру файлов проекта Zend Framework можно сделать чуть более оптимальной.
Так выглядит структура файлов проекта Zend Framework по умолчанию:
/
application/
default/
controllers/
layouts/
models/
views/
html/
library/Зачастую может оказаться так, что приложений у вас будет несколько (например, frontend/ и backend/), а модель вы будете использовать одну и ту же. В этом случае разумным было бы вынести вашу models/ в папку library/, в этом случае новая структура выглядела бы следующим образом:
/
application/
default/
controllers/
layouts/
views/
html/
library/
Model/Кроме того, как видно, папка models/ была переименована в Model. Далее действуем так.
- Скачиваем свежий дистрибутив Doctrine-x.x.x-Sandbox.tgz с официального сайта.
- Содержимое папки lib/ из архива копируем в папку library/ нашего проекта.
- Создаем в корне нашего проекта еще одну папку bin/sandbox/ и в нее копируем остальное содержимое архива (за исключением папки models/ и файла index.php — они нам не нужны).
Теперь файлы нашего проекта должны выглядеть примерно так:
/
application/
default/
controllers/
layouts/
views/
bin/
sandbox/
data/
lib/
migrations/
schema/
config.php
doctrine
doctrine.php
html/
library/
Doctrine/
Model/
Doctrine.phpПапку bin/sandbox/lib/ очищаем от содержимого — библиотека у нас теперь в другом месте.
Пришло время сконфигурировать Doctrine для работы в рамках новой структуры файлов.
Изменим значение константы MODELS_PATH в файле bin/sandbox/config.php на:
SANDBOX_PATH . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'Model'
Далее меняем настройки соединения с БД. Меняем значение константы DSN на то, что необходимо. Например, если вы используете СУБД MySQL, DSN может выглядеть следующим образом:
'mysql://root:123@localhost/mydbname'
Сконфигурируем include_paths первой срочкой в конфиге, чтобы наши скрипты могли отыскивать файлы на новых местах:
set_include_path( '.' . PATH_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . PATH_SEPARATOR . '.' . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path());
Далее подключим основной файл библиотеки Doctrine, сразу после установки путей, и установим функцию автозагрузки:
<?php
require_once 'Doctrine.php';
/**
* Setup autoload function
*/
spl_autoload_register( array(
'Doctrine',
'autoload'
));
?>Т.е., в общем, наш конфиг должен выглядеть примерно так:
<?php
set_include_path( '.' . PATH_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . PATH_SEPARATOR . '.' . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path());
require_once 'Doctrine.php';
/**
* Setup autoload function
*/
spl_autoload_register( array(
'Doctrine',
'autoload'
));
define('SANDBOX_PATH', dirname(__FILE__));
define('DATA_FIXTURES_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'fixtures');
define( 'MODELS_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'Model');
define('MIGRATIONS_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . 'migrations');
define('SQL_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'sql');
define('YAML_SCHEMA_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . 'schema');
define('DB_PATH', SANDBOX_PATH . DIRECTORY_SEPARATOR . 'sandbox.db');
define('DSN', 'mysql://root:123@localhost/mydbname');
Doctrine_Manager::connection( DSN, 'sandbox');
Doctrine_Manager::getInstance()->setAttribute('model_loading', 'conservative');
?>Вот теперь мы сосредоточимся на очень интересном моменте.
Дело в том, что Doctrine не генерирует set() и get() методы для свойств объектов, а использует автоматические методы __get() и __set(). А поскольку сами свойства скрыты в рамках одного свойства класса-родителя, то ни одна среда разработки никогда вам не подскажет их в автокомплите. Но это всего лишь неудобство от которого мы можем легко избавиться, а плюс к этому получить еще кое-какие дополнительные удобства. Сейчас мы продемонстрируем, как же это сделать.
Тюнингуем Doctrine Sandbox
В поставку консольного приложения для Doctrine входит класс Doctrine_Cli, который, собственно, и реализует его функциональность. Мы пронаследуем его и эту функциональность расширим следующим образом. Создадим свой класс SandboxCli:
<?php
/**
* Class SandboxCli
* Extends default Doctrine Client functionality
*
* @package Sandbox
*/
class SandboxCli extends Doctrine_Cli {
/**
* Public function to run the loaded task with a given argument
*
* @param array $args
* @return void
*/
public function run( $args) {
ob_start();
parent::run( $args);
$msg = ob_get_clean();
$this->_chmod();
if (isset( $args[1]) && ($args[1] == 'generate-models-yaml')) {
$this->_genBaseClasses();
$this->_genSgMethods();
$this->_chmod();
}
echo $msg;
}
/**
* Automatically creates base table and record classes if they are not exists
*
* @param void
* @return void
*/
protected function _genBaseClasses() {
$dir = $this->_config['models_path'] . DIRECTORY_SEPARATOR . 'Base' . DIRECTORY_SEPARATOR;
if (!is_dir( $dir)) {
mkdir( $dir);
}
if (!file_exists( $dir . 'Table.php')) {
file_put_contents( $dir . 'Table.php', 'load( $this->_config['yaml_schema_path'] . DIRECTORY_SEPARATOR . 'schema.yml', 'yml');
foreach ($result as $class => $data) {
require_once $this->_config ['models_path'] . DIRECTORY_SEPARATOR . $class . '.php';
$rClass = new ReflectionClass( $class);
foreach ($data ['columns'] as $column => $options) {
$methods = $this->_buildMethodName( $column);
foreach ($methods as $k => $name) {
if (! $rClass->hasMethod( $name)) {
$this->_addMethod( $class, $name, $column, $k, $options ['type']);
}
}
}
$this->_fixParents( $class);
$this->_createTableClass( $class);
}
}
/**
* Fixes parent for base classes from Doctrine_Record to Model_Base_Record
*
* @param string $class - original class name
* @return void
*/
protected function _fixParents($class) {
$dir = $this->_config['models_path'] . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR;
$baseClass = 'Base' . $class;
if (file_exists( $dir . $baseClass . '.php')) {
$content = file_get_contents( $dir . $baseClass . '.php');
$content = preg_replace( '/extends\s+Doctrine_Record\s+{/is', 'extends Model_Base_Record {', $content);
file_put_contents( $dir . $baseClass . '.php', $content);
}
}
/**
* Creates table classes if they have not been already exist
*
* @param string $class - original class name
* @return void
*/
protected function _createTableClass( $class) {
$dir = $this->_config['models_path'] . DIRECTORY_SEPARATOR . 'Tables' . DIRECTORY_SEPARATOR;
if (!is_dir( $dir)) {
mkdir( $dir);
}
$tblClass = $class . 'Table';
if (! file_exists( $dir . $tblClass . '.php')) {
$content = "_config ['models_path'] . DIRECTORY_SEPARATOR . $class . '.php');
$propType = $this->_type2php( $propertyType);
if ($methodType == 'get') {
$comment = "Returns a value of '$propertyName' field";
$args = '';
$implementation = "return \$this->$propertyName;";
$prms = ' void';
$rets = "$propType \$$propertyName $propertyType";
} elseif ($methodType == 'set') {
$comment = "Sets '$propertyName' field to a given value";
$args = ' $' . $propertyName;
$implementation = '$this->' . $propertyName . ' = $' . $propertyName . ';
return $this;';
$prms = $args;
$rets = $class;
} else {
return;
}
$addCode = " /**
* $comment
*
* @param $prms
* @return $rets
*/
public function $methodName($args) {
$implementation
}
";
$content = preg_replace( '/(class\s+' . preg_quote( $class) . '\s+.*?\{.*?)(\})([^}]*)$/is', '$1' . $addCode . '$2$3', $content);
file_put_contents( $this->_config['models_path'] . DIRECTORY_SEPARATOR . $class . '.php', $content);
}
/**
* Returns PHP type from YAML definition type
*
* @param string $type - YAML type
* @return string PHP type
*/
protected function _type2php( $type) {
$type = explode ( '(', $type );
$type = $type [0];
$types = array(
'boolean' => 'bool',
'integer' => 'int',
'float' => 'float',
'decimal' => 'float',
'string' => 'string',
'array' => 'array',
'object' => 'string',
'blob' => 'string',
'clob' => 'string',
'timestamp' => 'string',
'time' => 'string',
'date' => 'string',
'enum' => 'string',
'gzip' => 'string'
);
return $types[$type];
}
/**
* Builds method names from a property name
*
* @param string $column_name - original property name
* @return array
*/
protected function _buildMethodName($column_name) {
$method = preg_split( '/_+/', $column_name, - 1, PREG_SPLIT_NO_EMPTY);
foreach ($method as $k => $part) {
$method [$k] = ucfirst( $part);
}
$method = join( '', $method);
$return = array(
'get' => "get$method",
'set' => "set$method"
);
return $return;
}
/**
* Fixes group permissions for generated files
*
* @param void
* @return void
*/
protected function _chmod() {
$cmd = 'chmod -R g+w ' . MODELS_PATH;
echo `$cmd`;
}
}
?>И положим его в папку bin/sandbix/lib/.
Отлично, наша дополнительная функциональность готова. Что она нам дает:
- Автоматически создает базовые классы для объектов таблиц и записей, которые вы можете править руками (вы ведь не захотите править Doctrine_Table и Doctrine_Record, не так ли?). Это полезно, если вы захотите расширять их функциональность. Например, вы можете реализовать логгирование всех изменений записей в таблицах БД — и это именно то место.
- Автоматически создает все необходимые классы таблиц, которые наследуются от созданного нами базового класса.
- Автоматически добавляет методы getProperty() и setProperty( $property) для всех свойств классов записей. Теперь у вас будут работать автокомплиты, если вы используете при разработке Zend Studio, а также сможете расширять функциональность методов доступа к свойствам класса как сами того пожелаете.
Как видите, такое несложное решение значительно улучшает гибкость каркаса вашего приложения, а также не препятствует обновлению самих библиотек.
Теперь заставим Sandbox работать с нашим клиентом. Подправим файл bin/sandbox/doctrine.php:
<?php
require_once('config.php');
require_once 'SandboxCli.php';
// Configure Doctrine Cli
// Normally these are arguments to the cli tasks but if they are set here the arguments will be auto-filled
$config = array('data_fixtures_path' => DATA_FIXTURES_PATH,
'models_path' => MODELS_PATH,
'migrations_path' => MIGRATIONS_PATH,
'sql_path' => SQL_PATH,
'yaml_schema_path' => YAML_SCHEMA_PATH);
$cli = new SandboxCli( $config);
$cli->run( $_SERVER['argv']);
?>Вуаля! Можем испытать. Создайте в вашей базе данных несколько связанных таблиц, например, таких:

И запустим комманды:
./doctrine generate-yaml-db ./doctrine generate-models-yaml
В дальнейшем можно пользоваться второй командой для обновления вашей модели.
Проверьте, созданы ли все необходимые файлы в папке library/Model/.
Тюнингуем Zend Framework для работы с новой моделью
В первую очередь, создадим папку application/default/run/ и в ней файл bootstrap.php, и перенесем в него содержимое файла html/index.php. А в файле html/index.php напишем:
require '..' . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . 'run' . DIRECTORY_SEPARATOR . 'bootstrap.php';
Это сделает невозможным просмотр кода, даже если произойдет сбой в работе веб-сервера. В худшем случае будет видно только подключение другого файла.
Теперь внесем необходимые изменения в наш bootstrap.php, он должен выглядеть примерно следующим образом:
<?php
setAttribute( Doctrine::ATTR_AUTOLOAD_TABLE_CLASSES, true);
/**
* Turn all Doctrine validators on
*/
Doctrine_Manager::getInstance()->setAttribute( Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
/**
* Setup Doctrine connection
*/
Doctrine_Manager::connection( 'mysql://root:123@localhost/mydbname');
/**
* Set the model loading to conservative/lazy loading
*/
Doctrine_Manager::getInstance()->setAttribute( 'model_loading', 'conservative');
/**
* Load the models for the autoloader
*/
Doctrine::loadModels( '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'Model');
/**
* Setup controller
*/
$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory( '../application/default/controllers');
$controller->throwExceptions( true); // should be turned on in development time
/**
* bootstrap layouts
*/
Zend_Layout::startMvc( array(
'layoutPath' => '../application/default/layouts',
'layout' => 'main'
));
/**
* Run front controller
*/
$controller->dispatch();
?>Все, мы скрестили двух «зверей». теперь можем попробовать нашу модель в действии, например, в application/default/controllers/IndexController.php:
<?php
public function indexAction() {
$artist = new Artist();
$artist->setName( 'DDT')
->setDescription( 'Very cool russian rock-band')
->save();
$artist = Doctrine::getTable( 'Artist')->find( 1);
echo '<pre>';
print_r( $artist);
echo '</pre>';
}
?>Вы можете скачать полный пример в исходных кодах (4,53 Мб)
P. S. Кросс-пост с блога автора: mikhailstadnik.com/tuning-zf-with-doctrine
