Integrating Propel with the Zend Framework

April 25, 2006

Tutorials, Zend Framework

It is very easy to integrate other tools and components into the
Zend Framework. I have already shown, how to
integrate Smarty as a template
engine and the ez Components
to expand the selection of useful components to build your own framework based on
the Zend Framework.

Since the Zend Framework is currently (Preview Version 0.1.3) lacking an ORM-Layer
(Object Relational Mapping), I want to show you how to integrate
Propel. Propel allows you to access your database
using a set of objects, providing a simple API for storing and querying data. So Propel
can easily take over the model part in a MVC system. For detailled information on Propel,
its dependencies on Phing and
Creole and how to install it please refer to the
Propel website and the
User guide.

Directory structure for our libraries

First you need to set up the directory structure for our libraries. Besides the Zend
Framework we have added the ez Components, Smarty and Propel to our library. Propel
depends on Phing and Creole so we need to add them as well.

  /_library
    /creole
    /ezComponents
    /phing
    /propel
      /generator
      /runtime
    /Smarty
    /ZendFramework
      /Mycompany
      /Zend
      Zend.php

Below the /_library/ZendFramework we have the basic directory
Zend and a custom directory Mycompany for our custom classes
which extend the Zend Framework classes. Propel basically exists of two parts: generator
and runtime engine. Both libraries are located below the /_library/propel
directory.

Directory structure for our application

Our application also needs some structure. We need to place our controllers, views and
models, as well as our website root, some build files and our test files.

  /project
    /application
      /config
      /controller
      /model
      /view
    /build
    /www
      /view
      /website
        /files
          /css
          /js
        /img
    /tests

Our /project/application directory has four subdirectories for the
configuration, controller, model and view files. In /project/application/model
we will place the files generated by Propel later on. The /project/build
directory will hold the files Propel needs for building. The
/project/www/website will be the document root of the webserver where we
place CSS and Javascript files, our images and the
bootstrap file.

Add paths to include_path

To finish our setup we need to add the following paths to our include_path.

  /_library/creole/classes
  /_library/ezComponents
  /_library/phing/classes
  /_library/propel/runtime/classes
  /_library/propel/generator/classes
  /_library/Smarty
  /_library/ZendFramework
  /project/application/config

Creating your Propel files

For the Propel build process you need a couple of files which should be located in
the /project/build directory. Since this article is not meant to be an
introduction to Propel please refer to the Propel User Guide.

The most important file is the schema.xml which defines the structure
of your database. Here is an example for a very simple CMS with just three tables
cms_article, cms_category and user_main:

<?xml version="1.0" encoding="UTF-8"?>
<database name="mycms" defaultIdMethod="native">
  <table name="cms_article">
    <column name="art_id" type="INTEGER" primaryKey="true" required="true" autoIncrement="true"/>
    <column name="art_user_id" type="INTEGER" required="true"/>
    <column name="art_cat_id" type="TINYINT" required="true"/>
    <column name="art_title" type="VARCHAR" size="64" required="true"/>
    <column name="art_text" type="LONGVARCHAR" required="true"/>
    <foreign-key foreignTable="cms_category" name="category2article">
      <reference local="art_cat_id" foreign="cat_id"/>
    </foreign-key>
    <foreign-key foreignTable="user_main" name="user2article">
      <reference local="art_user_id" foreign="user_id"/>
    </foreign-key>
  </table>
  <table name="cms_category">
    <column name="cat_id" type="TINYINT" primaryKey="true" required="true" autoIncrement="true"/>
    <column name="cat_name" type="VARCHAR" size="32" required="true"/>
  </table>
  <table name="user_main">
    <column name="user_id" type="INTEGER" primaryKey="true" required="true" autoIncrement="true"/>
    <column name="user_name" type="VARCHAR" size="32" required="true"/>
    <column name="user_pass" type="VARCHAR" size="32" required="true"/>
    <column name="user_email" type="VARCHAR" size="64" required="true"/>
  </table>
