PHP, Инструменты → Тюнинг Zend Framework и Doctrine
Скрещиваем двух «зверей»
В принципе, скрестить 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 на:
1 | SANDBOX_PATH . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'Model' |
Далее меняем настройки соединения с БД. Меняем значение константы DSN на то, что необходимо. Например, если вы используете СУБД MySQL, DSN может выглядеть следующим образом:
'mysql://root@localhost/mydbname'
Сконфигурируем include_paths первой срочкой в конфиге, чтобы наши скрипты могли отыскивать файлы на новых местах:
1 | set_include_path( '.' . PATH_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . PATH_SEPARATOR . '.' . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path()); |
Далее подключим основной файл библиотеки Doctrine, сразу после установки путей, и установим функцию автозагрузки:
1 2 3 4 5 6 7 8 9 | require_once 'Doctrine.php'; /** * Setup autoload function */ spl_autoload_register( array( 'Doctrine', 'autoload' )); |
Т.е., в общем, наш конфиг должен выглядеть примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | /** * 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', '<?php /** * Class Model_Base_Table - abstraction parent layer for table objects * * This class was automatically generated by Doctrine sandbox client. * All project table classed MUST be inherited from this class. * You can add an extra functionality for all tables here if * required. This class generates automatically only if not exists and * it is available for manual editing. */ abstract class Model_Base_Table extends Doctrine_Table { } '); } if (!file_exists( $dir . 'Record.php')) { file_put_contents( $dir . 'Record.php', '<?php /** * Class Model_Base_Record - abstraction parent layer for record objects. * * This class was automatically generated by Doctrine sandbox client. * All project record classed MUST be inherited from this class. * You can add an extra functionality for all records here if * required. This class generates automatically only if not exists and * it is available for manual editing. */ abstract class Model_Base_Record extends Doctrine_Record { }'); } } /** * Automatically generates getter and setter methods for hand-make classes. * This method works fine to add a new methods if new DB properties has been added. * NOTE! REMOVAL of all methods for excluded properties should be done manually * * @param void * @return void */ protected function _genSgMethods() { $yml = new Doctrine_Parser_Yml(); $result = $yml->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)) { $type = is_array ($options) ? $options['type'] : $options; $this->_addMethod ($class, $name, $column, $k, $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); } } /** * 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 = "<?php /** * This class has been auto-generated by the Doctrine ORM Framework */ class $tblClass extends Model_Base_Table { } "; file_put_contents( $dir . $tblClass . '.php', $content); } } /** * Naturally adds a method definition content to a class definition * * @param string $class - original class name * @param string $methodName - method name required to be defined * @param string $propertyName - property associated with a method * @param string $methodType - setter or getter ('set'|'get') * @param string $propertyType - type of a property obtained from YAML * @return void */ protected function _addMethod( $class, $methodName, $propertyName, $methodType, $propertyType) { $content = file_get_contents( $this->_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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 напишем:
1 | require '..' . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . 'run' . DIRECTORY_SEPARATOR . 'bootstrap.php'; |
Это сделает невозможным просмотр кода, даже если произойдет сбой в работе веб-сервера. В худшем случае будет видно только подключение другого файла.
Теперь внесем необходимые изменения в наш bootstrap.php, он должен выглядеть примерно следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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:
1 2 3 4 5 6 7 8 9 10 11 | 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); } |
Вы можете скачать полный пример в исходных кодах (4,53 Мб)
Удачи в девелопменте!

22:47:53
Хочу предложить небольшую поправку к коду.
При генерации yaml-файла схемы, в некоторых случаях свойства столбцов записываются не массивом, а строкой, в которой указан только тип поля. Например, так:
DbName:
tableName: TableName
columns:
created: integer (4)
...
В связи с чем преобразование типов полей, не всегда проходит корректно и тянет за собой сбои при добавлении методов.
Решение тривиально:
Файл: SandboxCli.php
Строка: 92 (метод _genSgMethods)
Было:
$this->_addMethod ( $class, $name, $column, $k, $options ['type']);
Стало:
$type = is_array ($options) ? $options['type'] : $options;
$this->_addMethod ($class, $name, $column, $k, $type);
Спасибо.
11:53:05
> Хочу предложить небольшую поправку к коду...
Огромное вам спасибо за действительно ценную информацию. Я обновил текст статьи, равно как и исходники для скачивания.