Propel Gets Collections
Propel 1.5 keeps bringing new features for a better developer experience and improved performance. Today, let's see the latest addition in the Propel runtime package: the PropelCollection objects.
From Arrays to Collections
<?php // doSelect() returns an array $books = BookPeer::doSelect(new Criteria()); // $books is an array of Book objects ?> There are <?php echo count($books) ?> books: <ul> <?php foreach ($books as $book): ?> <li> <?php echo $book->getTitle() ?> </li> <?php endforeach; ?> </ul>Propel 1.5 introduces a new way to make queries on your model object. It's the occasion to improve the way Propel returns the results of a query. So starting with Propel 1.5, model queries return a collection object instead of an array. First, let's see what doesn't change. You can iterate over a collection object just like you do with an array:
<?php
// find() returns a PropelCollection, which you can use just like an array
$books = PropelQuery::from('Book')->find(); // $books is a PropelObjectCollection of Book objects
?>
There are <?php echo count($books) ?> books:
<ul>
<?php foreach ($books as $book): ?>
<li>
<?php echo $book->getTitle() ?>
</li>
<?php endforeach; ?>
</ul>
As you can see, no modification to the template code was required. `foreach()`, `count()`, `append()`, and even `unset()` can be executed on a PropelCollection object as you usually do on an array. This is because the `PropelCollection` class extends `ArrayObject`, one of the new SPL classes introduced by PHP 5. Tip: The generated `doSelect()` methods in your Peer classes keep on returning arrays in Propel 1.5. It's only if you use the new Query API that you get Collections in return. That makes this new feature completely backwards compatible with existing Propel code. PropelCollection AbilitiesA PropelCollection is more than just an array. First of all, you can call some special methods on it. Check the following example:<?php if($books->isEmpty()): ?> There are no books. <?php else: ?> There are <?php echo $books->count() ?> books: <ul> <?php foreach ($books as $book): ?> <li class="<?php echo $books->isOdd() ? 'odd' : 'even' ?>"> <?php echo $book->getTitle() ?> </li> <?php if($books->isLast()): ?> <li>Do you want more books?</li> <?php endif; ?> <?php endforeach; ?> </ul> <?php endif; ?>In this example, `isEmpty()`, `count()`, `isOdd()`, and `isLast()` are all methods of the `PropelObjectCollection` instance returned by `find()`. But there is more. The collection object offers methods allowing to alter the objects it contains:
<?php
foreach ($books as $book) {
$book->setIsPublished(true);
}
$books->save();
?>
Notice how the `save()` method is not called on each object, but on the collection object. This groups all the `UPDATE` queries into a single database transaction, which is faster than individual saves. A PropelCollection also allows you to delete all the objects in the collection in a single call with `delete()`, or to retrieve the primary keys with `getPrimaryKeys()`. Lastly, a `PropelCollection` can be exported to an array of arrays, so that you can easily inspect the results of a query. It is as simple as calling `toArray()` on a collection object:
<?php
$books = PropelQuery::from('Book')
->with('Book.Author')
->with('Book.Publisher')
->find();
print_r($books->toArray());
/* => array(
array(
'Id' => 123,
'Title' => 'War And Peace',
'ISBN' => '3245234535',
'AuthorId' => 456,
'PublisherId' => 567
'Author' => array(
'Id' => 456,
'FirstName' => 'Leo',
'LastName' => 'Tolstoi'
),
'Publisher' => array(
'Id' => 567,
'Name' => 'Penguin'
)
),
array(
'Id' => 535,
'Title' => 'Pride And Prejudice',
'ISBN' => '5665764586',
'AuthorId' => 853,
'PublisherId' => 567
'Author' => array(
'Id' => 853,
'FirstName' => 'Jane',
'LastName' => 'Austen'
),
'Publisher' => array(
'Id' => 567,
'Name' => 'Penguin'
)
),
) */
Using An Alternative CollectionIf what you need is actually an array of arrays, you'd better skip the collection of objects completely, and use a colleciton of arrays instead. This is easily done by specifying an alternative formatter when building the query, as follows:
<?php
$books = PropelQuery::from('Book')
->with('Book.Author')
->with('Book.Publisher')
->setFormatter(ModelCriteria::FORMAT_ARRAY)
->find();
Now, the result of the query is not a `PropelObjectCollection` anymore, but a `PropelArrayCollection`. The elements in the collection are associative arrays, where the keys are the column names:
<?php
foreach ($books as $book) {
echo $book['Title'];
}
And if you think that using a Collection object rather than a simple array is a bad idea regarding performance and memory consumption, try the new `PropelOnDemandCollection`. It behaves just like the `PropelObjectCollection`, except that the model objects are hydrated row-by-row and then cleaned up so that the query uses the same memory for 5 results as for 50,000:
<?php
$books = PropelQuery::from('Book')
->setFormatter(ModelCriteria::FORMAT_ON_DEMAND)
->limit(50000)
->find();
// You won't get a Fatal error for not enough memory with the following code
foreach($books as $book) {
echo $book->getTitle();
}
For those who want to deal with `PDOStatement` instances themselves, the `ModelCriteria::FORMAT_STATEMENT` formatter is at your disposal. Going FurtherThe Formatter/Collection system in the new Propel Query architecture is very extensible, so it's very easy to write a new formatter and collection objects to package your own custom hydration logic. Of course, as usual with Propel 1.5, this feature is fully unit tested and already documented. So you can start using it right now in the 1.5 branch.
5 comments
Francois Zaninotto said...
If you're iterating over a very large array, then ArrayObject might be too slow. But in the case of a very large array, Propel dies with an 'Out of memory' error anyway - be it with ArrayObject or simple array. For large arrays, the PropelOnDemandCollection is the solution.Now, if you are iterating over a reasonnable size array (say, less than 100 results), the performance hit of using ArrayObject over an array will be barely noticeable.
Lastly, if you want to implement your own 'PropelOldFashionedArrayCollection' class not extending ArrayObject, and use it in your own code, it's possible and easy!
Jan 04, 2010
Tomasz BudzyĆski said...
i don't understand why to change criteria to something new. And why to use stings again ? With old criteria i don't to remember all tables and columns name. IDE was helping me find all names. And i don't know if this is simpler way. But it's OK with ArrayObject and all the Iterator stuff
Francois Zaninotto said...
@Tomasz: Please read the "Propel's Criteria Gets Smarter" post earlier in this blog; the new Query API IS a Criteria, only on steroids. As for the why we want it to evolve, check the post to see all the things it allows that were not possible in Propel 1.4 (like complex hydration, named query reuse, etc). Lastly, for IDE completion, stay tuned: the new Propel Query API is about to get some improvements on that matter.
Jan 04, 2010
Tomasz Rutkowski said...
It would be nice to choose (in propel.ini or in Criteria) what generated doSelect should return - array or collection. I understand it returns array for backwards compatibility reason, but for new project you should allow to use new features - it will help keeping code coherent.