</database>

Next, we need the runtime-conf.xml file which holds the access data to
your database and some logging information. Here is a basic example:

<?xml version="1.0" encoding="UTF-8"?>
<config>
 <log>
  <ident>propel-mycms</ident>
  <level>7</level>
 </log>
 <propel>
  <datasources default="mycms">  
   <datasource id="mycms">
    <adapter>mysql</adapter>    
    <connection>
     <phptype>mysql</phptype>
     <hostspec>localhost</hostspec>
     <database>mycms</database>
     <username>root</username>
     <password>very secret</password>
    </connection>    
   </datasource>   
  </datasources> 
 </propel>
</config>

Finally, we need a build.properties which holds all properties for the
Propel build process. Here is an example with some comments inside.

# set the paths to the Propel installation, your project home and your
# build files
propel.home = full/path/to/_library/propel/generator
project.home = full/path/to/project
project.build = ${project.home}/build

# set some basic properties for the project and the database connection
propel.project = mycms
propel.database = mysql
propel.targetPackage = 
propel.database.url = mysql://root:@localhost/mycms
propel.mysql.tableType = InnoDB

# set the directories for the schema.xml and the runtime-conf.xml files 
# and the path to the template files that Propel uses
propel.schema.dir = ${project.build}
propel.conf.dir = ${project.build}
propel.templatePath = ${propel.home}/templates

# set the directories for the generated output, i.e. the data object classes, a
# PHP file with the configuration data and the SQL files
propel.output.dir = ${project.home}
propel.php.dir = ${propel.output.dir}/application/model
propel.phpconf.dir = ${propel.output.dir}/application/config
propel.sql.dir = ${project.build}/sql

# set the name for the configuration file
propel.runtime.phpconf.file = propel-config.php

Start the Propel build process

To build the data object classes you need change to the directory where you have
installed Propel and run the Propel build.

# Linux/Unix
$> cd /usr/local/propel/generator
$> phing -Dproject.dir=full/path/to/project/build -Dproject=mycms

# Windows
C:\> cd C:\path\to\propel\generator
C:\path\to\propel\generator> phing -Dproject.dir=c:\path\to\project\build -Dproject=mycms

When you start the Propel build, Phing will look for the build.properties
file in your /project/build directory. The schema.xml will be
read and the data object classes be created and saved in the directory you defined in
the propel.php.dir property. Also the SQL files for creating the database
and the configuration file will be created and saved in the defined directories.

If you look into your /project/application/model directory you will
notice that for each defined table in schema.xml two classes were created
in. These classes are stubs and all your changes to the data object classes should be
placed here.

In the subdirectory /project/application/model/om you will
find the base classes which will be recreated in each Propel build. So, if you amend your
schema.xml file and restart the Propel build, these files will be
overwritten while the files in /project/application/model will be kept
unchanged.

Use the Propel classes as your model classes

Now, finally we want to use the Propel built data object classes as our model classes
in the Zend Framework. Since we have already set up the include_path we can directly
include them in our Controller classes.

    public function indexAction()
    {
        $temp_file = 'list.htm';
        
        $view = Zend::registry('view');
        
        if (false === $view->isCached($temp_file))
        {
            require_once 'propel/Propel.php';
            Propel::init('propel-config.php');
            include_once 'propel/util/Criteria.php';
            include_once 'CmsArticlePeer.php';
            
            $c = new Criteria();
            $c->setLimit(10);
            
            $count = CmsArticlePeer::doCount($c);
            $list  = CmsArticlePeer::doSelect($c);
            
            $articles = array();
            
            foreach($list as $article) 
            {
                $row = array();
                $row['title'   ] = $article->getArtTitle();
                $row['text'    ] = $article->getArtText();
                $row['user'    ] = $article->getUserMain()->getUserName();
                $row['category'] = $article->getCmsCategory()->getCatName();
                
                $articles[] = $row;
            }
            
            $articles = $view->escape($articles);
            
            $view->assign('articles', $articles);
        }
        
        $view->output($temp_file);
    }

