Development

Changeset 10670

You must first sign up to be able to contribute.

Changeset 10670

Show
Ignore:
Timestamp:
08/05/08 18:54:28 (4 months ago)
Author:
francois
Message:

sfPropelFinderPlugin Emphasized DbFinder of sfPropelFinder

  • Turned README into Markdown syntax, and changed the main name to DbFinder
  • DbFinder::from(Article) returns an instance of ArticleFinder if it exists. That way, extending the finder gets easier.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • plugins/sfPropelFinderPlugin/README

    r10666 r10670  
    1 = sfPropelFinder plugin = 
    2  
    3 The `sfPropelFinder` is a symfony plugin that provides an easy API for finding Propel objects - that is, easier than the Peer methods and the Criteria stuff. 
    4  
    5 == Overview == 
    6  
    7 The idea behind this plugin is to write queries to retrieve Propel objects, but fast. Think of `sfPropelFinder` as "jQuery for Propel". It also aims at putting the things in the right order, meaning that writing a `find()` query will feel natural for those familiar with SQL. 
    8  
    9 {{{ 
    10 #!php 
    11 <?php 
    12 // With Peer and Criteria 
    13 $c = new Criteria() 
    14 $c->add(ArticlePeer::TITLE, '%world', Criteria::LIKE); 
    15 $c->add(ArticlePeer::IS_PUBLISHED, true); 
    16 $c->addAscendingOrderByColumn(ArticlePeer::CREATED_AT); 
    17 $articles = ArticlePeer::doSelectJoinCategory($c); 
    18  
    19 // with sfPropelFinder 
    20 $articles = sfPropelFinder::from('Article')-> 
    21   where('Title', 'like', '%world')-> 
    22   where('IsPublished', true)-> 
    23   orderBy('CreatedAt')-> 
    24   with('Category')-> 
    25   find(); 
    26 }}} 
    27  
    28 `sfPropelFinder` uses the same fluid interface as the `sfFinder`, so you won't be lost. It is compatible with symfony 1.0 and 1.1, and with Propel 1.2 and 1.3. 
     1DbFinder plugin 
     2=============== 
     3 
     4The `DbFinder` is a symfony plugin that provides an easy API for finding Model objects, whether the underlying ORM is Propel or Doctrine. It can be seen as: 
     5 
     6* a usability layer to ease the use of Propel's Criteria object and Peer classes 
     7* an extension to Propel's limited capabilities to provide: 
     8  - complex joins 
     9  - custom hydration of related objects and columns 
     10  - schema and relation introspection 
     11* a compatibility layer to allow plugins to work with both Propel and Doctrine 
     12 
     13Overview 
     14-------- 
     15 
     16The idea behind this plugin is to write queries to retrieve model objects through an ORM, but fast. Inspired by Doctrine, Rails has_finder plugin and SQLAlchemy, `DbFinder` can be seen as "jQuery for symfony's model layer". It also aims at putting the things in the right order, meaning that writing a `find()` query will feel natural for those familiar with SQL. 
     17 
     18    // With Propel Peer and Criteria 
     19    $c = new Criteria() 
     20    $c->add(ArticlePeer::TITLE, '%world', Criteria::LIKE); 
     21    $c->add(ArticlePeer::IS_PUBLISHED, true); 
     22    $c->addAscendingOrderByColumn(ArticlePeer::CREATED_AT); 
     23    $articles = ArticlePeer::doSelectJoinCategory($c); 
     24 
     25    // with DbFinder 
     26    $articles = DbFinder::from('Article')-> 
     27      where('Title', 'like', '%world')-> 
     28      where('IsPublished', true)-> 
     29      orderBy('CreatedAt')-> 
     30      with('Category')-> 
     31      find(); 
     32 
     33`DbFinder` uses the same fluid interface as the `sfFinder`, so you won't be lost. It is compatible with symfony 1.0 and 1.1, with Propel 1.2 and 1.3, and with Doctrine 0.11. `DbFinder` comes with a Propel and a Doctrine adapter (`sfPropelFinder`, `sfDoctrineFinder`). Whenever you use `DbFinder::from()`, the finder will check whether you look for Propel or Doctrine objects and use the appropriate adapter. 
    2934 
    3035You can also implement your own business logic to encapsulate complex queries, so that your queries look like real language: 
    3136 
    32 {{{ 
    33 #!php 
    34 <?php 
    35 $finder = new ArticleFinder(); 
    36 $articles = $finder->recent()->withComments()->notAnonymous()->wellRated()->find(); 
    37 }}} 
    38  
    39 == Installation == 
    40  
    41   * Install the plugin 
     37    // ArticleFinder extends sfPropelFinder. See how below 
     38    $finder = new ArticleFinder(); 
     39    $articles = $finder->recent()->withComments()->notAnonymous()->wellRated()->find(); 
     40 
     41Installation 
     42------------ 
     43 
     44* Install the plugin 
    4245   
    43     {{{ 
    44 #!sh 
    45 > php symfony plugin-install http://plugins.symfony-project.com/sfPropelFinderPlugin 
    46     }}} 
    47  
    48   * Clear the cache 
    49  
    50     {{{ 
    51 #!sh 
    52 > php symfony cc 
    53     }}} 
    54  
    55 == Usage == 
    56  
    57 === Finding objects === 
    58  
    59 {{{ 
    60 #!php 
    61 <?php 
    62 // Finding all Articles 
    63 $articles = sfPropelFinder::from('Article')->find(); 
    64 // Finding 3 Articles 
    65 $articles = sfPropelFinder::from('Article')->find(3); 
    66 // Finding a single Article 
    67 $article = sfPropelFinder::from('Article')->findOne(); 
    68 // Finding the last Article (the finder will figure out the column to use for sorting) 
    69 $article = sfPropelFinder::from('Article')->findLast(); 
    70 }}} 
    71  
    72 ''Tip'': When developing with the finder, you may prefer to have an array or string representation of the results rather than an array of objects. The finder objects provides three methods (`toArray()`, `__toString()` and `toHtml()`) that internally execute a `find()` and return something that you can output in your response. 
    73  
    74 === Adding WHERE clause === 
    75  
    76 {{{ 
    77 #!php 
    78 <?php 
    79 $articleFinder = sfPropelFinder::from('Article'); 
    80 // Finding all Articles where title = 'foo' 
    81 $articles = $articleFinder->where('Title', 'foo')->find(); 
    82 // Finding all Articles where title like 'foo%' 
    83 $articles = $articleFinder->where('Title', 'like', 'foo%')->find(); 
    84 // Finding all Articles where published_at less than time() 
    85 $articles = $articleFinder->where('PublishedAt', '<', time())->find(); 
    86  
    87 // You can chain WHERE clauses 
    88 $articles = $articleFinder-> 
    89   where('Title', 'foo')-> 
    90   where('PublishedAt', '<', time())-> 
    91   find(); 
    92 // Or even better, use the _and() and _or() methods for SQL-like code 
    93 $articles = $articleFinder-> 
    94   where('Title', 'foo')-> 
    95    _and('PublishedAt', '<', time())-> 
    96     _or('Title', 'like', 'bar%')-> 
    97   find(); 
    98  
    99 // The where() method accepts simple or composed column names ('ClassName.ColumnName') 
    100 $articles = $articleFinder->where('Article.Title', 'foo')->find(); 
    101 // You can also use the magic whereXXX() method, removing the column argument and concatenating it to the method name 
    102 $articles = $articleFinder->whereTitle('foo')->find(); 
    103 // Or, when your search is on a single column, use the magic findByXXX() method 
    104 $articles = $articleFinder->findByTitle('foo'); 
    105 }}} 
    106  
    107 === Ordering results === 
    108  
    109 {{{ 
    110 #!php 
    111 <?php 
    112 $articleFinder = sfPropelFinder::from('Article'); 
    113 // Finding all Articles ordered by created_at (ascending order by default) 
    114 $articles = $articleFinder-> 
    115   orderBy('CreatedAt')-> 
    116   find(); 
    117 // Finding all Articles ordered by created_at desc 
    118 $articles = $articleFinder-> 
    119   orderBy('CreatedAt', 'desc')-> 
    120   find(); 
    121 // You can also use the magic orderByXXX() method 
    122 $articles = $articleFinder-> 
    123   orderByCreatedAt()-> 
    124   find(); 
    125 }}} 
    126  
    127 === Chaining methods === 
    128  
    129 The methods of the `sfPropelFinder` object return the current finder object, so you can chain them together in a single call, and finish by any of the `find()` methods to launch the query. 
    130  
    131 {{{ 
    132 #!php 
    133 <?php 
    134 // everything chained together 
    135 $articles = sfPropelFinder::from('Article')->where('Title', 'like', '%world')->_and('IsPublished', true)->orderBy('CreatedAt')->find(); 
    136 // You can write it in several lines, too 
    137 $articles = sfPropelFinder::from('Article')-> 
    138   where('Title', 'like', '%world')-> 
    139    _and('IsPublished', true)-> 
    140   orderBy('CreatedAt')-> 
    141   find(); 
    142 }}} 
     46        > php symfony plugin-install http://plugins.symfony-project.com/sfPropelFinderPlugin 
     47 
     48* Clear the cache 
     49 
     50        > php symfony cc 
     51 
     52Usage 
     53----- 
     54 
     55### Finding objects 
     56 
     57    // Finding all Articles 
     58    $articles = DbFinder::from('Article')->find(); 
     59    // Finding 3 Articles 
     60    $articles = DbFinder::from('Article')->find(3); 
     61    // Finding a single Article 
     62    $article = DbFinder::from('Article')->findOne(); 
     63    // Finding the last Article (the finder will figure out the column to use for sorting) 
     64    $article = DbFinder::from('Article')->findLast(); 
     65 
     66**Tip**: When developing with the finder, you may prefer to have an array or string representation of the results rather than an array of objects. The finder objects provides three methods (`toArray()`, `__toString()` and `toHtml()`) that internally execute a `find()` and return something that you can output in your response. 
     67 
     68### Adding WHERE clause 
     69 
     70    $articleFinder = DbFinder::from('Article'); 
     71    // Finding all Articles where title = 'foo' 
     72    $articles = $articleFinder->where('Title', 'foo')->find(); 
     73    // Finding all Articles where title like 'foo%' 
     74    $articles = $articleFinder->where('Title', 'like', 'foo%')->find(); 
     75    // Finding all Articles where published_at less than time() 
     76    $articles = $articleFinder->where('PublishedAt', '<', time())->find(); 
     77 
     78    // You can chain WHERE clauses 
     79    $articles = $articleFinder-> 
     80      where('Title', 'foo')-> 
     81      where('PublishedAt', '<', time())-> 
     82      find(); 
     83    // Or even better, use the _and() and _or() methods for SQL-like code 
     84    $articles = $articleFinder-> 
     85      where('Title', 'foo')-> 
     86       _and('PublishedAt', '<', time())-> 
     87        _or('Title', 'like', 'bar%')-> 
     88      find(); 
     89     
     90    // The where() method accepts simple or composed column names ('ClassName.ColumnName') 
     91    $articles = $articleFinder->where('Article.Title', 'foo')->find(); 
     92    // You can also use the magic whereXXX() method, removing the column argument and concatenating it to the method name 
     93    $articles = $articleFinder->whereTitle('foo')->find(); 
     94    // Or, when your search is on a single column, use the magic findByXXX() method 
     95    $articles = $articleFinder->findByTitle('foo'); 
     96 
     97### Ordering results 
     98 
     99    $articleFinder = DbFinder::from('Article'); 
     100    // Finding all Articles ordered by created_at (ascending order by default) 
     101    $articles = $articleFinder-> 
     102      orderBy('CreatedAt')-> 
     103      find(); 
     104    // Finding all Articles ordered by created_at desc 
     105    $articles = $articleFinder-> 
     106      orderBy('CreatedAt', 'desc')-> 
     107      find(); 
     108    // You can also use the magic orderByXXX() method 
     109    $articles = $articleFinder-> 
     110      orderByCreatedAt()-> 
     111      find(); 
     112 
     113### Chaining methods 
     114 
     115The methods of the `DbFinder` object return the current finder object, so you can chain them together in a single call, and finish by any of the `find()` methods to launch the query. 
     116 
     117    // everything chained together 
     118    $articles = DbFinder::from('Article')->where('Title', 'like', '%world')->_and('IsPublished', true)->orderBy('CreatedAt')->find(); 
     119    // You can write it in several lines, too 
     120    $articles = DbFinder::from('Article')-> 
     121      where('Title', 'like', '%world')-> 
     122       _and('IsPublished', true)-> 
     123      orderBy('CreatedAt')-> 
     124      find(); 
    143125 
    144126The syntax should remind you of `sfFinder` and `sfTestBrowser`. 
    145127 
    146 === Finding records related to another one === 
    147  
    148 {{{ 
    149 #!php 
    150 <?php 
    151 // Propel way 
    152 $comments = $article->getComments(); 
    153 // sfPropelFinder way 
    154 $commentFinder = sfPropelFinder::from('Comment'); 
    155 $comments = $commentFinder-> 
    156   where('ArticleId', $article->getId())-> 
    157   find(); 
    158 // Or let the finder guess local and foreign columns based on the schema 
    159 $comments = $commentFinder-> 
    160   relatedTo($article)-> 
    161   find(); 
    162 }}} 
    163  
    164 Since the finder way is longer than the native Propel way, what is the interest of using this `relatedTo()`? You get a `sfPropelFinder` object when you use `relatedTo()`, so it allows you to do things that the generated Propel getter don't allow: 
    165  
    166 {{{ 
    167 #!php 
    168 <?php 
    169 // Retrieving the related comments, orderd by date 
    170 $comments = $commentFinder-> 
    171   relatedTo($article)-> 
    172   orderBy('CreatedAt', 'desc')-> 
    173   find(); 
    174 // Retrieving the last one of the related comments 
    175 $comments = $commentFinder-> 
    176   relatedTo($article)-> 
    177   findLast(); 
    178 }}} 
    179  
    180 Compare it to the code required to get these `Comment` objects without `sfPropelFinder`, and you will understand all the benefits the `relatedTo()` method provide. 
    181  
    182 ''Tip'': Alternatively, a finder can be initialized from an array of Propel object. The resulting SQL query contains a 'IN ()' clause, so use this possibility with caution. 
    183  
    184 {{{ 
    185 #!php 
    186 <?php 
    187 // Retrieving the last one of the related comments 
    188 $comments = sfPropelFinder::from($article->getComments())-> 
    189   findLast(); 
    190 }}} 
    191  
    192 === Joins === 
    193  
    194 {{{ 
    195 #!php 
    196 <?php 
    197 // Test data 
    198 $article1 = new Article(); 
    199 $article1->setTitle('Hello, world!'); 
    200 $article1->save(); 
    201 $comment = new Comment(); 
    202 $comment->setContent('You rock!'); 
    203 $comment->setArticle($article1); 
    204 $comment->save(); 
    205  
    206 // Add a join statement 
    207 $article = sfPropelFinder::from('Article')-> 
    208   join('Comment')-> 
    209   where('Comment.Content', 'You rock!')-> 
    210   findOne(); 
    211 // No need to tell the finder which columns to use for the join, just the related Class 
    212 // After all, the columns of the FK are already defined in the schema. 
    213  
    214 // If subsequent conditions use explicit column names, 
    215 // The finder can even guess the join table and you can omit the join() statement. 
    216 // This is the case here with Comment.Content, so the following also works 
    217 $article = sfPropelFinder::from('Article')-> 
    218   where('Comment.Content', 'You rock!')-> 
    219   findOne(); 
    220  
    221 // So join() is mostly useful if you need to specify the members of the join 
    222 $article = sfPropelFinder::from('Article')-> 
    223   join('Article.Id', 'Comment.ArticleId')-> 
    224   where('Comment.Content', 'You rock!')-> 
    225   findOne(); 
    226  
    227 // Or if you want a special type of join (left, right, inner) 
    228 $article = sfPropelFinder::from('Article')-> 
    229   innerJoin('Comment')-> 
    230   where('Comment.Content', 'You rock!')-> 
    231   findOne(); 
    232  
    233 // Or both 
    234 $article = sfPropelFinder::from('Article')-> 
    235   innerJoin('Article.Id', 'Comment.ArticleId')-> 
    236   where('Comment.Content', 'You rock!')-> 
    237   findOne(); 
    238  
    239 // You can chain joins if you want to make more complex queries 
    240 $article2 = new Article(); 
    241 $article2->setTitle('Hello again, world!'); 
    242 $article2->save(); 
    243 $author1 = new Author(); 
    244 $author1->setName('John'); 
    245 $author1->save(); 
    246 $comment = new Comment(); 
    247 $comment->setContent('You rock!'); 
    248 $comment->setArticle($article2); 
    249 $comment->setAuthor($author1); 
    250 $comment->save(); 
    251  
    252 $article = sfPropelFinder::from('Article')-> 
    253   join('Comment')-> 
    254   join('Author')-> 
    255   where('Author.Name', 'John')-> 
    256   findOne(); 
    257 // In this example, Author.Name allows the finder to guess the last join 
    258 // So you can omit it 
    259 $article = sfPropelFinder::from('Article')-> 
    260   join('Comment')-> 
    261   where('Author.Name', 'John')-> 
    262   findOne(); 
    263  
    264 // You can also use the magic joinXXX() method 
    265 $article = sfPropelFinder::from('Article')-> 
    266   joinComment()-> 
    267   where('Author.Name', 'John')-> 
    268   findOne(); 
    269 }}} 
    270  
    271 === Complex logic === 
    272  
    273 {{{ 
    274 #!php 
    275 <?php 
    276 // _and() and _or() only allow simple logical operations on a single condition 
    277 // For more complex logic, you have to use combine() 
    278 // It expects an array of named conditions to be combined, and an operator 
    279 // Use the fourth argument of where() to name a condition 
    280 $article = sfPropelFinder::from('Article')-> 
    281   where('Title', '=', 'Foo', 'cond1')->     // creates a condition named 'cond1' 
    282   where('Title', '=', 'Bar', 'cond2')->     // creates a condition named 'cond2' 
    283   combine(array('cond1', 'cond2'), 'or')->  // combine 'cond1' and 'cond2' with a logical OR 
    284   findOne(); 
    285 // SELECT article.* FROM article WHERE (article.TITLE = 'foo' OR article.TITLE = 'bar'); 
    286  
    287 // combine accepts more than two conditions at a time 
    288 $articles = sfPropelFinder::from('Article')-> 
    289   where('Title', '=', 'Foo', 'cond1')-> 
    290   where('Title', '=', 'Bar', 'cond2')-> 
    291   where('Title', '=', 'FooBar', 'cond3')-> 
    292   combine(array('cond1', 'cond2', 'cond3'), 'or')-> 
    293   find(); 
    294 // SELECT article.* FROM article WHERE (article.TITLE = 'foo' OR article.TITLE = 'bar') OR article.TITLE = 'FooBar'; 
    295  
    296 // combine() itself can return a named condition to be combined later 
    297 // So it allows for any level of logical complexity 
    298 $articles = sfPropelFinder::from('Article')-> 
    299   where('Title', '=', 'Foo', 'cond1')-> 
    300   where('Title', '=', 'Bar', 'cond2')-> 
    301   combine(array('cond1', 'cond2'), 'or', 'TitleFooBar')-> 
    302   where('PublishedAt', '<=', $end, 'cond3')-> 
    303   where('PublishedAt', '>=', $begin, 'cond4')-> 
    304   combine(array('cond2', 'cond3'), 'and', 'PublishedInBounds')-> 
    305   combine(array('TitleFooBar', 'PublishedInBounds'), 'or')-> 
    306   find(); 
    307 // SELECT article.* FROM article WHERE ( 
    308 //  (article.TITLE = 'foo' OR article.TITLE = 'bar') 
    309 //  OR 
    310 //  (article.PUBLISHED_AT <= $end AND article.PUBLISHED_AT >= $begin) 
    311 // ); 
    312 }}} 
    313  
    314 === Minimizing queries === 
    315  
    316 Even if you do a Join, Propel will issue new queries when you fetch related objects: 
    317 {{{ 
    318 #!php 
    319 <?php 
    320 $comment = sfPropelFinder::from('Comment')-> 
    321   join('Article')-> 
    322   where('Article.Title', 'Hello, world')-> 
    323   findOne(); 
    324 $article = $comment->getArticle();  // Needs another database query 
    325 }}} 
    326  
    327 Just as Propel offers generated `doSelectJoinXXX()` methods, `sfPropelFinder` alows you to hydrate related objects in a single query - you just have to call the `with()` method to specify which objects the main object should be hydrated with. 
    328 {{{ 
    329 #!php 
    330 <?php 
    331 $comment = sfPropelFinder::from('Comment')->with('Article')-> 
    332   join('Article')-> 
    333   where('Article.Title', 'Hello, world')-> 
    334   findOne(); 
    335 $article = $comment->getArticle();  // Same result, with no supplementary query 
    336 }}} 
     128### Finding records related to another one 
     129 
     130    // Propel way 
     131    $comments = $article->getComments(); 
     132    // DbFinder way 
     133    $commentFinder = DbFinder::from('Comment'); 
     134    $comments = $commentFinder-> 
     135      where('ArticleId', $article->getId())-> 
     136      find(); 
     137    // Or let the finder guess local and foreign columns based on the schema 
     138    $comments = $commentFinder-> 
     139      relatedTo($article)-> 
     140      find(); 
     141 
     142Since the finder way is longer than the native Propel way, what is the interest of using this `relatedTo()`? You get a `DbFinder` object when you use `relatedTo()`, so it allows you to do things that the generated Propel getter don't allow: 
     143 
     144    // Retrieving the related comments, orderd by date 
     145    $comments = $commentFinder-> 
     146      relatedTo($article)-> 
     147      orderBy('CreatedAt', 'desc')-> 
     148      find(); 
     149    // Retrieving the last one of the related comments 
     150    $comments = $commentFinder-> 
     151      relatedTo($article)-> 
     152      findLast(); 
     153 
     154Compare it to the code required to get these `Comment` objects without `DbFinder`, and you will understand all the benefits the `relatedTo()` method provide. 
     155 
     156**Tip**: Alternatively, a finder can be initialized from an array of Propel object. The resulting SQL query contains a 'IN ()' clause, so use this possibility with caution. 
     157 
     158    // Retrieving the last one of the related comments 
     159    $comments = DbFinder::from($article->getComments())-> 
     160      findLast(); 
     161 
     162### Joins 
     163 
     164    // Test data 
     165    $article1 = new Article(); 
     166    $article1->setTitle('Hello, world!'); 
     167    $article1->save(); 
     168    $comment = new Comment(); 
     169    $comment->setContent('You rock!'); 
     170    $comment->setArticle($article1); 
     171    $comment->save(); 
     172 
     173    // Add a join statement 
     174    $article = DbFinder::from('Article')-> 
     175      join('Comment')-> 
     176      where('Comment.Content', 'You rock!')-> 
     177      findOne(); 
     178    // No need to tell the finder which columns to use for the join, just the related Class 
     179    // After all, the columns of the FK are already defined in the schema. 
     180 
     181    // If subsequent conditions use explicit column names, 
     182    // The finder can even guess the join table and you can omit the join() statement. 
     183    // This is the case here with Comment.Content, so the following also works 
     184    $article = DbFinder::from('Article')-> 
     185      where('Comment.Content', 'You rock!')-> 
     186      findOne(); 
     187 
     188    // So join() is mostly useful if you need to specify the members of the join 
     189    $article = DbFinder::from('Article')-> 
     190      join('Article.Id', 'Comment.ArticleId')-> 
     191      where('Comment.Content', 'You rock!')-> 
     192      findOne(); 
     193 
     194    // Or if you want a special type of join (left, right, inner) 
     195    $article = DbFinder::from('Article')-> 
     196      innerJoin('Comment')-> 
     197      where('Comment.Content', 'You rock!')-> 
     198      findOne(); 
     199 
     200    // Or both 
     201    $article = DbFinder::from('Article')-> 
     202      innerJoin('Article.Id', 'Comment.ArticleId')-> 
     203      where('Comment.Content', 'You rock!')-> 
     204      findOne(); 
     205 
     206    // You can chain joins if you want to make more complex queries 
     207    $article2 = new Article(); 
     208    $article2->setTitle('Hello again, world!'); 
     209    $article2->save(); 
     210    $author1 = new Author(); 
     211    $author1->setName('John'); 
     212    $author1->save(); 
     213    $comment = new Comment(); 
     214    $comment->setContent('You rock!'); 
     215    $comment->setArticle($article2); 
     216    $comment->setAuthor($author1); 
     217    $comment->save(); 
     218 
     219    $article = DbFinder::from('Article')-> 
     220      join('Comment')-> 
     221      join('Author')-> 
     222      where('Author.Name', 'John')-> 
     223      findOne(); 
     224    // In this example, Author.Name allows the finder to guess the last join 
     225    // So you can omit it 
     226    $article = DbFinder::from('Article')-> 
     227      join('Comment')-> 
     228      where('Author.Name', 'John')-> 
     229      findOne(); 
     230 
     231    // You can also use the magic joinXXX() method 
     232    $article = DbFinder::from('Article')-> 
     233      joinComment()-> 
     234      where('Author.Name', 'John')-> 
     235      findOne(); 
     236 
     237### Complex logic 
     238 
     239    // _and() and _or() only allow simple logical operations on a single condition 
     240    // For more complex logic, you have to use combine() 
     241    // It expects an array of named conditions to be combined, and an operator 
     242    // Use the fourth argument of where() to name a condition 
     243    $article = DbFinder::from('Article')-> 
     244      where('Title', '=', 'Foo', 'cond1')->     // creates a condition named 'cond1' 
     245      where('Title', '=', 'Bar', 'cond2')->     // creates a condition named 'cond2' 
     246      combine(array('cond1', 'cond2'), 'or')->  // combine 'cond1' and 'cond2' with a logical OR 
     247      findOne(); 
     248    // SELECT article.* FROM article WHERE (article.TITLE = 'foo' OR article.TITLE = 'bar'); 
     249 
     250    // combine accepts more than two conditions at a time 
     251    $articles = DbFinder::from('Article')-> 
     252      where('Title', '=', 'Foo', 'cond1')-> 
     253      where('Title', '=', 'Bar', 'cond2')-> 
     254      where('Title', '=', 'FooBar', 'cond3')-> 
     255      combine(array('cond1', 'cond2', 'cond3'), 'or')-> 
     256      find(); 
     257    // SELECT article.* FROM article WHERE (article.TITLE = 'foo' OR article.TITLE = 'bar') OR article.TITLE = 'FooBar'; 
     258 
     259    // combine() itself can return a named condition to be combined later 
     260    // So it allows for any level of logical complexity 
     261    $articles = DbFinder::from('Article')-> 
     262      where('Title', '=', 'Foo', 'cond1')-> 
     263      where('Title', '=', 'Bar', 'cond2')-> 
     264      combine(array('cond1', 'cond2'), 'or', 'TitleFooBar')-> 
     265      where('PublishedAt', '<=', $end, 'cond3')-> 
     266      where('PublishedAt', '>=', $begin, 'cond4')-> 
     267      combine(array('cond2', 'cond3'), 'and', 'PublishedInBounds')-> 
     268      combine(array('TitleFooBar', 'PublishedInBounds'), 'or')-> 
     269      find(); 
     270    // SELECT article.* FROM article WHERE ( 
     271    //  (article.TITLE = 'foo' OR article.TITLE = 'bar') 
     272    //  OR 
     273    //  (article.PUBLISHED_AT <= $end AND article.PUBLISHED_AT >= $begin) 
     274    // ); 
     275 
     276### Minimizing queries 
     277 
     278Even if you do a Join, Propel or Doctrine will issue new queries when you fetch related objects: 
     279 
     280    $comment = DbFinder::from('Comment')-> 
     281      join('Article')-> 
     282      where('Article.Title', 'Hello, world')-> 
     283      findOne(); 
     284    $article = $comment->getArticle();  // Needs another database query 
     285 
     286Just as Propel offers generated `doSelectJoinXXX()` methods, `DbFinder` allows you to hydrate related objects in a single query - you just have to call the `with()` method to specify which objects the main object should be hydrated with. 
     287 
     288    $comment = DbFinder::from('Comment')->with('Article')-> 
     289      join('Article')-> 
     290      where('Article.Title', 'Hello, world')-> 
     291      findOne(); 
     292    $article = $comment->getArticle();  // Same result, with no supplementary query 
    337293 
    338294The power of the `with()` method is that it can guess relationships just as well as `join()`, and will add the call to `join()` if you didn't do it yourself. So you can do for instance: 
    339 {{{ 
    340 #!php 
    341 <?php 
    342 $category1 = new Category(); 
    343 $category1->setName('Category1'); 
    344 $category1->save(); 
    345 $article1 = new Article(); 
    346 $article1->setTitle('Hello, world!'); 
    347 $article1->setCategory($category1); 
    348 $article1->save(); 
    349 $comment = new Comment(); 
    350 $comment->setContent('foo'); 
    351 $comment->setArticle($article1); 
    352 $comment->save(); 
    353  
    354 $comments = sfPropelFinder::from('Comment')-> 
    355   with('Article', 'Category')-> 
    356   find();      // One single query here 
    357 foreach ($comments as $comment) 
    358 
    359   echo $comment->getArticle()->getCategory()->getName();  // No query needed, the related Article and article Category are already hydrated 
    360 
    361 }}} 
     295 
     296    $category1 = new Category(); 
     297    $category1->setName('Category1'); 
     298    $category1->save(); 
     299    $article1 = new Article(); 
     300    $article1->setTitle('Hello, world!'); 
     301    $article1->setCategory($category1); 
     302    $article1->save(); 
     303    $comment = new Comment(); 
     304    $comment->setContent('foo'); 
     305    $comment->setArticle($article1); 
     306    $comment->save(); 
     307 
     308    $comments = DbFinder::from('Comment')-> 
     309      with('Article', 'Category')-> 
     310      find();      // One single query here 
     311    foreach ($comments as $comment) 
     312    { 
     313      echo $comment->getArticle()->getCategory()->getName();  // No query needed, the related Article and article Category are already hydrated 
     314    } 
    362315 
    363316The `with()` method can also hydrate the related I18n objects, thus providing an equivalent to symfony's `doSelectWithI18n()` methods. 
    364 {{{ 
    365 #!php 
    366 <?php 
    367 // Consider the following schema 
    368 //article: 
    369 //  title:       varchar(255) 
    370 //article_i18n: 
    371 //  content:     varchar(255) 
    372 $article = new Article(); 
    373 $article->setTitle('Foo Bar'); 
    374 $article->setCulture('en'); 
    375 $article->setContent('english content'); 
    376 $article->setCulture('fr'); 
    377 $article->setContent('contenu français'); 
    378 $article->save(); 
    379  
    380 sfContext::getInstance()->getUser()->setCulture('en'); 
    381 $article = sfPropelFinder::from('Article')->with('I18n')->findOne(); 
    382 echo $article->getContent();   // english content 
    383 sfContext::getInstance()->getUser()->setCulture('fr'); 
    384 $article = sfPropelFinder::from('Article')->with('I18n')->findOne(); 
    385 echo $article->getContent();   // contenu français 
    386 }}} 
    387  
    388 Note: Since the `i18nTable` and the `ìs_culture` schema properties are lost after model generation, `with('I18n')` only works if the i18n table is named after the main table (e.g. 'Article' => 'ArticleI18n') and if the culture column name is `culture`. This is the default symfony behavior, so it should work if you didn't define special i18n table and column names. 
    389  
    390 === Adding columns === 
     317 
     318    // Consider the following schema 
     319    //article: 
     320    //  title:       varchar(255) 
     321    //article_i18n: 
     322    //  content:     varchar(255) 
     323    $article = new Article(); 
     324    $article->setTitle('Foo Bar'); 
     325    $article->setCulture('en'); 
     326    $article->setContent('english content'); 
     327    $article->setCulture('fr'); 
     328    $article->setContent('contenu français'); 
     329    $article->save(); 
     330 
     331    sfContext::getInstance()->getUser()->setCulture('en'); 
     332    $article = DbFinder::from('Article')->with('I18n')->findOne(); 
     333    echo $article->getContent();   // english content 
     334    sfContext::getInstance()->getUser()->setCulture('fr'); 
     335    $article = DbFinder::from('Article')->with('I18n')->findOne(); 
     336    echo $article->getContent();   // contenu français 
     337 
     338**Note**: Since the `i18nTable` and the `is_culture` schema properties are lost after Propel model generation, `with('I18n')` only works if the i18n table is named after the main table (e.g. 'Article' => 'ArticleI18n') and if the culture column name is `culture`. This is the default symfony behavior, so it should work if you didn't define special i18n table and column names. 
     339 
     340### Adding columns 
    391341 
    392342If what you need is a single property of a related object, you probably don't need to hydrate the whole related object. For those cases, the finder allows you to add only one column of a related object with `withColumn()`. You can retrieve supplementary columns added by the finder by calling `getColumn()` on the resulting objects. 
     
    394344Warning: The `withColumn()` feature requires symfony's Behavior system. It will only work if you enable behaviors in `propel.ini` and rebuild your model afterwards. 
    395345 
    396 {{{ 
    397 #!php 
    398 <?php 
    399 $article = sfPropelFinder::from('Article')-> 
    400   join('Category')-> 
    401   withColumn('Category.Name')-> 
    402   findOne(); 
    403 $categoryName = $article->getColumn('Category.Name');  // No supplementary query 
    404  
    405 // Beware that in this case, the related `Category` object is not hydrated, since `with()` was not used. 
    406 // That means that retrieving the related `Category` object will issue a new database query, 
    407 // so use `withColumn()` only when you need one or two supplementary columns instead of the whole object. 
    408 $categoryName = $article->getCategory()->getName();  // One supplementary query 
    409  
    410 // Just like with(), withColumn() adds an internal join if you don't do it yourself 
    411 $article = sfPropelFinder::from('Article')-> 
    412   withColumn('Category.Name')-> 
    413   findOne(); 
    414 $categoryName = $article->getColumn('Category.Name');  // Works without a call to `join('Category')` 
    415  
    416 // withColumn() can use a column alias as second argument. 
    417 $article = sfPropelFinder::from('Article')-> 
    418   join('Category')-> 
    419   withColumn('Category.Name', 'category')-> 
    420   findOne(); 
    421 $categoryName = $article->getColumn('category'); 
    422  
    423 // This is particularly useful if you want to reuse a calculated column for sorting or grouping 
    424 $articles = sfPropelFinder::from('Article')-> 
    425   join('Comment')-> 
    426   withColumn('COUNT(comment.ID)', 'NbComments')-> 
    427   orderBy('NbComments')-> 
    428   find(); 
    429 $articles = sfPropelFinder::from('Article')-> 
    430   join('Comment')-> 
    431   groupBy('Article.Id')-> 
    432   withColumn('COUNT(comment.ID)', 'NbComments')-> 
    433   find(); 
    434  
    435 // Lastly, the supplementary columns added with withColumn() are considered string by default 
    436 // But you can force another data type by providing a third argument 
    437 $article = sfPropelFinder::from('Article')-> 
    438   join('Category')-> 
    439   withColumn('Category.CreatedAt', 'CategoryCreatedAt', 'Timestamp')-> 
    440   findOne(); 
    441 $categoryName = $article->getColumn('CategoryCreatedAt'); 
    442 }}} 
    443  
    444 === Counting objects === 
    445  
    446 {{{ 
    447 #!php 
    448 <?php 
    449 // Counting all Articles 
    450 $nbArticles = sfPropelFinder::from('Article')->count(); 
    451 }}} 
    452  
    453 === Getting a paginated list of results === 
    454  
    455 {{{ 
    456 #!php 
    457 <?php 
    458 // Getting an initialized sfPropelPager object 
    459 $pager = sfPropelFinder::from('Article')->paginate($currentPage = 1, $maxResultsPerPage = 10); 
    460 // You can use the pager object as usual 
    461 printf("Showing results %d to %d on %d\n", 
    462   $pager->getfirstIndice(), 
    463   $pager->getLastIndice(), 
    464   $pager->getNbResults()); 
    465 foreach($pager->getResuts() as $article) 
    466 
    467   echo $article->getTitle(); 
    468 
    469 }}} 
    470  
    471 === Deleting objects === 
    472  
    473 {{{ 
    474 #!php 
    475 <?php 
    476 // Deleting all Articles 
    477 $nbArticles = sfPropelFinder::from('Article')->delete(); 
    478 // Deleting a selection of Articles 
    479 $nbArticles = sfPropelFinder::from('Article')-> 
    480   where('Title', 'like', 'foo%')-> 
    481   delete(); 
    482 }}} 
    483  
    484 === Updating objects === 
    485  
    486 {{{ 
    487 #!php 
    488 <?php 
    489 $article1 = new Article; 
    490 $article1->setTitle('foo'); 
    491 $article1->save(); 
    492 $article2 = new Article; 
    493 $article2->setTitle('bar'); 
    494 $article2->save(); 
    495  
    496 // set() issues an UPDATE ... SET query based on an associative array column => value 
    497 sfPropelFinder::from('Article')-> 
    498   where('Title', 'foo')-> 
    499   set(array('Title' => 'updated title')); // 1 
    500  
    501 // set() returns the number of modified columns 
    502 sfPropelFinder::from('Article')-> 
    503   where('Title', 'updated title')-> 
    504   count(); // 1 
    505  
    506 // Beware that set() updates all records found in a signle row 
    507 // And bypasses any behavior registered on the save() hooks 
    508 // You can force a one-by-one update by setting the second parameter to true 
    509 sfPropelFinder::from('Article')-> 
    510   set(array('Title' => 'updated title'), true); 
    511 // Beware that it may take a long time 
    512 }}} 
    513  
    514 === Writing your own business logic into a finder === 
    515  
    516 You can create a new finder for your objects, with custom methods. The only prerequisites are to extend `sfPropelFinder` and to define a protected `$class` property. Then, the object has access to a protected `$criteria` property, which is a Propel Criteria that can be augmented in the usual way. Don't forget to return the current object (`$this`) in the new methods. 
    517  
    518 {{{ 
    519 #!php 
    520 <?php 
    521 // For instance, add a `recent()` method to an article finder 
    522 class ArticleFinder extends sfPropelFinder 
    523 
    524   protected $class = 'Article'; 
    525  
    526   public function recent() 
    527   { 
    528     return $this->where('CreatedAt', '>=', time() - sfConfig::get('app_recent_days', 5) * 24 * 60 * 60); 
    529   } 
    530 
    531 // You can now use your custom finder and its methods together with the usual ones 
    532 $articleFinder = new ArticleFinder(); 
    533 $articles = $articleFinder-> 
    534   recent()-> 
    535   orderByTitle()-> 
    536   find(); 
    537 }}} 
    538  
    539 === Finding Objects From A Primary Key === 
    540  
    541 {{{ 
    542 #!php 
    543 <?php 
    544 $article = sfPropelFinder::from('Article')->findPk(123); 
    545 // is equivalent to 
    546 $article = ArticlePeer::retrieveByPk(123); 
    547  
    548 // But it's longer to write so what's the point? 
    549 // You can hydrate related objects by using with() 
    550 // So you need a single query to retrieve an object and its related objects 
    551 $article = sfPropelFinder::from('Article')-> 
    552   with('Category', 'I18n')-> 
    553   findPk(123); 
    554  
    555 // Also works for objects with composite primary keys 
    556 $articleI18n = sfPropelFinder::from('ArticleI18n')->findPk(array(123, 'fr')); 
    557 }}} 
    558  
    559 === Using Class Shortcuts === 
    560  
    561 {{{ 
    562 $article = sfPropelFinder::from('Article a')-> 
    563   where('a.Title', 'foo')-> 
    564   findOne(); 
    565 // same as 
    566 $article = sfPropelFinder::from('Article')-> 
    567   where('Article.Title', 'foo')-> 
    568   findOne(); 
    569 }}} 
    570  
    571 === Hacking the finder === 
    572  
    573 If the finder doesn't (yet) provide the method to build the query you need, you can still call `Criteria` methods on the finder objects, and they will be applied to the finder's internal `Criteria` object. 
    574  
    575 {{{ 
    576 #!php 
    577 <?php 
    578 $articles = sfPropelFinder::from('Article')-> 
    579   where('Title', 'like', 'foo%')-> 
    580   addOr(ArticlePeer::TITLE, 'bar%', Criteria::LIKE)-> // that's a Criteria method 
    581   findOne(); 
    582 }}} 
     346 
     347    $article = DbFinder::from('Article')-> 
     348      join('Category')-> 
     349      withColumn('Category.Name')-> 
     350      findOne(); 
     351    $categoryName = $article->getColumn('Category.Name');  // No supplementary query 
     352 
     353    // Beware that in this case, the related `Category` object is not hydrated, since `with()` was not used. 
     354    // That means that retrieving the related `Category` object will issue a new database query, 
     355    // so use `withColumn()` only when you need one or two supplementary columns instead of the whole object. 
     356    $categoryName = $article->getCategory()->getName();  // One supplementary query 
     357 
     358    // Just like with(), withColumn() adds an internal join if you don't do it yourself 
     359    $article = DbFinder::from('Article')-> 
     360      withColumn('Category.Name')-> 
     361      findOne(); 
     362    $categoryName = $article->getColumn('Category.Name');  // Works without a call to `join('Category')` 
     363 
     364    // withColumn() can use a column alias as second argument. 
     365    $article = DbFinder::from('Article')-> 
     366      join('Category')-> 
     367      withColumn('Category.Name', 'category')-> 
     368      findOne(); 
     369    $categoryName = $article->getColumn('category'); 
     370 
     371    // This is particularly useful if you want to reuse a calculated column for sorting or grouping 
     372    $articles = DbFinder::from('Article')-> 
     373      join('Comment')-> 
     374      withColumn('COUNT(comment.ID)', 'NbComments')-> 
     375      orderBy('NbComments')-> 
     376      find(); 
     377    $articles = DbFinder::from('Article')-> 
     378      join('Comment')-> 
     379      groupBy('Article.Id')-> 
     380      withColumn('COUNT(comment.ID)', 'NbComments')-> 
     381      find(); 
     382 
     383    // Lastly, the supplementary columns added with withColumn() are considered string by default 
     384    // But you can force another data type by providing a third argument 
     385    $article = DbFinder::from('Article')-> 
     386      join('Category')-> 
     387      withColumn('Category.CreatedAt', 'CategoryCreatedAt', 'Timestamp')-> 
     388      findOne(); 
     389    $categoryName = $article->getColumn('CategoryCreatedAt'); 
     390 
     391### Counting objects 
     392 
     393    // Counting all Articles 
     394    $nbArticles = DbFinder::from('Article')->count(); 
     395 
     396### Getting a paginated list of results 
     397 
     398 
     399    // Getting an initialized sfPropelPager object 
     400    $pager = DbFinder::from('Article')->paginate($currentPage = 1, $maxResultsPerPage = 10); 
     401    // You can use the pager object as usual 
     402    printf("Showing results %d to %d on %d\n", 
     403      $pager->getfirstIndice(), 
     404      $pager->getLastIndice(), 
     405      $pager->getNbResults()); 
     406    foreach($pager->getResuts() as $article) 
     407    { 
     408      echo $article->getTitle(); 
     409    } 
     410 
     411### Deleting objects 
     412 
     413    // Deleting all Articles 
     414    $nbArticles = DbFinder::from('Article')->delete(); 
     415    // Deleting a selection of Articles 
     416    $nbArticles = DbFinder::from('Article')-> 
     417      where('Title', 'like', 'foo%')-> 
     418      delete(); 
     419 
     420### Updating objects 
     421 
     422 
     423    $article1 = new Article; 
     424    $article1->setTitle('foo'); 
     425    $article1->save(); 
     426    $article2 = new Article; 
     427    $article2->setTitle('bar'); 
     428    $article2->save(); 
     429 
     430    // set() issues an UPDATE ... SET query based on an associative array column => value 
     431    DbFinder::from('Article')-> 
     432      where('Title', 'foo')-> 
     433      set(array('Title' => 'updated title')); // 1 
     434 
     435    // set() returns the number of modified columns 
     436    DbFinder::from('Article')-> 
     437      where('Title', 'updated title')-> 
     438      count(); // 1 
     439 
     440    // Beware that set() updates all records found in a signle row 
     441    // And bypasses any behavior registered on the save() hooks 
     442    // You can force a one-by-one update by setting the second parameter to true 
     443    DbFinder::from('Article')-> 
     444      set(array('Title' => 'updated title'), true); 
     445    // Beware that it may take a long time 
     446 
     447### Writing your own business logic into a finder 
     448 
     449You can create a new finder for your objects, with custom methods. The only prerequisites are to extend `DbFinder` or its adapters ('sfPropelFinder`, `sfDoctrineFinder`) and to define a protected `$class` property.  
     450 
     451For instance, you can create an child of `sfPropelFinder` to retrieve Propel `Article` objects. This new finder has access to a protected `$criteria` property, which is a Propel Criteria that can be augmented in the usual way. Don't forget to return the current object (`$this`) in the new methods. 
     452 
     453    class ArticleFinder extends sfPropelFinder 
     454    { 
     455      protected $class = 'Article'; 
     456 
     457      public function recent() 
     458      { 
     459        return $this->where('CreatedAt', '>=', time() - sfConfig::get('app_recent_days', 5) * 24 * 60 * 60); 
     460      } 
     461    } 
     462    // You can now use your custom finder and its methods together with the usual ones 
     463    $articleFinder = new ArticleFinder(); 
     464    $articles = $articleFinder-> 
     465      recent()-> 
     466      orderByTitle()-> 
     467      find(); 
     468 
     469**Tip**: Once you define an `ArticleFinder` class, any call to `DbFinder::from('Article')` will return an instance of `ArticleFinder` instead of an instance of `DbFinder`. This also means that you can use the finder API to query model objects that are not backed by any ORM at all. 
     470 
     471### Finding Objects From A Primary Key 
     472 
     473    $article = DbFinder::from('Article')->findPk(123); 
     474    // is equivalent to 
     475    $article = ArticlePeer::retrieveByPk(123); 
     476 
     477    // But it's longer to write so what's the point? 
     478    // You can hydrate related objects by using with() 
     479    // So you need a single query to retrieve an object and its related objects 
     480    $article = DbFinder::from('Article')-> 
     481      with('Category', 'I18n')-> 
     482      findPk(123); 
     483 
     484    // Also works for objects with composite primary keys 
     485    $articleI18n = DbFinder::from('ArticleI18n')->findPk(array(123, 'fr')); 
     486 
     487### Using Class Shortcuts 
     488 
     489    $article = DbFinder::from('Article a')-> 
     490      where('a.Title', 'foo')-> 
     491      findOne(); 
     492    // same as 
     493    $article = DbFinder::from('Article')-> 
     494      where('Article.Title', 'foo')-> 
     495      findOne(); 
     496 
     497### Hacking the finder 
     498 
     499If the finder doesn't (yet) provide the method to build the query you need, you can still call `Criteria` methods on the `sfPropelFinder` objects, or call `Doctrine_Query` methods on the `sfDoctrineFinder` objects, and they will be applied to the finder's internal query object. 
     500 
     501    $articles = DbFinder::from('Article')-> 
     502      where('Title', 'like', 'foo%')-> 
     503      addOr(ArticlePeer::TITLE, 'bar%', Criteria::LIKE)-> // that's a Criteria method 
     504      findOne(); 
    583505 
    584506If you're not sure about what query is issued by the finder, you can always check the SQL code before executing a termination method by calling `getCriteria()->toString()`, or after executing a termination method by calling the `getLatestQuery()` method. 
    585507 
    586 {{{ 
    587 #!php 
    588 <?php 
    589 $finder = sfPropelFinder::from('Article')->where('Title', 'foo'); 
    590 echo $finder->getCriteria()->toString(); 
    591 // SELECT FROM article WHERE article.TITLE=? 
    592 $finder->findOne(); 
    593 echo $finder->getLatestQuery(); 
    594 // 'SELECT article.ID, article.VERSION, article.TITLE, article.CATEGORY_ID FROM article WHERE article.TITLE=\'foo\' LIMIT 1' 
    595 }}} 
    596  
    597 == TODO / Ideas == 
     508    $finder = DbFinder::from('Article')->where('Title', 'foo'); 
     509    echo $finder->getCriteria()->toString(); 
     510    // SELECT FROM article WHERE article.TITLE=? 
     511    $finder->findOne(); 
     512    echo $finder->getLatestQuery(); 
     513    // 'SELECT article.ID, article.VERSION, article.TITLE, article.CATEGORY_ID FROM article WHERE article.TITLE=\'foo\' LIMIT 1' 
     514 
     515TODO / Ideas 
     516------------ 
    598517  
    599  * Fix connection issue (defined at initialization with Doctrine, at the end with Propel) 
    600  * Allow i18n hydration of related objects (#3897) 
    601  * Allow `between` as a `where()` operator for simplicity 
    602  * Add a method returning a description of the conditions 
    603  * Add support for `withColumn()` in array/text output methods 
    604  * Bypass hydration in array/text output methods 
    605  * Handle self-referencing relationships (e.g. parent_id), especially in with() 
    606  * Handle multiple references to the same table (c.f. getFooRelatedByBarId()) 
    607  * Put as a parent class in the PeerBuilder so that every Peer class can be a finder 
    608  * Merge with sfPropelImpersonatorPlugin! 
    609  * Implement iterator interface? That way, the query is only executed upon a foreach or an array access... And the finder can be seen as a collection 
    610  * Column finder, which provides an easy interface to Creole (and PDO) for retrieval of columns instead of objects? 
    611  
    612 == Changelog == 
    613  
    614 === 2008-08-05 | Trunk === 
    615  
    616  * francois: Added more phpdoc to `sfPropelFinder` and `sfDoctrineFinder` 
    617  * mrhyde:   Fixed issue when calling several termination methods on a finder 
    618  * francois: Implemented `sfDoctrineFinder::count()` 
    619  * francois: [BC Break] Replaced `sfPropelFinder::setPeerClass()` by `sfPropelFinder::setClass()` (will break classes extending sfPropelFinder) 
    620  * francois: Refactored connection management, query reinitialization, and simplified executers signature 
    621  * francois: Implemented `sfDoctrineFinder::fromArray()`, and `sfDoctrineFinder::getLatestQuery()` 
    622  * francois: Added `DbFinderAdminGenerator` (WIP) 
    623  * francois: Fixed problem with `join()` and `with()` when called by children of `sfPropelPager` 
    624  * windock:  Fixed problem with `paginate()` when called by children of `sfPropelPager` 
    625  * mrhyde:   Added `sfPropelFinder::groupByClass()` to ease PostgreSQL grouping 
    626  * francois: Fixed problem with table alias and PostgreSQL (based on a patch by mrhyde) 
    627  * mrhyde:   Fixed problem with group by clauses being ripped off by pager 
    628  * francois: Implemented `DbFinder::toArray()`, `DbFinder::__toString()` and `DbFinder::toHtml()` 
    629  * francois: Implemented `sfDoctrineFinder::findBy()`, `findOneBy()`, `findPk()`, and initialized `where()` 
    630  * francois: Added preliminary support for table aliases (`from('Article a')`) in Doctrine and Propel finders 
    631  * francois: Implemented `sfDoctrineFinder::findOne()`, `findFirst()`, `findLast()` and `orderBy()` 
    632  * francois: Initialized `DbFinder` and `sfDoctrineFinder` (WIP) 
    633  
    634 === 2008-07-07 | 0.3.0 Beta === 
    635  
    636  * francois: Added `sfPropelFinder::combine()` method to handle complex queries with And and Or 
    637  * francois: Added support for `with()` in `findPk()` (and documented the method) 
    638  * francois: Added the ability to do left, right, and inner joins in a simple way 
    639  * francois: Made `join()` useless if there is an explicit `where()` on the table afterwards 
    640  * francois: Added a `prove.php` test file to launch all tests at once in a test harness 
    641  * francois: Moved utility methods as static methods of a third-party class to take some weight off the main class 
    642  * francois: Preferring `ClassName.ColumnName` over `ClassName.ColumnName` for complete column names 
    643  * francois: Added Propel 1.3 compatibility 
    644  * francois: Added `sfPropelFinder::set()` method (based on a patch by jug) 
    645  * francois: Added `sfPropelFinder::withI18n()` method 
    646  * francois: Added `sfPropelFinderPager` class and `sfPropelFinder::paginate()` method 
    647  * francois: Added `sfPropelFinder::groupBy()` method 
    648  * francois: `sfPropelFinder::from()` now accepts an array of Propel objects 
    649  * francois: Added `sfPropelFinder::findByXXX()` and `sfPropelFinder::findOneByXXX()` methods 
    650  * francois: Added `sfPropelFinder::relatedTo()` method 
    651  * francois: Added `sfPropelFinder::findFirst()` and `sfPropelFinder::findLast()` methods 
    652  * francois: Added `sfPropelFinder::withColumn()` method 
    653  * jug:      Fixed problem in a particular join case 
    654  * francois: Added `sfPropelFinder::with()` method (based on `sfPropelObjectPeerImpersonator::populateObjects()` code by hartym) 
    655  * francois: Added support for magic `andXXX()` and `orXXX()` methods. 
    656  * jug:      Fixed `_and()` and `_or()` so that they give expected results, rather than the buggy results of Propel's `addAnd()` and `addOr()` 
    657  
    658 === 2008-03-31 | 0.2.0 Beta === 
    659  
    660  * francois: De-emphasized the use of magic methods in the unit tests and README 
    661  * francois: Added `sfPropelFinder::_and()` and `sfPropelFinder::_or()` methods 
    662  * francois: Added support for Criter