Zend Framework Doctrine Model Autoloader

There have been several tutorials outlining how to autoload Doctrine Models using the Zend_Loader_Autoloader. However, none of these have permitted Zend / PEAR style naming conventions for models. I prefer to use these conventions because, although it makes my model names longer, the “name-spacing” gives a certain degree of organization and order to the application.

For example, lets say we have two classes (and two tables) in our database. The first class is a very simple person model. A person has an Id, which is used at the primary key, and a name. This person class would be described in YAML notation as follows:

App_Model_Person:
 tableName: T_person
 columns:
  id: integer
  name: string(64)

People have addresses. One person may have more than one address (billing, mailing, physical, for example). Therefore, we need a second, person address table similar to the following:

App_Model_Person_Address:
 tableName: T_person_address
 package: Person
 columns:
  person_id: integer
  address: string

Notice that the App_Model_Person_Addres model is in the Person package. This is done to ensure that it will be placed in the person subfolder, so the final path of this class will be App/Model/Person/App_Model_Person_Address.php

Before, using the doctrine command line tool to generate the classes, we must make sure that we are going to generate the models in the appropriate location. Assuming that we have an application library on the include path (eg: library/ is on the include path and it has a folder App in it), then we want Doctrine generating the models in the library/App/Model folder. This can be done by setting the models_path to the proper location. Eg:

new Doctrine_Cli(array(
	...,
	'models_path'         =>  APPLICATION_PATH . '/library/App/Model',
	...
));

The main problem with the auto-generated classes produced by the Doctrine\_Cli is the filename of the php file that is generated. For, example, the full path of the generated App_Model_Person class will be library/App/Model/App_Model_Person.php; however, according to Zend conventions it should be library/App/Model/Person.php.

The Doctrine Model Autoloader provided below attempts to solve these problems by detecting when a Doctrine Model is being loaded, and then generating the appropriate path (with the full class name acting as the filename). To complicate matters, there are some special cases for base-classes and package classes as well, specifically that they may have Base or Package prefixing the name of the class. The Model autoloader addresses this by detecting the presence of these terms using regular expressions and tweaking the generation of the paths accordingly.

It is very simple to use the Doctrine Model Autoloader class. Simply instantiate it, passing it the prefix of the Doctrine Model namespace. In this case it would be App_Model_. Don’t forget the trailing underscore (“_”), however the class should detect that its missing and add it in for you if you do.

An example usage for the above case would be simply:

$doctrineAutoloader = new Zext_Loader_Autoloader_DoctrineModel('App_Model_');

Upon instantiation, the autoloader registers itself with the Zend_Loader_Autoloader for the passed namepace, so no further setup is required.

<?php

class Zext_Loader_Autoloader_DoctrineModel implements Zend_Loader_Autoloader_Interface
{
    /**
     * The namespace of the models
     * @var string
     */
    protected $_namespace;

    /**
     * Whether or not to suppress file not found warnings
     * @var bool
     */
    protected $_suppressNotFoundWarnings = true;

    /**
     *
     * @param string $modelNamespace    The namespace of the model to load. Eg: 'Zext_Model_'
     * @param boolean $quiet            If this is true, suppresses warnings if the file isn't found
     */
    public function __construct($modelNamespace, $quiet=true)
    {
        if(strrpos($modelNamespace, '_') + 1 != strlen($modelNamespace)){
            $modelNamespace .= '_';
        }

        $this->_namespace = $modelNamespace;
        $this->_suppressNotFoundWarnings = $quiet;

        $this->init();
    }

    /**
     * Initializes Zend_Loader_Autoloader, pushing the doctrine autoloader onto the autoloading stack
     * @return void
     */
    protected function _init()
    {
        $namespaces = array(
            'Package' . $this->_namespace,
            'Base' . $this->_namespace,
            $this->_namespace
        );

        $autoloader = Zend_Loader_Autoloader::getInstance();

        foreach($namespaces as $namespace){
            $autoloader->pushAutoloader($this, $namespace);
        }
    }

    /**
     * (non-PHPdoc)
     * @see tests/library/Zend/Loader/Autoloader/Zend_Loader_Autoloader_Interface#autoload($class)
     */
    public function autoload($className)
    {
        if (class_exists($className, false) || interface_exists($className, false)) {
            return true;
        }


        //Check to see if we are loading a package / base
        if (preg_match('/(?P<prefix>.+)' . $this->_namespace  . '(?P<class>.+)/mx', $className, $matches)) {
            $result = $matches['prefix'];
            switch(strtolower($matches['prefix'])){
                case 'package':
                    $classPath = $this->_loadPackage($matches['class']);
                break;

                case 'base':
                    $classPath = $this->_loadBase($matches['class']);
                break;
            }
        }else{
            $classPath = $this->_getPath(
                substr($className, , strrpos($className, '_') + 1),
                $className
            );
        }


        if($this->_suppressNotFoundWarnings){
            @Zend_Loader::loadFile($classPath, null, true);
        }else{
            Zend_Loader::loadFile($classPath, null, true);
        }

        if(!class_exists($className, false) && !interface_exists($className, false)){
            return false;
        }

        return true;
    }

    /**
     * Gets the correct path for loading packages
     *
     * @param $className
     * @return string
     */
    protected function _loadPackage($className)
    {
        $package = substr($className, , strpos($className, '_'));

        return $this->_getPath(
            $this->_namespace . 'packages_' . $package . '_',
            'Package' . $this->_namespace . $className
        );
    }

    /**
     * Gets the correct path for loading bases
     *
     * @param $className
     * @return string
     */
    protected function _loadBase($className)
    {
        $package = substr($className, , strpos($className, '_'));

        return $this->_getPath(
            $this->_namespace . $package . '_generated_',
            'Base' . $this->_namespace . $className
        );
    }

    /**
     * Gets the classpath from an _ separated path and the classname
     *
     * @param string $path
     * @param string $name
     * @return string
     */
    protected function _getPath($path, $name)
    {
        return str_replace('_', DIRECTORY_SEPARATOR, $path) . $name . '.php';
    }
}