= What's new in Propel 1.5? = [[PageOutline]] First and foremost, don't be frightened by the long list of new features that follows. Propel 1.5 is completely backwards compatible with Propel 1.4 and 1.3, so there is no hidden cost to benefit from these features. If you didn't do it already, upgrade the propel libraries, rebuild your model, and you're done - your application can now use the Propel 1.5 features. == New Query API == This is the killer feature of Propel 1.5. It will transform the painful task of writing Criteria queries into a fun moment. === Model Queries === Along Model and Peer classes, Propel 1.5 now generates one Query class for each table. These Query classes inherit from Criteria, but have additional abilities since the Propel generator has a deep knowledge of your schema. That means that Propel 1.5 advises that you use ModelQueries instead of raw Criteria. Model queries have smart filter methods for each column, and termination methods on their own. That means that instead of writing: {{{ #!php add(BookPeer::TITLE, 'War And Peace'); $book = BookPeer::doSelectOne($c); }}} You can write: {{{ #!php filterByTitle('War And Peace'); $book = $q->findOne(); }}} In addition, each Model Query class benefits from a factory method called `create()`, which returns a new instance of the query class. And the filter methods return the current query object. So it's even easier to write the previous query as follows: {{{ #!php filterByTitle('War And Peace'); ->findOne(); }}} The termination methods are `find()`, `findOne()`, `count()`, `paginate()`, `update()`, and `delete()`. They all accept a connection object as last parameter. Remember that a Model Query IS a Criteria. So your Propel 1.4 code snippets still work: {{{ #!php addJoin(BookPeer::AUTHOR_ID, AuthorPeer::ID); ->add(AuthorPeer::LAST_NAME, 'Tolstoi') ->addAscendingOrderByColumn(BookPeer::TITLE) ->findOne(); }}} But you will soon see that it's faster to use the generated methods of the Model Query classes: {{{ #!php useAuthorQuery(); ->filterByLastName('Tolstoi') ->endUse() ->orderByTitle() ->findOne(); }}} That's right, you can embed a query into another; Propel guesses the join to apply from the foreign key declared in your schema. That makes it very easy to package your own custom model logic into reusable query methods. After a while, your code can easily look like the following: {{{ #!php filterByPublisher($publisher) ->cheap() ->recent() ->useAuthorQuery(); ->stillAlive() ->famous() ->endUse() ->orderByTitle() ->find(); }}} The Model Queries can understand `findByXXX()` method calls, where `'XXX'` is the phpName of a column of the model. That answers one of the most common customization need: {{{ #!php findOneByTitle('War And Peace'); }}} Eventually, these Query classes will replace the Peer classes; you should place all the code necessary to request or alter Model object in these classes. The Criteria/Peer way of doing queries still work exactly the same as in previous Propel versions, so your existing applications won't suffer from this update. '''Tip''': Incidentally, if you use an IDE with code completion, you will see that writing a query has never been so easy. === Collections And On-Demand Hydration === The `find()` method of generated Model Query objects returns a `PropelCollection` object. You can use this object just like an array of model objects, iterate over it using `foreach`, access the objects by key, etc. {{{ #!php limit(5) ->find(); // $books is a PropelCollection object foreach ($books as $book) { echo $book->getTitle(); } }}} Propel also returns a `PropelCollection` object instead of an array when you use a getter for a one-to-many relationship: {{{ #!php getBooks(); // $books is a PropelCollection object }}} If your code relies on list of objects being arrays, you will need to update it a little. The `PropelCollection` object provides a method for most common array operations: {{{ Array | Collection object ------------------------ | ----------------------------------------- foreach($books as $book) | foreach($books as $book) count($books) | count($books) or $books->count() $books[]= $book | $books[]= $book or $books->append($book) $books[0] | $books[0] or $books->getFirst() $books[123] | $books[123] or $books->offsetGet(123) unset($books[1]) | unset($books[1]) or $books->remove(1) empty($books) | $books->isEmpty() in_array($book, $books) | $books->contains($book) array_pop($books) | $books->pop() etc. }}} '''Warning''': `empty($books)` always returns false when using a collection, even on a non-empty one. This is a PHP limitation. Prefer `$books->isEmpty()`, or `count($books)>0`. '''Tip''': If you can't afford updating your code to support collections instead of arrays, you can still ask Propel to generate 1.4-compatible model objects by overriding the `propel.builder.object.class` setting in your `build.properties`, as follows: {{{ #!ini propel.builder.object.class = builder.om.PHP5ObjectNoCollectionBuilder }}} The `PropelCollection` class offers even more methods that you will soon use a lot: {{{ #!php getArrayCopy() // get the array inside the collection $books->toArray() // turn all objects to associative arrays $books->getPrimaryKeys() // get an array of the primary keys of all the objects in the collection $books->getModel() // return the model of the collection, e.g. 'Book' }}} Another advantage of using a collection instead of an array is that Propel can hydrate model objects on demand. Using this feature, you'll never fall short of memory again. Available through the `setFormatter()` method of Model Queries, on-demand hydration is very easy to trigger: {{{ #!php limit(50000) ->setFormatter(ModelCriteria::FORMAT_ON_DEMAND) // just add this line ->find(); foreach ($books as $book) { echo $book->getTitle(); } }}} In this example, Propel will hydrate the `Book` objects row by row, after the `foreach` call, and reuse the memory between each iteration. The consequence is that the above code won't use more memory when the query returns 50,000 results than when it returns 5. `ModelCriteria::FORMAT_ON_DEMAND` is one of the many formatters provided by the new Query objects. You can also get a collection of associative arrays instead of objects, if you don't need any of the logic stored in your model object, by using `ModelCriteria::FORMAT_ARRAY`. The [wiki:Users/Documentation/1.5/ModelCriteria documentation] describes each formatter, and how to use it. === Model Criteria === Generated Model Queries inherit from `ModelCriteria`, which extends your good old `Criteria`, and adds a few useful features. Basically, a `ModelCriteria` is a `Criteria` linked to a Model; by using the information stored in the generated TableMaps at runtime, `ModelCriteria` offers powerful methods to simplify the process of writing a query. For instance, `ModelCriteria::where()` provides similar functionality to `Criteria::add()`, except that its [http://www.php.net/manual/en/pdostatement.bindparam.php PDO-like syntax] removes the burden of Criteria constants for comparators. {{{ #!php where('Book.Title LIKE ?', 'War And P%') ->findOne(); }}} Propel analyzes the clause passed as first argument of `where()` to determine which escaping to use for the value passed as second argument. In the above example, the `Book::TITLE` column is declared as `VARCHAR` in the schema, so Propel will bind the title as a string. The `where()` method can also accept more complex clauses. You just need to explicit every column name as `'ModelClassName.ColumnPhpName'`, as follows: {{{ #!php where('UPPER(Book.Title) LIKE ?', 'WAR AND P%') ->where('(Book.Price * 100) <= ?', 1500) ->findOne(); }}} Another great addition of `ModelCriteria` is the `join()` method, which just needs the name of a related model to build a JOIN clause: {{{ #!php join('Book.Author') ->where('CONCAT(Author.FirstName, " ", Author.LastName) = ?', 'Leo Tolstoi') ->find(); }}} `ModelCriteria` has a built-in support for table aliases, which allows to setup a query using two joins on the same table, which was not possible with the `Criteria` object: {{{ #!php join('b.Author a') // use 'a' as an alias for 'Author' in the query ->where('CONCAT(a.FirstName, " ", a.LastName) = ?', 'Leo Tolstoi') ->find(); }}} This syntax probably looks familiar, because it is very close to SQL. So you probably won't need long to figure out how to write a complex query with it. The documentation offers [wiki:Users/Documentation/1.5/ModelCriteria an entire chapter] dedicated to the new `ModelCriteria` class. Make sure you read it to see the power of this new query API. === Criteria Enhancements === Generated queries and ModelQueries are not the only ones to have received a lot of attention in Propel 1.5. The Criteria object itself sees a few improvements, that will ease the writing of queries with complex logic. `Criteria::addOr()` operates the way you always expected it to. For instance, in Propel 1.4, `addOr()` resulted in a SQL `AND` if called on a column with no other condition: {{{ #!php add(BookPeer::TITLE, '%Leo%', Criteria::LIKE); $c->addOr(BookPeer::TITLE, '%Tolstoi%', Criteria::LIKE); // translates in SQL as // WHERE (book.TITLE LIKE '%Leo%' OR book.TITLE LIKE '%Tolstoi%') // addOr() used to fail on a column with no existing condition $c = new Criteria(); $c->add(BookPeer::TITLE, '%Leo%', Criteria::LIKE); $c->addOr(BookPeer::ISBN, '1234', Criteria::EQUAL); // translates in SQL as // WHERE book.TITLE LIKE '%Leo%' AND book.ISBN = '1234' }}} This is fixed in Propel 1.5. This means that you don't need to call upon the `Criterion` object for a simple OR clause: {{{ #!php add(BookPeer::TITLE, '%Leo%', Criteria::LIKE); $c->addOr(BookPeer::ISBN, '1234', Criteria::EQUAL); // translates in SQL as // WHERE (book.TITLE LIKE '%Leo%' OR book.ISBN = '1234') // and it's much faster to write than $c = new Criteria(); $c1 = $c->getNewCriterion(BookPeer::TITLE, '%Leo%', Criteria::LIKE); $c2 = $c->getNewCriterion(BookPeer::ISBN, '1234', Criteria::EQUAL); $c1->addOr($c2); $c->add($c1); }}} `add()` and `addOr()` only allow simple logical operations on a single condition. For more complex logic, Propel 1.4 forced you to use Criterions again. This is no longer the case in Propel 1.5, which provides a new `Criteria::combine()` method. It expects an array of named conditions to be combined, and an operator. Use `Criteria::addCond()` to create a condition, instead of the usual `add()`: {{{ #!php addCond('cond1', BookPeer::TITLE, 'Foo', Criteria::EQUAL); // creates a condition named 'cond1' $c->addCond('cond2', BookPeer::TITLE, 'Bar', Criteria::EQUAL); // creates a condition named 'cond2' $c->combine(array('cond1', 'cond2'), Criteria::LOGICAL_OR); // combine 'cond1' and 'cond2' with a logical OR // translates in SQL as // WHERE (book.TITLE = 'Foo' OR book.TITLE = 'Bar'); }}} `combine()` accepts more than two conditions at a time: {{{ #!php addCond('cond1', BookPeer::TITLE, 'Foo', Criteria::EQUAL); $c->addCond('cond2', BookPeer::TITLE, 'Bar', Criteria::EQUAL); $c->addCond('cond3', BookPeer::TITLE, 'FooBar', Criteria::EQUAL); $c->combine(array('cond1', 'cond2', 'cond3'), Criteria::LOGICAL_OR); // translates in SQL as // WHERE ((book.TITLE = 'Foo' OR book.TITLE = 'Bar') OR book.TITLE = 'FooBar'); }}} `combine()` itself can return a named condition to be combined later. So it allows for any level of logical complexity: {{{ #!php addCond('cond1', BookPeer::TITLE, 'Foo', Criteria::EQUAL); $c->addCond('cond2', BookPeer::TITLE, 'Bar', Criteria::EQUAL); $c->combine(array('cond1', 'cond2'), Criteria::LOGICAL_OR, 'cond12'); $c->addCond('cond3', BookPeer::ISBN, '1234', Criteria::EQUAL); $c->addCond('cond4', BookPeer::ISBN, '4567', Criteria::EQUAL); $c->combine(array('cond3', 'cond4'), Criteria::LOGICAL_OR, 'cond34'); $c->combine(array('cond12', 'cond34'), Criteria::LOGICAL_AND); // WHERE (book.TITLE = 'Foo' OR book.TITLE = 'Bar') // AND (book.ISBN = '1234' OR book.ISBN = '4567'); }}} The new `combine()` method makes it much easier to handle logically complex criterions. The good news is that if your application code already uses the old Criterion way, it will continue to work with Propel 1.5 as all these changes are backwards compatible. Of course, since Model Queries extend Criteria, this new feature is available for all your queries, with a slightly different syntax, in order to support column phpNames: {{{ #!php condition('cond1', 'Book.Title = ?', 'Foo') ->condition('cond2', 'Book.Title = ?', 'Bar') ->combine(array('cond1', 'cond2'), Criteria::LOGICAL_OR, 'cond12') ->condition('cond3', 'Book.ISBN = ?', '1234') ->condition('cond4', 'Book.ISBN = ?', '4567') ->combine(array('cond3', 'cond4'), Criteria::LOGICAL_OR, 'cond34') ->combine(array('cond12', 'cond34'), Criteria::LOGICAL_AND) ->find(); // WHERE (book.TITLE = 'Foo' OR book.TITLE = 'Bar') // AND (book.ISBN = '1234' OR book.ISBN = '4567'); }}} == Many-to-Many Relationships == At last, Propel generates the necessary methods to retrieve related objects in a many-to-many relationship. Since this feature is often needed, many developers already wrote these methods themselves. To avoid method collision, the generation of many-to-many getters is therefore optional. All you have to do is to add the `isCrossRef` attribute to the cross reference table, and rebuild your model. For instance, if a `User` has many `Groups`, and the `Group` has many `Users`, the many-to-many relationship is materialized by a `user_group` cross reference table: {{{ #!xml