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.

Comments
HTH, Ralf
Instead of including the table classes whenever we need to use them what about creating singleton objects for each class? -SF
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.
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
> - 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
Please do not refer to their documentation but describe all required tasks to make this a real good tutorial.
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
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.
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.
Nice feature list:
http://swik.net/Doctrine
svn:
http://doctrine.pengus.net/svn
homesite:
www.phpdoctrine.com
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?
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.