| 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. |
|---|