Development

/plugins/sfPropelFinderPlugin/README

You must first sign up to be able to contribute.

root/plugins/sfPropelFinderPlugin/README

Revision 10809, 27.7 kB (checked in by francois, 3 weeks ago)

Play well with symfony-project site Markdown code highliter

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