Blog: Propel Gets Collections

The Propel Team – 04 January 2010

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

In Propel 1.4, the results of a `doSelect()` call used to be an array of model objects. That made iteration on the results of a query very straighforward:
 
[code] <?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> [/code]

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:

[code] <?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> [/code]

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 Abilities

A PropelCollection is more than just an array. First of all, you can call some special methods on it. Check the following example:

[code] <?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; ?> [/code]

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:

[code] <?php foreach ($books as $book) {   $book->setIsPublished(true); } $books->save(); ?> [/code]

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:

[code] <?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'        )      ),   ) */ [/code]

Using An Alternative Collection

If 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:

[code] <?php $books = PropelQuery::from('Book')   ->with('Book.Author')   ->with('Book.Publisher')   ->setFormatter(ModelCriteria::FORMAT_ARRAY)   ->find(); [/code]

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:

[code] <?php foreach ($books as $book) {   echo $book['Title']; } [/code]

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:

[code] <?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(); } [/code]

For those who want to deal with `PDOStatement` instances themselves, the `ModelCriteria::FORMAT_STATEMENT` formatter is at your disposal.

Going Further

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