Please note: This example uses the Smarty integration which was
described in the former articles.

The usage of Propel data object classes is quite simple. In this example we select a
list of articles, loop through it and assign the data to the template. If you want to
retrieve an object by a given primary key, just use a
$article = CmsArticlePeer::retrieveByPK(1) function call.

Since I like to keep my action methods as simple and short as possible, the
including stuff for Propel is annoying me a bit. To solve this, we can amend the
__autoload() function.

Amend __autoload() for Propel

I have formerly set up my __autoload() function to load all Zend
classes and ez Components whenever they are used. To add autoloading for Propel we can
amend it like this:

function __autoload($class)
{
    /**
     * autoload Propel classes
     */
    if ('Propel' == $class)
    {
        require_once 'propel/Propel.php';
        Propel::init('propel-config.php');
    }
    elseif ('Criteria' == $class)
    {
        include_once 'propel/util/Criteria.php';
    }
    elseif ('Cms' == substr($class, 0, 3) or 'User' == substr($class, 0, 4))
    {
        include_once($class . '.php');
    }
    /**
     * autoload ezComponents classes
     */
    elseif ('ezcBase' == $class)
    {
        require_once 'Base/src/base.php';
    }
    elseif ('ezc' == substr($class, 0, 3))
    {
        ezcBase::autoload($class);
    }
    /**
     * autoload Zend Framework classes
     */
    else
    {
        Zend::loadClass($class);
    }
}

The first if-statement loads and initializes the Propel class, whenever it is
requested. The second if-statement makes sure that the Criteria class is loaded,
whenever you need it. The third if-statement makes sure that your individual data object
classes are autoloaded. Since we use a prefix of either “Cms” or “User” it can be
identified by these prefixes. But what happens, if we have more than two table prefixes?
We would need to add all of them to this if-statement.

Since this is very suboptimal we need a way to tell Propel to automatically add a
custom prefix to all the data object classes which are generated.

Amend the Propel build process

We are looking for a build property to add a prefix to all generated classes, but
unfortunately there is none currently (version 1.2.0RC1). What we can find is a
propel.basePrefix property to add a prefix to all the base classes. But how
can we set up a new propel.stubPrefix property to be used by Propel?

Have a closer look at the properties propel.builder.peer.class,
propel.builder.object.class, propel.builder.objectstub.class
and propel.peerstub.peer.class which are set to default classes in the
default.properties file in the /_library/propel/generator
path.

All we need to do is to create our own builder classes and extend the four default
classes PHP5ComplexObjectBuilder.php, PHP5ComplexPeerBuilder.php,
PHP5ExtensionObjectBuilder.php and PHP5ExtensionPeerBuilder.php.
We name our classes MyComplexObjectBuilder.php and so on and place them in a
subdirectory /project/build/om. In all these four classe we only need to
write getClassname() method.

// getClassname() method in MyExtensionObjectBuilder.php
public function getClassname()
{
    return $this->getBuildProperty('stubPrefix') . $this->getTable()->getPhpName();
}

// getClassname() method in MyExtensionPeerBuilder.php
public function getClassname()
{
    return $this->getBuildProperty('stubPrefix') . $this->getTable()->getPhpName() . 'Peer';
}

// getClassname() method in MyComplexObjectBuilder.php
public function getClassname()
{
    return $this->getBuildProperty('basePrefix') . $this->getTable()->getPhpName();
}

// getClassname() method in MyComplexPeerBuilder.php
public function getClassname()
{
    return $this->getBuildProperty('basePrefix') . $this->getTable()->getPhpName() . 'Peer';
}

To make sure that our new classes are used we need to amend our build.properties
file. So we add the following properties to this file.

# set our new stubPrefix property
propel.stubPrefix = Dao

