Archives
Visitors
  • 45372This month:
  • 824Today:
  • 12Currently online:



LeaseWeb CDN

Tuning Zend framework and Doctrine

In principle, the combination of Zend Framework with Doctrine is not too difficult. But first let’s talk about the preparations. According to the author of Zend Framework, the default file structure of project can be a bit more optimal.

Here is the default structure of the Zend Framework project files:


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

It can often be that you will have a number of applications (e.g., frontend and backend ), and you want to use the same model for them. In this case, it can be a good practice to create your models folder in library/, in which case the new structure would look as follows:

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

In addition, the folder models/ is renamed to Model. We now proceed as follows:

  1. Download a fresh copy of Doctrine-xxx-Sandbox.tgz from the official website.
  2. Copy the contents of the lib/folder from the archive to our project library/ folder.
  3. Create another folder bin/sandbox/ in the root of our project and copy the rest of the archive there (except models/ folder and the index.php file).

Now the structure of our project should look like this:

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

Clear the content of the folder bin/sandbox/lib/ – we now have the library in another place.
Now it’s time to configure the Doctrine to work with new file structure.

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

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


Next, change the connection settings for the database. Change the value of the DSN constant to reflect your database settings. For example, if you use the DBMS MySQL, the DSN might look like this:

'mysql://root@localhost/mydbname'


Configure include_paths on the first line in the config file, so our script can find files on new locations:

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

Then connect the main Doctrine library file directly after installation paths, and set the startup function:

require_once 'Doctrine.php';

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

Now our complete configuration would look like this:

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 side effect.

The fact is that Doctrine does not generate set () and get () methods for object properties, but uses automated methods __get () and __set (). And since the properties themselves are hidden within a class property of the parent, none of the development environments will be able to suggest them in autocomplete. But this is just an inconvenience we can easily get rid of, but a plus for this to get some more extras. Now we will demonstrate how to do it.

Doctrine Sandbox tuning

In a Doctrine application a console is included in the Doctrine_Cli class, which in fact, implements its functionality. We inherit it, and extend the functionality. Let’s create a class SandboxCli:

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

}

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

Ok, our additional functionality is ready. What this gives us:

    1. Automatically create base classes for objects and tables of records that you can edit manually.This is useful if you want to expand their functionality. For example, you can implement logging of all changes to records in the tables of the database – and this is the place to implement it.
    2. Automatically create all the necessary classes for tables, which are inherited from the base class we created.
    3. Automatically add the methods getProperty() and setProperty($ property) for all classes of property records. Now your autocomplete will work if you use an IDE, and also can extend the functionality of the methods to access class properties if you wis

h.

As you can see, this simple solution significantly improves the flexibility of the framework for your application, and does not prevent the renewal of the libraries themselves.

Sandbox is now made to work with our client. We need to modify the bin/sandbox/doctrine.php file:

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

Create several related tables in your database:

Now run the commands:

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

You can use the second command to update your model in the future.

Check if all required files are created in library/Model/ folder.

Tuning Zend framework to work with the new model

First, create the application/default/run/ folder and bootstrap.php file in there, and move the content of the html/index.php file to bootstrap.php. In the html/index.php file, add the following:

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 will fail. In the worst case we will see only the include of another file.

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

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

We are done!
Now we can try our model in action, for example, in application/default/controllers/IndexController.php:

public function indexAction() {
    $artist = new Artist();
    $artist->setName( 'DDT')
             ->setDescription( 'Very cool Russian band')
             ->save();

    $artist = Doctrine::getTable( 'Artist')->find( 1);

    echo '<pre>';
    print_r( $artist);
}

Conclusion

So there you have it, a clean & simple application using both Zend Framework and Doctrine. As both follow somewhat the same filosophy, it’s possible to integrate these in a very clean way, making application building a real pleasure.  This use-at-will architecture is great: it allows us to develop true Zend Framework applications, without using the ZF database abstraction: Zend_Db. While Zend_Db isn’t a bad technology, it is still quite low-level and close to the underlying database. Using Doctrine, you can manipulate your data like objects, without worrying about the database too much.

Thank you for your attention!

This post is an adjusted and updated translation from an original version by Mikhail Stadnik. The original (Russian) post can be found at http://mikhailstadnik.com/tuning-zf-with-doctrine

2 Responses to “Tuning Zend framework and Doctrine”

Leave a Reply