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. Далее действуем так.

  1. Скачиваем свежий дистрибутив Doctrine-x.x.x-Sandbox.tgz с официального сайта.
  2. Содержимое папки lib/ из архива копируем в папку library/ нашего проекта.
  3. Создаем в корне нашего проекта еще одну папку 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' =&gt; "get$method",
			'set' =&gt; "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 Мб)

Удачи в девелопменте!

Хорошая статьяПлохая статья +3
   |    Опубликовано: Декабрь, 3 2008г.    |    Автор: Михаил Стадник

Комментарии (4) к статье “Тюнинг Zend Framework и Doctrine”

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