Скрещиваем двух «зверей»
В принципе, скрестить 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