# set the builder classes
propel.builder.peer.class = path.to.project.build.om.MyComplexPeerBuilder
propel.builder.object.class = path.to.project.build.om.MyComplexObjectBuilder
propel.builder.objectstub.class = path.to.project.build.om.MyExtensionObjectBuilder 
propel.builder.peerstub.class = path.to.project.build.om.MyExtensionPeerBuilder

Before we restart the Propel build we should delete all the classes from the
/project/application/model directory. After the Propel build we will find
our data object classes be prefixed with Dao.

The last step is to amend the __autoload() function.

    [...]
    elseif ('Dao' == substr($class, 0, 4))
    {
        include_once($class . '.php');
    }
    [...]

Conclusion

To integrate Propel to the Zend Framework is a bit more complicated than to integrate
Smarty or the ez Components. Nevertheless, it is feasible. If you don’t mind to bloat
your action methods with the include-stuff for Propel you can forget about amending
your __autoload() function and the Propel build process. If you are like me and want to
keep your controllers as simple as possible you should do the extra work.

Any comments regarding the Propel integration into the Zend Framework are welcome. If
you have some detail questions regarding Propel please refer to the
Propel Website or the
Propel mailing lists.

15 Responses to “Integrating Propel with the Zend Framework”

  1. kanian Says:

    Sorry for the typos and sending the same message three times (!?).

    I wrote an <a href=’http://kanian77.wordpress.com/2007/11/05/how-do-i-integrate-propel-with-zend/’>article </a>where I discuss how a plugin can be used to integrate propel, or any third party application for that matter, to Zend. I think that there are some advantages to such an approach. For example, being able to trigger business logic related to other entity in the model based on the name of the model being accessed.

  2. kanian Says:

    I wrote an <a href=’http://kanian77.wordpress.com/2007/11/05/how-do-i-integrate-propel-with-zend/>article </a>where I discuss how a plugin can be used to integrate propel, or any third party application for that matter, to Zend. I think that there are some advantages to such an approach. For example, being able to trigger business logic related to other entity in the model based on the name of the model being accessed.

  3. kanian Says:

    I wrote an article where I discuss how a plugin can be used to integrate propel, or any third party application for that matter, to Zend. I think that there are some advantages to such an approach. For example, being able to trigger business logic related to other entity in the model based on the name of the model being accessed.

  4. ckassab Says:

    Why the need for complex autoloaders if its so simple to have a classname/pathname convention?!

    If everyone would stick to this we wouldn’t have so much of an overhead trying to integrate other packages and libraries!

    E.g. MyApp_Class_Name would map to MyApp/Class/Name.php

    Can it get any easier?

  5. zYne-- Says:

    Why not using Doctrine. It is an ActiveRecord based ORM solution (no XML configuration – configured in runtime!). It has many features missing from Propel like object population through rawSql (with even arbitrary joins), specialized object query language called DQL as well as many other features.

    Nice feature list:

    http://swik.net/Doctrine

    svn:

    http://doctrine.pengus.net/svn

    homesite:

    http://www.phpdoctrine.com

  6. jeromejtk Says:

    Your not the only one that might have a tendency to pick on Propel, I used to use it but I’ve since stopped due to it being complicated, and slow to update. I got pretty fed up when I needed to add a column to a table and had to go through the whole generation process. I’m always looking to improve the development process and propel was just slowing it down, even though having it manage relations and validations was handy, I opt for a simpler solution. I wrote my own much simpler system which is completely dynamic and flexible, just lacks much power in comparison.

    I’m more interested in things similar to the Ruby on Rails style of development with it’s convention over configuration. CakePHP and such aren’t bad but not sure yet if they fit with our setup yet.

  7. _____anonymous_____ Says:

    Nice article and all but I have never been able to understand just what propel really brings to the table.

    As far as basic CRUD goes, a simple set of classes like Zend_Db_Table/Row work fine. Propel can do validation but it’s really just as easy to add a list of validators to the Zend table class as it is to add them to the schema.xml file.

    The relationship management is where you might think Propel offers significat value but I’m not so sure.

    Consider this line:
    $row['user'] = $article->getUserMain()->getUserName();

    To get the user name requires your action understands enough about the database schema to know that you first need to get the main user then ask it for the name. These sorts of details are supposed to be hidden inside of the model layer. What if I want to change the way articles and users are related? I’d probably have to go into my action classes and readjust the code.

    What we really want is this:
    $row['user'] = $article->getMainUserName();

    Now you might say that this is not a problem. I could just add getMainUserName() to the article row class. But if I need to start adding methods just to keep model things in the model layer then why not just use the simple Zend classes?

    A further objection is that $article->getUserMain() will kick off a new query for each article. Now one should avoid premature optimization but having a bunch of little queries getting kicked off everywhere should be avoided. Propel does attempt to address this problem by adding custom join methods to the peer class but once you start having multiple joins then things break down. And in any event, your action again would need to know exactly which join to kick off.

    And as a minor point, the entire user record will always be read in even though all you needed was the name.

    What it boils down to is that even this simple example really requires writing a custom query by hand. Which in turn makes most of the Propel runtime enviroment of little value.

    Understand that I am not trying to pick on Propel here. It’s probably the best PHP ORM system out there. But I just can’t seem to see the value for php web based applications.

  8. frille Says:

    Hi,

    what exactly does not work? The setup? The build process? Retrieving records? If you would explain your problem in more detail, I might be able to help you to solve this problem.

    Best Regards,
    Ralf

  9. _____anonymous_____ Says:

    It simply does not work. Please try it yourself on a clean system. The current propel documentation seems outdated or their packages broken or whatever.

    Please do not refer to their documentation but describe all required tasks to make this a real good tutorial.

  10. frille Says:

    Hi ufux,

    > – It’s hard to use resultsets without populating them to data objects.
    > But most of the time, you just need the simple key=>value sets.

    Just for that issue I extended the PHP5ComplexObjectBuilder and PHP5ComplexPeerBuilder classes to create some new methods for selecting the data into an array of array data not an array with objects. So now I only need to do something like this to get an array:

    $c = new Criteria();
    $c->setLimit(10);
    $articles = DobCmsArticlePeer::doSelectArray($c);

    Now I only need to escape the $articles array and pass it to the View for output.

    For a single data row, I can use the following:

    $article = DobCmsArticlePeer::retrieveByPK(1);
    $data = $article->getColumnArray();

    If you take some time to digg into extending the class building classes PHP5ComplexObjectBuilder and PHP5ComplexPeerBuilder, it is quite handy to amend them in every way you want to. If you are interested in this maybe I find the time to write down what I did.

    HTH, Ralf

  11. frille Says:

    Hi Styles722,

    I also do not think that a singleton will help here. Just imagine if you need two article objects in the same script, you do not want a singleton there. The __autoload() function is just for keeping the controllers free of lots of include_once() lines and just to load a class when it is really needed. If you don’t like it you can easily leave the Propel loading out of the __autoload() function and include the Propel files in your controller whenever you need them.

    HTH, Ralf

  12. ufux Says:

    Creating singleton objects won’t be very useful because; to get an array of populated data objects, you must use static methods of Peer subclasses.

    I’am using propel for a while and it seems well designed and stable. But I think it has a few setbacks:

    – Criterias are not easy to use and they can really slow down development process.
    – It’s hard to use resultsets without populating them to data objects. But most of the time, you just need the simple key=>value sets.

  13. Styles722 Says:

    Hi Ralf,

    Instead of including the table classes whenever we need to use them what about creating singleton objects for each class? -SF

  14. frille Says:

    Yes, that is true because the ez Components depend on PHP 5.1.x.

    HTH, Ralf

  15. roderik1 Says:

    Am i right to assume that by using ezComponents in an application you raise the PHP 5 requirements from 5.0.x to 5.1.x?