Archive for the 'DbFinder' Category

Sorting By Custom Column in the Symfony Admin Generator

Did you ever wish you could sort by a partial column in the admin generator? Using DbFinder and a few lines of code, it is now possible.

The symfony admin generator allows you to select which properties of a model you want to display. You can include foreign key fields, or even a partial field to display pretty much everything you want in the list view. The following example uses this ability to display the name of article authors, based on the fact that the Article model has a many to one relationship to the User model:

# in mymodule/config/generator.yml
generator:
  class:          sfPropelAdminGenerator
  param:
    model_class:  Blog
    theme:        default

list:
  display:        [=title, user, category, _nb_posts, created_at]
  fields: 
    user:         { name: Author }

This generator configuration includes a partial field that counts the number of blog posts for each blog:

// in mymodule/templates/_nb_posts.php
<?php echo $blog->countBlogPosts() ?>


The problem is that only the "True" fields (that is, the ones that correspond to a column in the main table) are sortable. The result is that, with the following example, only the title column is sortable.

With symfony alone, there is no way to make the other columns sortable except overriding the whole _list_th_tabular.php partial in your module, overriding the addSortCriteria() method in the action, and losing the ability to add or remove columns in the future.

Enters DbFinderPlugin. You probably know from this blog that DbFinder offers a very powerful and yet simple way to replace Propel Criteria queries. What you might not know is that the DbFinder plugin bundles a full admin generator theme. It has the exact same features and syntax as the standard symfony admin generator, but it is entirely written with DbFinder queries. And to make this generator theme very usable, it includes the batch_actions extension from symfony 1.1 (that's what allows to display the checkboxes on the left side of the list to perform an action on several records at a time), and the ability to sort by any type of column.

To use the DbFinder admin generator, no need to switch your entire project to DbFinder. Just install the plugin, edit the generator.yml of one of your generated modules, and change the class property from sfPropelAdminGenerator (or sfDoctrineAdminGenerator, if you use Doctrine) to DbFinderAdminGenerator. Refresh the page in your browser, and you should normally see no change. That's good news: despite the fact that all the generator code has been rewritten to work with DbFinder instead of Propel, it is completely backwards compatible.

And once a generated module uses DbFinder, you gain access to the new sort_method option for custom fields:

# in mymodule/config/generator.yml
generator:
  class:          DbFinderAdminGenerator
  param:
    model_class:  Blog
    theme:        default

list:
  display:        [=title, user, category, _nb_posts, created_at]
  fields: 
    user:         { name: Author, sort_method: orderByUsername }
    category:     { sort_method: orderByCategory }
    nb_posts:     { sort_method: orderByNbPosts }

Refresh the list view, and voila, the column headers are now clickable.

Don't click the new links yet: you've defined three methods for custom ordering, and you still have to write them. To do so, you need to create a BlogFinder, which is a finder class specific to the Blog model class. So create a lib/model/BlogFinder.php class with the following content:

// in lib/model/BlogFinder.php
class BlogFinder extends DbFinder
{
  protected $class = 'Blog';
 
  public function orderByUsername($order = 'asc')
  {
    return $this->orderBy('User.Name', $order);
  }

  public function orderByCategory($order = 'asc')
  {
    return $this->orderBy('Category.Name', $order);
  }
 
  public function orderByNbPosts($order = 'asc')
  {
    return $this->
      leftJoin('BlogPost')->
      groupBy('Blog.Id')->
      withColumn('COUNT(BlogPost.Id)', 'nbPosts')->
      orderBy('nbPosts', $order);
  }
}


The finder is smart enough to guess the relationship between the Blog and the User model, as well as the relationship with the Category model, because the YAML schema defines foreign keys between the related tables.

Clear the cache (to allow the autoloading to find the new finder class), refresh your list, and enjoy fully sortable columns.

To finish, here is a small trick to drastically improve your backend performance. Every time the _nb_posts partial is called (and that's once per row in the list), symfony issues a COUNT query. That means that the current configuration will run n+1 queries, n being the number of results per page (typically 20). That's pretty bad for performance. What if you could hydrate an additional column in the main query and use this column in the _nb_posts partial? With DbFinder, that's very easy. Just add a finder_methods setting to your list configuration, as follows:

# in mymodule/config/generator.yml
list:
  display:        [=title, user, category, _nb_posts, created_at]
  fields: 
    user:         { name: Author, sort_method: orderByUsername }
    category:     { sort_method: orderByCategory }
    nb_posts:     { sort_method: orderByNbPosts }
    finder_methods: [withNbPosts]

Symfony executes all the methods defined in the finder_methods before displaying the list. It allows you to define a default ordering, to filter out some records, or, like here, to add custom column to the main query.

Now it's time to create this BlogFinder::withNbPosts() method. Since it contains part of the code of orderByNbPosts(), and that the finder generator executes sort methods at the end of the action, you can reduce the orderByNbPosts() code accordingly:

// in lib/model/BlogFinder.php
public function withNbPosts($order = 'asc')
{
  return $this->
    leftJoin('BlogPost')->
    groupBy('Blog.Id')->
    withColumn('COUNT(BlogPost.Id)', 'nbPosts');
}

public function orderByNbPosts($order = 'asc')
{
  return $this->orderBy('nbPosts', $order);
}


Now the main list query includes the call for the calculated nbPosts column, and you can change the _nb_posts partial to use it:

// in mymodule/templates/_nb_posts.php
<?php echo $blog->getColumn('nbPosts') ?>


Refresh the list view: Ta-da, the result is the same, but using a single query instead of n+1.

So the DbFinder generator offers the same features as the current symfony 1.1 generator, except more. Don't wait until you upgrade your project to symfony 1.2 to enhance your generated modules. Read the DbFinder admin generator documentation, and download the plugin right away.

Everybody Goes to Symfony Camp

And that includes me. I will be giving a presentation there, in two weeks from now, called "Developing for Developers - Usability Applied to Programming", and illustrated by my recent work on DbFinderPlugin.

If you'd like to meet the best PHP5 developers in the world, or me, you should definitely go to the Symfony Camp on September 12th and 13th. The conference is in The Netherlands, not far from Amsterdam - that means not far from anybody in Europe. I heard there are some tickets left for the conference, but it won't last long. The price is not free, but with the great people talking there, the tasty barbecue, the unique atmosphere and a huge lawn to put your tent in, it's a bargain. Besides, they may fill the swimming pool this year.

We'll have plenty of time to speak about symfony, plugins, documentation, the future and everything else. It's a unique opportunity to meet in person all those who lead the symfony community. Also, there are one or two seats left for the training session, so if you want to become operational in symfony quickly, dive in.

One last world for those who expect drama: There Won't Be Blood.

sfPropelFinder becomes DbFinder - Announcing 1.0 release

The sfPropelFinder plugin, which I've told you about a lot lately, has recently been renamed to DbFinder. This emphasizes the fact that the plugin is not Propel-specific anymore, and that you can use it with Doctrine without any change in the API.

Also, I have released a version 0.9 of the plugin today, which marks the 100% coverage of the API with both the Propel and the Doctrine adapters. That's right, now any piece of code using DbFinder will work seamlessly, whatever the ORM you use in symfony.

Take the following code, for instance:

// Look in the Article model
// For objects where the author object related to the article has $nickname for nickname
// Hydrated with related translation in the current culture and category
// And put the result into a pager implementing sfPager for easy display in a web page
$pager = DbFinder::from('Article')->
  where('Author.Nickname', $nickname)->
  with('I18n', 'Category')->
  paginate($currentPage = 1, $maxResultsPerPage = 10);


Getting the same result with either Propel or Doctrine takes considerably more code.

To be honest, the Doctrine coverage is only 99%, since there is still an issue with sfDoctrineFinder::withColumn() when dealing with a calculated column - and this is something that requires Doctrine 1.0 to be fixed. The current Doctrine adapter is based on sfDoctrinePlugin and Doctrine 0.11. But as soon as Doctrine 1.0 is released, withColumn() will be updated to work exactly the same as with Propel.

This release can be considered as a 1.0 beta 1 - meaning I'll probably not add more features before releasing a stable version. I'll work on performance and edge cases if bugs are reported, so you are encouraged to download the plugin, test it, and give me as much feedback as you can.

Eating My Own Dog Food

I spent the last three hours porting my sfSimpleBlog plugin to sfPropelFinder. While it was the occasion to polish the sfPropelFinder API and fix a bug, it was also a great pleasure to replace Propel Peer/Criteria code with finder one.

The blog plugin is up and running, the code is now much cleaner, and as a bonus, the query count has been reduced. If you want to test it, checkout the latest trunk version of the plugin (the plugin release system of the symfony project website doesn't seem to appreciate my PEAR package).

I find sfPropelFinder code to be naturally flowing. It cuts model classes sizes by 50%, it is much more readable, and makes a few custom model methods useless.

See for yourself. Here is the old PluginsfSimpleBlogPostPeer class, holding the methods required to retrieve blog posts, in the previous version:

class PluginsfSimpleBlogPostPeer extends BasesfSimpleBlogPostPeer
{
  public static function getRecentPager($max, $page)
  {
    $pager = new sfPropelPager('sfSimpleBlogPost', $max);
    $c = new Criteria();
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $pager->setCriteria($c);
    $pager->setPage($page);
    $pager->setPeerMethod('doSelectJoinAll');
    $pager->init();

    return $pager;
  }

  public static function getRecent($max = 10)
  {
    $c = new Criteria();
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $c->setLimit($max);

    return self::doSelectJoinAll($c);
  }

  public static function getTaggedPager($tag, $max, $page)
  {
    $pager = new sfPropelPager('sfSimpleBlogPost', $max);
    $c = new Criteria();
    $c->addJoin(sfSimpleBlogTagPeer::SF_BLOG_POST_ID, self::ID);
    $c->add(sfSimpleBlogTagPeer::TAG, $tag);
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $pager->setCriteria($c);
    $pager->setPage($page);
    $pager->setPeerMethod('doSelectJoinAll');
    $pager->init();

    return $pager;
  }

  public static function getTagged($tag, $max)
  {
    $c = new Criteria();
    $c->addJoin(sfSimpleBlogTagPeer::SF_BLOG_POST_ID, self::ID);
    $c->add(sfSimpleBlogTagPeer::TAG, $tag);
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $c->setLimit($max);

    return sfSimpleBlogPostPeer::doSelectJoinAll($c);
  }

  public static function retrieveByStrippedTitleAndDate($text, $date, $con = null)
  {
    if ($con === null)
    {
      $con = Propel::getConnection(self::DATABASE_NAME);
    }

    $criteria = new Criteria(sfSimpleBlogPostPeer::DATABASE_NAME);
    $criteria->add(sfSimpleBlogPostPeer::STRIPPED_TITLE, $text);
    if (sfConfig::get('app_sfSimpleBlog_use_date_in_url', false))
    {
      $criteria->add(sfSimpleBlogPostPeer::PUBLISHED_AT, $date);
    }

    $v = sfSimpleBlogPostPeer::doSelect($criteria, $con);

    return !empty($v)> 0 ? $v[0] : null;
  }
}


And here is the revised version. It is no longer a Peer class, but a Finder class extending DbFinder:

class PluginsfSimpleBlogPostFinder extends Dbfinder
{
  protected $class = 'sfSimpleBlogPost';

  public function recent()
  {
    return $this->
      with(sfConfig::get('app_sfSimpleBlog_user_class', 'sfGuardUser'))->
      where('IsPublished', true)->
      orderBy('CreatedAt', 'desc');
  }

  public function tagged($tag)
  {
    return $this->
      join('sfSimpleBlogTag')->
      where('sfSimpleBlogTag.Tag', $tag);
  }

  public function withNbComments()
  {
    return $this->
      leftJoin('sfSimpleBlogComment c')->
      withColumn('COUNT(sf_blog_comment.id)', 'NbComments')->
      where('c.IsModerated', false)->
      groupBy('c.SfBlogPostId');
  }

  public function findByStrippedTitleAndDate($text, $date)
  {
    $this->where('StrippedTitle', $text);
    if (sfConfig::get('app_sfSimpleBlog_use_date_in_url', false))
    {
      $this->where('PublishedAt', $date);
    }

    return $this->findOne();
  }
}


For those who followed my previous posts, you probably understand that the only thing that prevents sfSimpleBlog from working with Doctrine is the advance in the implementation of sfDoctrineFinder class. Once that is finished - and I'm progressing quite fast - sfSimpleBlogPlugin will be the first true ORM agnostic plugin.

Admin Generator compatible with Propel and Doctrine

Just a quick note to mention a recent addition I made to the sfPropelFinderPlugin. It now features an admin generator theme, identical in functionality to the Propel and Doctrine admin generators, except... It uses DbFinder queries instead of Criteria or Doctrine_Query calls. See more in the Generator README file.

This has two implications:

  • Modules based on this generator are easier to customize, especially if you need to override methods of the action class. Instead of dealing with complicated Criterion conditions, you manipulate finder objects, with all the ease of use it implies.
  • Modules based on this generator are ORM agnostic, meaning they work both with Propel and Doctrine (actually, this is not entirely true, since sfDoctrineFinder doesn't implement all the features required by the DbFinder generator yet... but it will soon be true).

It makes the writing of ORM-agnostic plugins possible, especially for plugins like sfSimpleCMSPlugin or sfSimpleBlogPlugin who feature backend modules generated by symfony.

That's decided, the next version of the plugins I maintain will use DbFinder!

Reconciling Propel and Doctrine

Would you like to use Propel plugins in your Doctrine application, or vice-versa? For instance, our recent sfAssetsLibraryPlugin provides a great media management utility, but it requires Propel to use it. Thanks to recent progress on the sfPropelFinder plugin, you may soon be able to use it with doctrine.

Getting Abstract

My recent work on sfPropelFinder aimed at providing Propel with an easier requesting API than the current Criteria API. In order to do that, I took inspiration from other ORMs who provide fluid interfaces to the database layer (including Doctrine, Rails has_finder plugin and SQL Alchemy).

The resulting API facilitates object retrieval and modification with Propel - as much as PHP and the underlying Propel API allow it:

# Listing 1 - Finding Propel objects with sfPropelFinder
$article = sfPropelFinder::from('Article')->
  where('Title', 'like', '%foo')->
  leftJoin('Author')->
  where('Author.Name', 'John Doe')->
  orderBy('CreatedAt', 'desc')->
  findOne();


This will look familiar to Doctrine users, since the Doctrine Query API works in a similar way:

# Listing 2 - Finding Doctrine objects with Doctrine_Query
$article = Doctrine_Query::create()->
  from('Article a')->
  where('a.title like ?', '%foo')->
  leftJoin('a.Author u')->
  where('u.name = ?', 'John Doe')->
  orderby('a.created_at DESC')->
  fetchOne();


The APIs are so similar that it is not very difficult to translate sfPropelFinder method calls to Doctrine_Query calls. So I tested this idea and created a sfDoctrineFinder class:

# Listing 3 - Finding Doctrine objects with sfDoctrineFinder
$article = sfDoctrineFinder::from('Article')->
  where('Title', 'like', '%foo')->
  leftJoin('Author')->
  where('Author.Name', 'John Doe')->
  orderBy('CreatedAt', 'desc')->
  findOne();


Note: sfDoctrineFinder::leftJoin() is not yet implemented at the time of writing, but I keep the same code as above to make things clearer.

Internally, the code from Listing 3 is translated to the one from Listing 2. But it looks so identical to the one of Listing 1 that you could easily imagine an abstraction of the ORM layer. That's what I did by creating DbFinder, a class helper that instanciates either a sfPropelFinder, or a sfDoctrineFinder, based on the nature of the model requested.

# Listing 4 - Finding Model objects with DbFinder
$article = DbFinder::from('Article')->
  where('Title', 'like', '%foo')->
  leftJoin('Author')->
  where('Author.Name', 'John Doe')->
  orderBy('CreatedAt', 'desc')->
  findOne();


So the code from Listing 4 will work whether the Article class extends Doctrine_Record or BaseObject - in other terms, whether the underlying ORM is Doctrine or Propel. DbFinder provides a unified API for communicating with both ORMs.

What's The Use?

I hear the voices of Doctrine users, shouting something like: "DbFinder is much less powerful than Doctrine_Query, so why should I use it?". The answer is 'You should not'. If your ORM is Doctrine, stick to Doctrine_Query and don't use DbFinder at all. Unless you want to switch to Propel sometime in your project, but that's very unlikely.

I also hear voices saying: "Both Doctrine and Propel are abstraction layers. It doesn't make sense to build a layer to abstract abstraction layers". Those voices say right, and being abstract for the sake of being abstract is useless and too Java-like for us RAD fans.

So why bother to develop a Doctrine adapter for DbFinder if it doesn't make sense and if Doctrine users won't use it? Because it allows to develop ORM agnostic plugins. Imagine a plugin where all database queries are made by way of DbFinder. Since both Propel and Doctrine objects extend generated "Base" objects, and share a common API (getters and setters for columns and related objects), this is possible.

That means that plugins like sfSimpleForumPlugin, sfSimpleCMSPlugin, or sfGuardPlugin could have one single version working in both ORMs. No more code duplication between a Propel and an Doctrine version, no more time lost to backport modifications from one version to the other. And most important: a larger user base for all plugins, since both Doctrine users and Propel users can use them.

As a bonus, sfPropelFinder is already a compatibility layer between Propel 1.2 and Propel 1.3. Since you don't need any custom hydration with sfPropelFinder, and since it manipulates resultsets in a different way whether you are on Propel 1.2 or Propel 1.3, all the code written with sfPropelFinder already works in both versions of the ORM.

Where Do I Download This Great Thing?

Don't be in a rush, the Doctrine adapter for DbFinder is far from being finished yet. Only a small share of the sfPropelFinder features are ported to sfDoctrineFinder. And there is no available plugin using DbFinder for now. But if you write your Propel plugins with DbFinder calls starting from now (which you should, since it's so much easier), there is a good chance that you will need little to no effort to make them work with Doctrine once sfDoctrineFinder is finished.

Also, if you are a Doctrine guru and want to give me a hand to write the Doctrine adapter to DbFinder, please send me an email and we'll arrange something.

Comparing Propel, Doctrine and sfPropelFinder

When it comes to ORMs, it's all a matter of preference. Is it, really? This post compares side-by-side the code required to perform some simple operations with three OO database requesting API. The purpose is to demonstrate that productivity, and not only style, can vary a lot depending on the ORM you choose.

There are not many robust Object Relational Mapping layers in PHP5. I'll consider two of them:

  • Propel is an ORM that "allows you to access your database using a set of objects, providing a simple API for storing and retrieving data. Propel allows you, the web application developer, to work with databases in the same way you work with other classes and objects in PHP."

  • Doctrine is an ORM that "sits on top of a powerful PHP DBAL (database abstraction layer). One of its key features is the ability to optionally write database queries in an OO (object oriented) SQL-dialect called DQL inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that maintains a maximum of flexibility without requiring needless code duplication."

I will also consider an additional component to Propel named sfPropelFinder. It "provides an easy API for finding Propel objects - that is, easier than the Peer methods and the Criteria stuff". sfPropelFinder is a symfony plugin, but it can be used with Propel alone.

For the examples, I'll use the classic Article/Comment model.

Disclaimer: Being the author of sfPropelFinder, you may think that I chose examples that make it look better. To avoid this bias, I wrote a lot of examples, including some where this plugin does not perform very well. Still, if the sfPropelFinder comparison with the two other ORMs is not objective, the comparison between Propel and Doctrine is quite so.

Scope

This comparison will only focus on the API - I voluntarily leave the performance benchmarks to whoever wants to do it. But I think the gross performance comparison probably looks like:

Slowest    sfPropelFinder + Propel 1.2
|          Propel 1.2
|          Doctrine 0.11
|          sfPropelFinder + Propel 1.3
Fastest    Propel 1.3

As for the features, it is hard to give an objective comparison without getting too much in the details. If you wonder if a particular ORM does something that another can't do, post a comment about it and I'll try to give you an honest answer.

Bear in mind that sfPropelFinder is very young, that Doctrine is quite young, and that Propel has a longer history and is the most stable and mature of all three.

Retrieving an article by its primary key

// Propel
$article = ArticlePeer::retrieveByPk(123);
// Doctrine
$article = Doctrine::getTable('Article')->find(123);
// sfPropelFinder
$article = sfPropelFinder::from('Article')->findPk(123);


Retrieving the comments related to an article

// Propel
$comments = $article->getComments();
// Doctrine
$comments = $article->Comments;
// sfPropelFinder
$comments = $article->getComments(); // no change - use Propel


Retrieving an article from its title

// Propel
$c = new Criteria();
$c->add(ArticlePeer::TITLE, 'FooBar');
$article = ArticlePeer::doSelectOne($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  where('a.title = ?', array('FooBar'))->
  fetchOne();
// Doctrine (faster)
$article = Doctrine::getTable('Article')->
  findOneByTitle('FooBar');

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  where('Title', 'FooBar')->
  findOne();
// sfPropelFinder (faster)
$article = sfPropelFinder::from('Article')->
  findOneByTitle('FooBar');


Retrieving the latest 5 articles

// Propel
$c = new Criteria();
$c->addDescendingOrderByColumn(ArticlePeer::PUBLISHED_AT);
$c->setLimit(5);
$articles = ArticlePeer::doSelect($c);

// Doctrine
$articles = Doctrine_Query::create()->
  from('Article a')->
  orderby('a.published_at DESC')->
  limit(5)->
  execute();

// sfPropelFinder
$articles = sfPropelFinder::from('Article')->
  orderBy('PublishedAt', 'desc')->
  find(5);


Retrieving the last 5 comments related to an article

// Propel
$c = new Criteria();
$c->addDescendingOrderByColumn(CommentPeer::PUBLISHED_AT);
$c->setLimit(5);
$comments = $article->getComments($c);

// Doctrine
$comments = Doctrine_Query::create()->
  from('Comment c')->
  where('c.article_id = ?', array($article->getId()))->
  orderby('c.published_at DESC')->
  limit(5)->
  execute();

// sfPropelFinder
$comments = sfPropelFinder::from('Comment')->
  relatedTo($article)->
  orderBy('PublishedAt', 'desc')->
  find(5);


Retrieving the last comment related to an article

// Propel
$c = new Criteria();
$c->addDescendingOrderByColumn(CommentPeer::PUBLISHED_AT);
$c->add(CommentPeer::ARTICLE_ID, $article->getId());
$comment = CommentPeer::doSelectOne($c);

// Doctrine
$comments = Doctrine_Query::create()->
  from('Comment c')->
  where('c.article_id = ?', array($article->getId()))->
  orderby('c.published_at DESC')->
  fetchOne();

// sfPropelFinder
$comments = sfPropelFinder::from('Comment')->
  relatedTo($article)->
  findLast();


Retrieving articles based on a word appearing in the title or the summary

// Propel
$c = new Criteria();
$cton1 = $c->getNewCriterion(ArticlePeer::TITLE, '%FooBar%', Criteria::LIKE);
$cton2 = $c->getNewCriterion(ArticlePeer::SUMMARY, '%FooBar%', Criteria::LIKE);
$cton1->addOr($cton2);
$c->add($cton1);
$articles = ArticlePeer::doSelect($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  where('a.title like ? OR a.summary like ?', array('%FooBar%', '%FooBar%'))->
  execute();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  where('Title', 'like', '%FooBar%')->
  _or('Summary', 'like', '%FooBar%')->
  find();


Retrieving articles based on a complex AND/OR clause

// Articles having name or summary like %FooBar% and published between $begin and $end

// Propel
$c = new Criteria();
$cton1 = $c->getNewCriterion(ArticlePeer::TITLE, '%FooBar%', Criteria::LIKE);
$cton1 = $c->getNewCriterion(ArticlePeer::SUMMARY, '%FooBar%', Criteria::LIKE);
$cton1->addOr($cton2);
$c->add($cton1);
$c->add(ArticlePeer::PUBLISHED_AT, $begin, Criteria::GREATER_THAN);
$c->addAnd(ArticlePeer::PUBLISHED_AT, $end, Criteria::LESS_THAN);
$article = ArticlePeer::doSelect($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  where('(a.title like ? OR a.summary like ?) and (article.published_at> ? and article.published_at> ?)', array('%FooBar%', '%FooBar%', $begin, $end))->
  execute();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
    where('Title', 'like', '%FooBar%', 'cond1')->
    where('Summary', 'like', '%FooBar%', 'cond2')->
   combine(array('cond1', 'cond2'), 'or', 'cond3')->
    where('PublishedAt', '>', $begin, 'cond4')->
    where('PublishedAt', '<', $end, 'cond5')->
   combine(array('cond4', 'cond5'), 'and', 'cond6')->
  combine(array('cond3', 'cond6'), 'and')->
  find();


Retrieving articles authored by someone

// Propel
$c = new Criteria();
$c->addJoin(ArticlePeer::AUTHOR_ID, AuthorPeer::ID);
$c->add(AuthorPeer::NAME, 'John Doe');
$articles = ArticlePeer::doSelect($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  leftJoin('a.Author b')->
  where('b.name = ?', array('John Doe'))->
  execute();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  where('Author.Name', 'John Doe')-> // Guesses the join from the schema
  find();


Retrieving articles authored by people of a certain group

// Propel
$c = new Criteria();
$c->addJoin(ArticlePeer::AUTHOR_ID, AuthorPeer::ID);
$c->addJoin(AuthorPeer::GROUP_ID, GroupPeer::ID);
$c->add(GroupPeer::NAME, 'The Foos');
$articles = ArticlePeer::doSelect($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  leftJoin('a.Author b')->
  leftJoin('b.Group c')->
  where('c.name = ?', array('The Foos'))->
  execute();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  join('Author')->
  where('Group.Name', 'The Foos')-> // Guesses the Group join from the schema
  find();


Retrieving all articles and hydrating their category object in the same query

// Propel
$c = new Criteria();
$articles = ArticlePeer::doSelectJoinCategory($c);

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  leftJoin('a.Category c')->
  execute();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  with('Category')->
  find();


Retrieving an article and its category by the article primary key

// Propel
$c = new Criteria();
$c->add(ArticlePeer::ID, 123);
$c->setLimit(1);
$articles = ArticlePeer::doSelectJoinCategory($c);
$article = isset($articles[0]) ? $articles[0] : null;

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  leftJoin('a.Category c')->
  where('a.id = ?', array(123))->
  fetchOne();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  with('Category')->
  findPk(123);


Retrieving articles and hydrating their author object and the author group

// Propel
// Impossible do to it simply - need for a custom hydration method (approx 40 LOC)

// Doctrine
$article = Doctrine_Query::create()->
  from('Article a')->
  leftJoin('a.Author b')->
  leftJoin('b.Group c')->
  where('a.id = ?', array(123))->
  fetchOne();

// sfPropelFinder
$article = sfPropelFinder::from('Article')->
  with('Category', 'Group')->
  findPk(123);


Conclusion

That's a lot of queries. And I didn't mention many-to-many relations, addition of columns, behaviors, update/delete queries, count queries, or pagers. But overall, my conclusion after writing these examples is:

  • Propel is the most verbose ORM of all three
  • sfPropelFinder is the most magic of all three
  • sfPropelFinder and Doctrine are the fastest to write, depending on the cases
  • Some limits of Propel are very frustrating (limited doSelectJoinXXX(), Criterions, custom hydration)
  • Propel and sfPropelFinder will never beat DQL for complex queries

Finally, if you are wondering which ORM to choose for your next symfony project, make sure that you put the productivity in the balance.

sfPropelFinder: Now With I18n, Pagination and Update Queries

Development goes on at a decent pace on the sfPropelFinderPlugin. Recent additions make it even more suitable for your needs. It should cover about 80% of the common use cases, so you'd better try it fast.

Compatible With I18n

Projects using symfony's internationalization (i18n) layer to store various versions of a record in the database based on the user culture can also use sfPropelFinder. As an alternative to doSelectWithI18n(), you can use the plugin's native with() method to hydrate the related i18n objects.

For instance, if you have an Article class with i18n for text columns:

article:
  id:         ~
  author:     varchar(255)
  created_at: ~
article_i18n:
  content:     varchar(255)

This structure allows you to define several values for the content column, directly on the Article object:

$article = new Article();
$article->setAuthor('John');
$article->setCulture('en');
$article->setContent('english content');
$article->setCulture('fr');
$article->setContent('contenu français');
$article->save();


You probably know how to request Articles and their i18n version in a single query with doSelectWithI18n():

$c = new Criteria();
$c->add(ArticlePeer::AUTHOR, 'John');
$articles = ArticlePeer::doSelectWithI18n($c);


The sfPropelFinder offers an alternative way to do this:

$articles = sfPropelFinder::from('Article')->
  where('Author', 'John')->
  with('I18n')->
  find();


Not ony is it faster to write, it also allows you to use i18n with other 'with()' hydratation clauses, or to retrieve a single object, which is impossible with the generated i18n methods:

$article = sfPropelFinder::from('Article')->
  where('Author', 'John')->
  with('I18n', 'Category')->
  findOne();


Just like doSelectWithI18N(), with('I18n') will select the version of the i18n content based on the current user culture. If you want to force the retrieval of a particular culture, you can use the withI18n($culture) synonym:

$article = sfPropelFinder::from('Article')->
  withI18n('en')->
  findOne();


with('I18n') and withI18n() without further arguments are synonyms. But, as I always forget whether the final 'N' of 'I18n' must be lower or uppercase, the with() version is permissive and will work with 'i18n', 'i18N', 'I18n' and 'i18N'. No more silly mistakes.

Update Queries

Thanks to a patch from an early adopter of the plugin named jug, sfPropelFinder now allows you to update several records in a row. This is something that required the use of BasePeer::doUpdate() in the past, now it is as simple as an associative array defining the columns to change:

$article = sfPropelFinder::from('Article')->
  where('Author', 'John')->
  set(array('IsRead' => true));


The set() method is a termination method similar to find(), count() or delete(), meaning that it doesn't return an sfPropelFinder object.

Beware that such a query will not trigger any of the behaviors registered on the save() method - it uses BasePeer::doUpdate() in the background. You can choose to force a record-by-record update by setting the second parameter of the set() method to true.

$article = sfPropelFinder::from('Article')->
  where('Author', 'John')->
  set(array('IsRead' => true), true);


Finder From A List Of Records

If, for any reason, you already have an array of Propel objects that you want to filter further, you can initialize a finder with this array. All the finder methods will work normally, based on the list.

$comments = $article->getComments();
$comments = sfPropelFinder::from($comments)->
  where('content', 'like', '%foo%')->
  orderBy('Author')->
  findLast();


This will result in a single query using IN () to limit the result to the original array.

Paginating A List Of Results

Using sfPropelPager is easy, only it takes a couple lines. Plus, it uses a Criteria object, and the purpose of the sfPropelFinder is to avoid using Criteria as much as possible. So how do you get a paginated list, i.e. a pager object, based on a finder? Simply call the paginate($page, $maxPerPage) termination method.

$pager = sfPropelFinder::from('Article')->
  with('I18n', 'Category')->
  where('Author', 'John')->
  paginate(1, 10);
// Use the usual pager methods
echo $pager->getNbResults();
foreach ($pager->getResults() as $article)
{
  echo $article->getTitle();
}


The setPeerMethod() is useless, since you can set the pager to hydrate related tables and columns with the with() method before paginating it.

Conclusion

It's only the beginning. sfPropelPager covers a large share of the object model retrieval use case, and the objective is to reduce the need for Criteria to less than 1% of the cases.

If you're interested in this plugin, make sure you read the previous posts on this blog:

A Finder Smarter Than Propel Getters

The sfPropelFinder symfony plugin keeps getting better. The addition of two new methods, relatedTo() and findLast() make things even easier than before. Let's see it with an example.

Propel is smart enough to generate getter methods for related objects when you define a foreign key in your schema. For instance, if your schema relates Comment to Article as follows:

propel:
  article:
    title:      varchar(255)
    body:       longvarchar
    created_at: ~
  comment:
    article_id: ~
    author:     varchar(100)
    body:       longvarchar
    created_at: ~

Then after you build your model, the BaseArticle class will provide a getComments() method that will facilitate the retrieval of the comments related to an existing article:

// Getting all comments
$comments = $article->getComments();


But now, what if you need to get the list of comments ordered from the last posted to the first posted? The generated getter method accepts a Criteria as its first parameter, so you can write:

// Getting all comments, ordered by date
$c = new Criteria();
$c->addDescendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = $article->getComments($c);


If you want the latest comment posted on an article, you will need to make something slightly more complicated. For the sake of the example, the code appears on the same fashion as the previous ones, but you should definitely wrap it up in a method stored in the Article class.

// Getting the last comment, ordered by date
$c = new Criteria();
$c->addDescendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = $article->getComments($c);
if(isset($comments[0])
  $comment = $comments[0];
}
else
{
  $comment = null;
}

// Alternative way, but not really shorter
$c = new Criteria();
$c->add(CommentPeer::ARTICLE_ID, $article->getId());
$c->addDescendingOrderByColumn(CommentPeer::CREATED_AT);
$comment = CommentPeer::doSelectOne($c);


The sfPropelFinder provides an alternative, and I believe better way of doing this. The finder object can filter results related to a given object thanks to its new relatedTo() method. This method uses the schema to guess the local and foreign columns, so you don't have to pass any other argument than the related object:

// Getting all comments
$comments = sfPropelFinder::from('Comment')->
  relatedTo($article)->
  find();


How is that better than $article->getComments()? It is not. But for the following examples, the interest of the relatedTo() method appears more clearly:

// Getting all comments, ordered by date
$comments = sfPropelFinder::from('Comment')->
  relatedTo($article)->
  orderByCreatedAt()->
  find();


// Getting the last comment, ordered by date
$comments = sfPropelFinder::from('Comment')->
  relatedTo($article)->
  findLast();


findLast() is also a recent addition to the plugin. It returns a single record, the last one, based on the creation date (or on the id if there is no creation date column).

The relatedTo() method offers the same convenient way to retrieve related objects by a single method call as Propel generated getters. But since it is embedded in the sfPropelFinder class, it allows for further manipulation of the results with a very simple API.

So if you didn't give it a try yet, checkout the sfPropelFinderPlugin from the symfony Subversion repository. You will soon see how it dramatically reduces the amount of code you write in the model.

Finding “Augmented” Propel Objects

Retrieving objects with Propel is easy with sfPropelFinderPlugin. And, since the addition of the with() method, you can hydrate related objects in a single query with no pain.

But there is still a use case where the sfPropelFinder can't help you, and this is when you need to add one or two columns from a related objects but not the whole object.

For instance, imagine that you have an Article and a Comment object, the Comment being related to the Article in the schema by a article_id column.

Until now, if you wanted to retrieve some comments and the related article title with no additiona query, you had to either get a resultset and parse it by hand, or hydrate both the Article and the Comment object:

$comment = sfPropelFinder::from('Comment')->
  findOne();
echo $comment->getArticle()->getTitle(); // requires an additional db query
$comment = sfPropelFinder::from('Comment')->
  with('Article')->
  findOne();
echo $comment->getArticle()->getTitle(); // no additional db query, but a whole object is hydrated just for one column


Today, the sfPropelFinder just made it simpler. You can add a single column from a related object with the withColumn() finder method, and retrieve it afterwards on any Propel object with the getColumn() method:

$comment = sfPropelFinder::from('Comment')->
  withColumn('Article_Title')->
  findOne();
echo $comment->getColumn('Article_Title'); // no additional db query, and only one more column fetched


Internally, the finder will look for a relation between the Comment and the Article classes in the TableMap, and add the related join in its internal Criteria. It will call the Criteria's addAsColumn() method and deal with the augmented resultset so that the additional column is available in the Comment objects.

This new method accepts calculated columns through raw SQL code, and alias names. That means that you can add a column and reuse it in an orderBy() clause:

$articles = sfPropelFinder::from('Article')->
  join('Comment')->
  withColumn('COUNT(comment.ID)', 'NbComments')->
  orderBy('NbComments')->
  find();


Not only will the resulting Article objects be ordered by number of comment, but each individual object will have access to its number of comments through getColumn(). All that with a single SQL query and a very short syntax.

So the number of situations where you need to get a resultset instead of an array of Propel objects has reduced again. the sfPropelFinder takes care of all the dirty job for you, and lets you focus on the design of your business model rather than the details. If you didn't test it already, it's time to get the HEAD SVN revision of sfPropelFinderPlugin and give it a try.

Next Page »