Deprecated/0.10/PropelIntegration

Warning! This page has the following tags:

  • This page is deprecated, and is only useful for developers using Agavi 0.10.
  • This page exists for reference purposes only.

Improving Agavi's Propel Support

There was a discussion on the forums recently about how Agavi's Propel integration might be improved. So I decided to just go and try if what I had in mind was possible to implement - here's the results.

PropelDatabase? Ain't Using Propel

PropelDatabase (SVN version!) benefits from the fact that (as of June 11, 2005 - so be sure to grab a fresh version from CVS first) Creole's getConnection() method won't duplicate connections if it's called multiple times with identical connection parameters. So my idea was to just grab the contents of your Propel project's config file (that's that bookstore-conf.php file that is generated when you build a project - it stores connection information and maps it to a datasource) and use it with Creole to create a connection. After all, this is exactly what Propel::getConnection() does anyway. But not using Propel has some substancial speed benefits, as it doesn't have to be included and parsed when it's not already loaded. This makes sense since you are going to use your custom SQL anyway and might not even need Propel at that point.

Ease of Use: databases.ini

Enough of the theory for a while. In reality, it's all quite easy. You simply ask to use PropelDatabase in databases.ini and add the path to the Propel runtime config file as a parameter. So provided that your project uses Propel, but you want to have painless Creole support over the same connection, you'd simply do this in your databases.ini:

[databases]
  default = "Default"

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"

Then, to get a connection (and keep in mind it's the very same connection your Propel instance uses), you'd do

  $con = $this->getContext()->getDatabaseConnection();

Voila. Simple as that. But that's nothing spectacular yet.

Multiple Datasources

Provided that you have more than one datasource in your bookstore-conf.php (for whatever reason), you can, of course, use those, too:

[databases]
  default = "Default"
  secondary = "SecondSource"

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"

[SecondSource]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"
  param.datasource = "dbtwo"

If no datasource is specified, PropelDatabase will automatically determine the default one by looking at the runtime config file you specified. You may also explicitly demand this behavior:

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"
  param.datasource = "default"

This will retrieve the correct datasource for the default connection. In this case, this would be bookstore, so as a third alternative, you could write

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"
  param.datasource = "bookstore"

But What About Propel Now?

So far, we have achieved a nice thing: we're avoiding redundancy by using connection information stored in Propel's runtime config. Now, wouldn't it be nice to have Propel setup automatically by Agavi? The common method to use Propel in your project is to include it somewhere, usually in config.php:

require_once('propel/Propel.php');
Propel::init(AG_WEBAPP_DIR . '/config/bookstore-conf.php');

This has several disadvantages:

  1. It always includes Propel, even if it's not used, slowing down the application.
  2. You have to edit config.php to change the path to the runtime config. A strange voice in your head always told you stuff like that belongs into databases.ini.
  3. It simply redundant, as the connection information is already in databases.ini, provided that you don't want to use Propel::getConnection() but instead rely on the frameworks's mechanics to retrieve a database connection.

Don't Try This at Home…

There actually is a way I used to use (it's an ugly one, mind you!) to couple your Propel config and a CreoleDatabase; I doubt many people knew about this:

define('PROPEL_CONFIG', AG_WEBAPP_DIR . '/config/bookstore-conf.php');
require_once('propel/Propel.php');
Propel::init(PROPEL_CONFIG);
$dbconf = include(PROPEL_CONFIG);
foreach($dbconf['propel']['datasources'][$dbconf['propel']['datasources']['default']]['connection'] as $key => $value)
{
  $_ENV['propel_' . $key] = $value;
}
[databases]
  default = "Default"
[Default]
  class = "CreoleDatabase"
  param.method = "env"
  param.username = "propel_username"
  param.password = "propel_password"
  param.database = "propel_database"
  param.hostspec = "propel_hostspec"
  param.phptype = "propel_phptype"

That would work - getDatabaseConnection() returns a connection with the parameters specified in bookstore-conf.php. However, this is extremely hacky. And as the heading already anticipated - thou shall not do this. Eeeeeevil. Stay away. Instead, read on - pure heaven is just around the corner.

First Step to Paradise: Know Your Configs

PropelDatabase introduces a hidden, completely transparent functionality: it keeps track of the Propel config files that are registered in databases.ini. To be specific, it always uses the last of all runtime configs that are specified, but you can, of course, tell it to prefer a certain connection - and access it later. You might be wondering what the heck I am talking about. Well. One side-effect of this is that you needn't know the path to your Propel runtime config file anywhere in the code. Specifying it in databases.ini is enough. You can easily fetch the path to the config file that was used to establish a PropelDatabase connection - and you can always fetch the default one. The obvious thing would now be to put one and one together and take advantage of what we know to initialize Propel in a smarter way:

Propel::init(Controller::getInstance()->getContext()->getDatabaseConnection('secondary')->getConfigPath());

or, even easier:

Propel::init(PropelDatabase::getDefaultConfigPath());

As I mentioned, you can flag any registered database setup as default. This is fairly easy to do:

[databases]
  default = "Default"
  secondary = "SecondSource"

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"

[SecondSource]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"
  param.datasource = "dbtwo"
  param.use_as_default = true

The only caveat is: where do we initialize Propel now? config.php doesn't work anymore, since Agavi isn't even loaded yet at the time that file is parsed. Using a filter would work. But hey, is that really the cleanest approach we can come up with? After all, we wanted Propel to only load when necessary, remember?

Autoload Magic to the Rescue

This is where autoload.ini comes into play. We add the class Propel there - but instead of having it point to propel/Propel.php, we use %AG_APP_DIR%/database/PropelAutoload.php as the target. This file has a fairly simple structure - actually, it contains only two lines of code:

require_once('propel/Propel.php');
Propel::init(PropelDatabase::getDefaultConfigPath());

That's it. End of a long journey. No more including Propel anywhere. No longer you have to worry about where to initialize. That's all history. Instead, we put everything together and can finally reap the fruit of our work.

Putting it all together

database.ini:

[databases]
  default = "Default"

[Default]
  class = "PropelDatabase"
  param.config = "%AG_WEBAPP_DIR%/config/bookstore-conf.php"

autoload.ini:

; Propel auto-init magic
Propel        = "%AG_APP_DIR%/database/PropelAutoload.php"
; Adding Criteria so we don't always have to include it
Criteria      = "propel/util/Criteria.php"
; Also adding our object model classes for convenience
Book          = "%AG_WEBAPP_DIR%/lib/propelom/Book.php"
BookPeer      = "%AG_WEBAPP_DIR%/lib/propelom/BookPeer.php"
Author        = "%AG_WEBAPP_DIR%/lib/propelom/Author.php"
AuthorPeer    = "%AG_WEBAPP_DIR%/lib/propelom/AuthorPeer.php"
Publisher     = "%AG_WEBAPP_DIR%/lib/propelom/Publisher.php"
PublisherPeer = "%AG_WEBAPP_DIR%/lib/propelom/PublisherPeer.php"

!ViewBookAction::execute():

// let's fetch that book...
$book = BookPeer::retrieveByPK($request->getParameter('book_id'));

if(Propel::isInit())
{
  echo "Just for the record... Propel has already automatically been initialized! And we didn't have to write a single line of code to trigger that!";
}

// fetch another book, randomly, using plain SQL
// theoretically, we could just as well use Propel::getConnection() to retrieve the database connection
// but using Agavi to do that is cleaner and more portable.
$con = $this->getContext()->getDatabaseConnection();
$statement= $con->createStatement();
$statement->setLimit(1);


$rs = $statement->executeQuery("SELECT * FROM book ORDER BY RAND()");
while($rs->next())
{
  $anotherBook = $rs;
}

As you can see, we didn't have to include and initialize Propel anywhere - Agavi did that for us. And since we also added Criteria and our Stub and Peer classes to autoload.ini, we were able to start coding right away instead of having to deal with including the necessary stuff. And since Propel and all associated classes and files are auto-loaded, our application's speed will benefit greatly in case Propel is not used at all during a request (which may happen, for example, if you're caching things).

Some Notes

  1. This is experimental. I didn't even test the code properly yet, nor did I write Unit Tests. It's working like a charm :)
  2. To work, this will need development snapshots of Creole (a version from June 12th 2005 or newer) and Propel (Revision 116 or later)
  3. At the time of writing, the necessary changes to Creole weren't yet checked in to CVS. The stuff above will work anyway; however, multiple connections to the database might be established for now. The necessary changes have been in Creole.php, revision 1.10, as of June 13th 2005
  4. This is in it's own branch and won't be merged into trunk before June 20th because... Has been merged to trunk quite some time ago :)
  5. I'm on vacation in Alanya, Turkey (wish me good weather) from June 12th to June 19th :) Sweet sunshine every single day

Some more notes

  1. Propel's generated files are including others using relative paths, so to make it work you should extend include_path preferably in config.php like this:
    set_include_path(get_include_path() . PATH_SEPARATOR . AG_WEBAPP_DIR . '/lib/')