Integrate Propel with the Zend Framework
Let me guess your first question after reading the title: "Why should I use Propel? The Zend Framework has its own database abstraction." Check out my previous blog post on that topic for an answer: Why I prefer Propel over Zend_Db
Installing Propel
The easiest and fastest way to install Propel is PEAR. Open your command line and type the following:
pear install -a phpdb/propel_generator
pear install -a phpdb/propel_runtime
Afterwards Propel should be installed including all needed dependencies (like Phing). If you have problems installing Propel, check out the official documentation for alternative ways.
Setting Up the Zend Framework Project
If you never set up the Zend Framework before, please refer to the official Quickstart Guide. I use the same structure as created by the zf Command Line Tool.
Setting Up Propel
At first I must create some configuration files, so Propel knows how my database layout should look like, where to put the generated models and how to connect to my database server. Place the configuration files under application/configs/.
Creating schema.xml
The schema.xml file describes my database. Consequently that is my root tag.
Inside the database tag I can define the tables I need in my database. I will create some tables which could be used for a simple blog application.
<table name="post"></table>
<table name="comment"></table>
<table name="user"></table>
</database>
The file will grow now: Each table needs some columns of course. Columns have to be placed inside the corresponding table tag.
<table name="post">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="title" type="varchar" size="255" required="true" />
<column name="text" type="longvarchar" required="true" />
<column name="timestamp" type="timestamp" required="true" />
<column name="user_id" type="integer" required="true"/>
</table>
<table name="comment">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="title" type="varchar" size="255" required="true" />
<column name="text" type="longvarchar" required="true" />
<column name="timestamp" type="timestamp" required="true" />
<column name="post_id" type="integer" required="true"/>
<column name="user_id" type="integer" required="true"/>
</table>
<table name="user">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="email" type="varchar" size="255" required="true" />
<column name="password" type="varchar" size="40" required="true" />
</table>
</database>
Last step is to define the foreign keys. The foreign key definitions must be placed inside the table tags like the columns.
<table name="post">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="title" type="varchar" size="255" required="true" />
<column name="text" type="longvarchar" required="true" />
<column name="timestamp" type="timestamp" required="true" />
<column name="user_id" type="integer" required="true"/>
<foreign-key foreignTable="user">
<reference local="user_id" foreign="id"/>
</foreign-key>
</table>
<table name="comment">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="title" type="varchar" size="255" required="true" />
<column name="text" type="longvarchar" required="true" />
<column name="timestamp" type="timestamp" required="true" />
<column name="post_id" type="integer" required="true"/>
<column name="user_id" type="integer" required="true"/>
<foreign-key foreignTable="post">
<reference local="post_id" foreign="id"/>
</foreign-key>
<foreign-key foreignTable="user">
<reference local="user_id" foreign="id"/>
</foreign-key>
</table>
<table name="user">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
<column name="email" type="varchar" size="255" required="true" />
<column name="password" type="varchar" size="40" required="true" />
</table>
</database>
By parsing the schema.xml Propel learns how my database should look like. It will create SQL statements and models out of it for me. But before I can start the generation process, I have two more things to do.
Creating build.properties
The Propel generator needs some additional information about my database. I provide the information in the form of a simple property file.
# database settings
propel.database = mysql
propel.database.url = mysql:host=localhost;dbname=blog
propel.database.user = myuser
propel.database.password = mypassword
# directories
propel.output.dir = /var/www/blog/htdocs/application/models
propel.php.dir = ${propel.output.dir}/classes
propel.phpconf.dir = ${propel.output.dir}/conf
propel.sql.dir = ${propel.output.dir}/db/sql
# mysql specific
propel.mysql.tableType = InnoDB
propel.addIncludes = false
propel.addGenericAccessors = true
propel.addGenericMutators = true
The database settings should be self-explanatory. The "directories" part configures where the Propel generator puts his generated files. I point all subdirs in my general output dir which is my "models" directory. The last directives tell the Propel generator what customizations I wish. This is only neccessary if you want to change default values.
Creating runtime-conf.xml
The last config file is responsible for the runtime settings.
<config>
<propel>
<datasources default="blog">
<datasource id="blog">
<adapter>mysql</adapter>
<connection>
<dsn>mysql:host=localhost;dbname=blog</dsn>
<user>myuser</user>
<password>mypassword</password>
<settings>
<setting id="charset">utf8</setting>
</settings>
</connection>
</datasource>
</datasources>
</propel>
</config>
Attention: The datasource id must match the database name I defined in schema.xml. That's it! Now comes the fun.
Generating Models
I open the command line again to start propel-gen. propel-gen expects an absolute path to the directory in which the config files reside. In my case it is /var/www/blog/htdocs/application/configs.
You should see a lot of text appearing on your terminal and it should end with:
Total time: 1.1469 second
Open your models directory and you should find three new folders called classes, conf and db. Open the folder db and afterwards the folder sql. Open the file schema.sql. Depending on what DBMS you are using the file should look exactly or at least pretty similar to this:
# It "suspends judgement" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;
#-----------------------------------------------------------------------------
#-- post
#-----------------------------------------------------------------------------
DROP TABLE IF EXISTS `post`;
CREATE TABLE `post`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`text` TEXT NOT NULL,
`timestamp` DATETIME NOT NULL,
`user_id` INTEGER NOT NULL,
PRIMARY KEY (`id`),
INDEX `post_FI_1` (`user_id`),
CONSTRAINT `post_FK_1`
FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`)
)Type=InnoDB;
#-----------------------------------------------------------------------------
#-- comment
#-----------------------------------------------------------------------------
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`text` TEXT NOT NULL,
`timestamp` DATETIME NOT NULL,
`post_id` INTEGER NOT NULL,
`user_id` INTEGER NOT NULL,
PRIMARY KEY (`id`),
INDEX `comment_FI_1` (`post_id`),
CONSTRAINT `comment_FK_1`
FOREIGN KEY (`post_id`)
REFERENCES `post` (`id`),
INDEX `comment_FI_2` (`user_id`),
CONSTRAINT `comment_FK_2`
FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`)
)Type=InnoDB;
#-----------------------------------------------------------------------------
#-- user
#-----------------------------------------------------------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`password` VARCHAR(40) NOT NULL,
PRIMARY KEY (`id`)
)Type=InnoDB;
# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;
As you can see Propel generated all SQL statements needed to create tables and columns I defined in my schema.xml including the foreign keys. Apply the SQL now to your database.
Fine ... but where are my Models?
Open the classes directory which resides under the model folder. There should be a subdirectory called Blog. Inside the Blog folder you find your models.
For example there should be a file Post.php and a file PostPeer.php. Post.php contains a class called Post which represents a row from our post table. The class Post extends BasePost. The class BasePost includes a lot of logic generated by Propel in order to interact with the database. This class will never be touched by a developer. Propel regenerates the Base classes whenever neccessary. Therefore your changes would be lost. If additional logic is needed in the row object, it must be added in the Post class. These classes are only created if they do not exist. Propel will never touch them again after initial creation. Same applies to the Peer classes.
In order to use my new models I need to integrate Propel with my Zend Framework bootstrap process.
Integrating Propel with the Zend Framework
First of all make sure the PEAR folder (or whereever you located Propel) is part of the include_path. If not, add it in your php.ini or via set_include_path. The generated model files are not compatible with the Zend Framework autoloader, so I need to bootstrap Propel seperately. Open your Bootstrap.php file in your APPLICATION_PATH and add the following _initPropel method to it.
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
/* other bootstrapping ... */
protected function _initPropel()
{
require_once 'propel/Propel.php';
Propel::init(APPLICATION_PATH . '/models/conf/Blog-conf.php');
}
}
Having Fun with Propel
Open your IndexController.php to do some experiments. At first I create some data so later I can select something.
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$user = new User();
$user->setEmail('mike@roetgers.org')
->setPassword(sha1('qwertz'))
->save();
$anotherUser = new User();
$anotherUser->setEmail('anothermike@roetgers.org')
->setPassword(sha1('qwerty'))
->save();
$post = new Post();
$post->setTitle('Some title ...')
->setText('Some text ...')
->setTimestamp(new DateTime())
->setUser($user)
->save();
$comment = new Comment();
$comment->setTitle('The sun ...')
->setText('... is shining.')
->setUser($anotherUser)
->setPost($post)
->save();
}
}
No more SQL. I can forget about the database and just work with comfortable objects. For example I don't have to pass the user_id into the Post object in order to associate it with the user who wrote it. I simply pass the whole User object, Propel does the rest.
class IndexController extends Zend_Controller_Action
{
// ...
public function selectAction()
{
$criteria = new Criteria();
$user = UserPeer::doSelectOne($criteria);
$posts = $user->getPosts();
foreach ($posts as $post) {
$comments = $post->getComments();
// ...
}
}
}
The Criteria objects are like the WHERE statements in my SQL statement. With their help I can define what exact data I want from the database. The UserPeer class returns one User. In this case the Criteria object is empty, so it grabs the first one from the database. With the User object I can then fetch all posts of that specific user, iterate over them and grab all associated comments.
Let the Comment object return its creation timestamp. You don't get a string with the DATETIME information from the database but a DateTime object just like you would expect it.
Propel is very intuitive. If you have an IDE with auto completion you can handle your database without the slightest effort. Check out the documentation for more examples.
October 25th, 2009 - 15:34
the question I would ask is rather “why not Doctrine?”
As far as I know, the Propel project is kinda dead.
October 25th, 2009 - 16:54
Propel isn’t dead. Propel is alive and doing well.
http://twitter.com/#search?q=%23propel
You can use doctrine, why you are acting like this?
October 25th, 2009 - 17:01
@Jani: Last commits were yesterday. There is an unstable version 1.4 in active development and plannings for 2.0. Doesn’t look dead to me.
December 2nd, 2009 - 17:02
Hi Guys,
Love the solution… one question though.. since the propel autoloader works on specific folders…
How did you get past the AutoLoad of the ORM classes…
I had to modify the include path to get it to work.. what am I missing here
protected function _initPropel() {
set_include_path ( APPLICATION_PATH . ‘/models/classes’ . PATH_SEPARATOR . get_include_path () );
require_once ‘propel/Propel.php’;
Propel::init ( APPLICATION_PATH . ‘/models/conf/isap-conf.php’ );
}
Much appreciated!
January 29th, 2010 - 20:59
Mike, your e-mail address at mike@roetgers.org doesn’t seem to be working. Also, you may want to check your RSS feed generator because my RSS reader is not picking it up. Thanks. Ciao.