Pull to refresh

Тюнинг Zend Framework + Doctrine

Reading time8 min
Views2.6K

Скрещиваем двух «зверей»



В принципе, скрестить 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 на:

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
Tags:
Hubs:
+34
Comments29

Articles

Change theme settings