Tuning Zend Framework + Doctrine

We cross two "animals"



Basically, crossing Zend Framework with Doctrine is not that difficult. But first, let's talk about the preparatory work. According to the author, the default file structure of the Zend Framework project can be made slightly more optimal.

This is how the default Zend Framework project file structure looks like:

/
  application/
    default/
      controllers/
      layouts/
      models/
      views/
  html/
  library/


It often happens that you will have several applications (for example, frontend / and backend /), and you will use the same model. In this case, it would be wise to move your models / to the library / folder, in this case the new structure would look like this:

/
  application/
    default/
      controllers/
      layouts/
      views/
  html/
  library/
    Model/


In addition, as you can see, the models / folder has been renamed to Model. Further we act as follows.

  1. Download the latest Doctrine-xxx-Sandbox.tgz distribution from the official website .
  2. The contents of the lib / folder from the archive are copied to the library / folder of our project.
  3. We create another bin / sandbox / folder in the root of our project and copy the rest of the archive contents into it (except for the models / folder and the index.php file - we do not need them).


Our project files should now look something like this:

/
  application/
    default/
      controllers/
      layouts/
      views/
  bin/
    sandbox/
      data/
      lib/
      migrations/
      schema/
      config.php
      doctrine
      doctrine.php
  html/
  library/
    Doctrine/
    Model/
    Doctrine.php


We clear the bin / sandbox / lib / folder from the contents - the library is now in a different place.

It's time to configure Doctrine to work within the new file structure.

Change the value of the MODELS_PATH constant in the bin / sandbox / config.php file to:

SANDBOX_PATH . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'Model'


Next, change the database connection settings. Change the value of the DSN constant to what is needed. For example, if you are using a MySQL DBMS, the DSN might look like this:

'mysql://root:123@localhost/mydbname'


We will configure include_paths as the first term in the config so that our scripts can find files in new places:

set_include_path( '.' . PATH_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . PATH_SEPARATOR . '.' . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path());


Next, connect the main file of the Doctrine library, immediately after installing the paths, and install the autoload function:

<?php
require_once 'Doctrine.php';

/**
 * Setup autoload function
 */
spl_autoload_register( array(
    'Doctrine',
    'autoload'
));
?>


That is, in general, our config should look something like this:

<?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');
?>


Now we will focus on a very interesting point.

The fact is that Doctrine does not generate set () and get () methods for object properties, but uses the __get () and __set () automatic methods. And since the properties themselves are hidden within the framework of one property of the parent class, no development environment will ever tell you them in the autocomplete. But this is just an inconvenience from which we can easily get rid of, and plus to this still get some additional conveniences. Now we will demonstrate how to do this.

Tuning Doctrine Sandbox



The Doctrine console application comes with the Doctrine_Cli class, which, in fact, implements its functionality. We will inherit it and expand this functionality as follows. Create your own SandboxCli class:

<?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`;
	}

}
?>


And put it in the bin / sandbix / lib / folder.

Great, our extra functionality is ready. What does she give us:

  • Automatically creates base classes for table and record objects that you can edit by hand (you don’t want to edit Doctrine_Table and Doctrine_Record, right?). This is useful if you want to expand their functionality. For example, you can implement logging of all changes to records in the database tables - and this is exactly the place.
  • Automatically creates all the necessary table classes that inherit from the base class we created.
  • Automatically adds getProperty () and setProperty ($ property) methods for all properties of record classes. Now autocomplits will work for you, if you use Zend Studio during development, and you can also expand the functionality of the methods for accessing class properties as you wish.

As you can see, such a simple solution significantly improves the flexibility of the framework of your application, and also does not prevent the libraries themselves from being updated.

Now let's get Sandbox to work with our client. Let's fix the bin / sandbox / doctrine.php file:

<?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']);
?>


Voila! We can experience it. Create several related tables in your database, for example, these:
And run the commands:

./doctrine generate-yaml-db
./doctrine generate-models-yaml


In the future, you can use the second command to update your model.

Check if all the necessary files have been created in the library / Model / folder.

Tuning the Zend Framework to work with the new model



First of all, create the application / default / run / folder and the bootstrap.php file in it, and transfer the contents of the html / index.php file to it. And in the html / index.php file we write:

require '..' . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . 'run' . DIRECTORY_SEPARATOR . 'bootstrap.php';


This will make it impossible to view the code, even if the web server crashes. In the worst case, only the connection of another file will be visible.

Now make the necessary changes to our bootstrap.php, it should look something like this:

<?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();
?>


That's all, we crossed two "animals". теперь можем попробовать нашу модель в действии, например, в 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>';
}
?>


You can download the full example in the source code (4.53 Mb)
PS Cross-post from the author’s blog: mikhailstadnik.com/tuning-zf-with-doctrine