Archive for July, 2008

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.

Sorry to Disappoint You

Yesterday's post got its share of reactions, mostly negative, as I expected. People were either not getting my point, or complaining that I'm using the wrong method, or thinking that I crossed a line.

For those who didn't understand, let me rephrase it. The release of symfony 1.1 arrived too late, didn't create any buzz, left some developers on the side of the road, and was not enough prepared. It's not that big a deal - it's just a release, and symfony 1.1 is a great framework, with a bright future. I know that most of it is not intentional, that finalizing symfony 1.1 required a lot of work, and that choices had to be made. But it's sad because it could have been avoided, and I was disappointed by the way it happened, after a year and a half waiting for it.

All these remarks were expressed in the past, either in private or in public, and in a kind way. I also proposed two solutions to avoid the kind of mistakes I am talking about: more communication, and more community. I suggested that as well, many times. With approximately no official reaction. So I rephrased my griefs, so that people would know that things could get better. I took advantage of the post to be sarcastic about these things, because they are not serious enough to get angry. People who don't like that tone can just stop reading this blog, since it's the way I am.

Hitting a project with only the intention to hurt it is a stupid thing to do. If you think that's what my post was about, read this blog again. Symfony matters to me, because I gave a lot of my time to this project, because I still use it, and because I believe that it has a huge potential. Even if not part of the core team, I am still an active contributor to the project, I dedicate a large share of my free time to contribute tutorials and plugins, and I want symfony to get better. So I think that I'm allowed to voice my opinion about it. This is not a personal matter at all, it is all about a community project I feel involved in.

Please note that my post was provocative, but that it didn't include lies nor insults of any kind. So yes, I trolled on the sole purpose to provoke a large reaction on a calm and sunny afternoon. I guess it worked, and I hope for the sake of symfony that my concerns will be addressed in the future.

Symfony 1.1: A Smart Move

You're probably thrilled about the recent release of symfony 1.1. The new features and the rework of the inner architecture are just astounding, and yet you probably miss the real beauty in it. There is a strategy behind this release, that gives a glimpse of the future of the web. Once you get all the pieces together, you can do nothing but acknowledge that symfony 1.1 is, indeed, the best framework ever.

A Perfect Timing

Symfony 1.1 was released on a Sunday night, the last day of June 2008. In the future, people will remember this day as being the first day of the holidays. They will probably associate it with a nice movie watched on TV, or a ride back home after a week-end in the countryside. Or, most probably, they will just forget this day. Only the true developers, the ones that keep an eye on the symfony timeline all the time, will ever keep 2008/06/29 in their deepest heart as a moment of deliverance.

Symfony 1.1 was released at the perfect time to leave aside all the people that are not really involved. If you just used symfony for one project, and left if afterwards because it lacked some feature that you needed, then you don't deserve to be informed that this new release does have this feature. Symfony 1.1 is so good, that it should not be left in everybody's hands. Think of it as a forbidden manuscript of the middle ages, that only a few copyists ever read and got a fair picture of.

It could have been released a month before, and people would probably have heard of it. It could have been released during the week, and people would have heard of it. It could have been released when people expected it (at least 6 months ago), and people surely would have heard of it. Instead of that, symfony 1.1 was released the same day MedicineNet.com published their famous paper "Working While Tired May Harm Heart", and that's a sign.

An Event Like No Other

Do you remember Firefox 3 download day? The event was prepared so much in advance that the guys at Mozilla got to the Guiness Book. This is the kind of publicity symfony really flees. Being popular brings in a lot of undesirable people, like non-professional PHP developers, people who ask questions, or worse: business opportunities.

Instead, symfony didn't do more than a single blog post. No screencast of the new features, no public call for blog posts and community buzz, no special event in Paris, France, where some champagne could make heads dizzy. Let Ruby on Rails, CakePHP and CodeIgniter spend money in stupid events that just aim at creating buzz. Symfony is serious, it is for professionals, it has no time or energy to lose in such childishness. The code is what's important, nothing else. And most of all, the website was not redesigned for the occasion. The current design of the symfony project framework is so great (I made it three years ago) that it should never be changed - ever.

So basically, the release of symfony 1.1 was organized as a non-event. The whole world had to understand that, even if it was the end of an 18 month development cycle, in one of the best open source web application frameworks, it was not a matter of importance. Please people, don't come and try symfony 1.1, we don't want you.

Barriers For The Masses

But that's not enough. Even with absolutely no publicity, there is still a slight chance that the news of the symfony 1.1 release come to a non-symfony developer. Fortunately, a fake "First Project Tutorial" was there at the time of the release to repel such an unconscious fellow. Let him try symfony and get stuck in an impossible and non-working tutorial. Any normal-minded developer following this tutorial should just feel insulted and walk away. The impossible tutorial was removed from the documentation index afterwards, so people can't get fooled anymore. That's a pity, but at least they have no simple way to get into symfony - there is no "First Project tutorial" at all for symfony 1.1.

And if any madman really sticks to symfony 1.1, then he has to read 50 more pages than for the 1.0 version and understand the sfValidatorSchema to build a simple contact form. The fact that all the available plugins, tutorials and applications use a much simpler method should take care of the last non-motivated ones. Anyone who stays after that is either a masochist or a true symfony zealot.

Only then can a developer consider himself a symfony 1.1 developer, and join the brotherhood of the Proud PHP Developers Who Hate PHP.

Never Answer The Critics

With this strategy of focusing on a little kernel of early adopters and leaving the others out, there is a risk that some dissidents voice their opinion and protest about being ignored. How can symfony 1.1 and the symfony Core Team react in such case? What if the part of the community they are getting rid of publicly disagree? What if someone expresses a diverting opinion concerning the people left aside?

Once again, the symfony project gives us a great lesson of communication. They just ignore the criticisms and never answer them. Their motto is: Never reply to hostile posts, never answer embarrassing questions, never explain your choices. Symfony users must understand that they embrace a religion more than a framework, and that decisions are made for the greater good. It's all a matter of trust. If you don't trust the symfony core team, then you should just leave.

Conclusion

Once all that in mind, if you are in the core of the symfony target, you probably feel very comforted in your opinion that symfony 1.1 is, indeed, the best framework ever. And you probably agree that the future is bright for what was once a set of "Professional tools for lazy folks". Symfony 1.1 is a set of "Professional tools for not lazy folks". And be prepared: symfony 2.0 will be "Professional tools for somebody else than you".

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: