= 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
}}} Then, both end of the relationship see the other end through a one-to-many relationship. That means that you can deal with related objects just like you normally do, without ever creating instances of the cross reference object: {{{ #!php setName('John Doe'); $group = new Group(); $group->setName('Anonymous'); // relate $user and $group $user->addGroup($group); // save the $user object, the $group object, and a new instance of the UserGroup class $user->save(); // retrieve objects as if they shared a one-to-many relationship $groups = $user->getGroups(); // the model query also features a smart filter method for the relation $groups = GroupPeer::create() ->filterByUser($user) ->find(); }}} The syntax should be no surprise, since it's the same as the one for one-to-many relationships. Find more details about many-to-many relationships in the [wiki:Users/Documentation/1.5/Relationships relationships documentation]. == New Behaviors == The new behavior system, introduced in Propel 1.4, starts to unleash its true power with this release. Three new behaviors implement the most common customizations of object models: `nested_sets`, `sluggable`, and `sortable`. === Nested Set Behavior === Using the `treeMode` attribute in a schema, you could turn a Propel model into a hierarchical data store starting with Propel 1.3. This method is now deprecated in favor of a new `nested_set` behavior, that does eactly the same thing, but in a more extensible and effective way. The main difference between the two implementations is performance. On the first levels of a large tree, the Propel 1.3 implementation of Nested sets used to consume a very large amount of memory and CPU to retrieve the siblings or the children of a given node. This is no longer true with the new behavior. This performance boost comes at a small price: you must add a new "level" column to your nested set models, and let the behavior update this column for the whole tree. For instance, if you used nested sets to keep a list of categories, the schema used to look like: {{{ #!xml
}}} The upgrade path is then pretty straightforward: 1 - Update the schema, by removing the `treeMode` and `nestedSet` attributes and adding the `nested_set` behavior and the `tree_level` column: {{{ #!xml
}}} 2 - Rebuild the model 3 - Change the parent class of your model classes (object and peer) that used the nested set `treeMode`: {{{ #!php }}} Now, every time you save a new `Post` object, Propel will compose its slug according to the pattern defined in the behavior parameter and save it in an additional `slug` column: {{{ #!php setTitle('How Is Life On Earth?'); $post1->setContent('Lorem Ipsum...'); $post1->save(); echo $post1->getSlug(); // '/posts/how-is-life-on-earth' }}} Propel replaces every name enclosed between brackets in the slug pattern by the related column value. It also cleans up the string to make it URL-compatible, and ensures that it is unique. If you use this slug in URLs, you will need to retrieve a `Post` object based on it. This is just a one-liner: {{{ #!php findOneBySlug('/posts/how-is-life-on-earth'); }}} There are many ways to customize the `sluggable` behavior to match the needs of your applications. Check the new [wiki:Users/Documentation/1.5/Behaviors/sluggable sluggable behavior documentation] for more details. === Concrete Table Inheritance Behavior === Propel has offered [wiki:Users/Documentation/1.5/Inheritance#SingleTableInheritance Single Table Inheritance] for a long time. But for complex table inheritance needs, it is necessary to provide [http://martinfowler.com/eaaCatalog/concreteTableInheritance.html Concrete Table Inheritance]. Starting with Propel 1.5, this inheritance implementation is supported through the new `concrete_inheritance` behavior. In the following example, the `article` and `video` tables use this behavior to inherit the columns and foreign keys of their parent table, `content`: {{{ #!xml
}}} The behavior copies the columns of the parent table to the child tables. That means that the generated `Article` and `Video` models have a `Title` property and a `Category` relationship: {{{ #!php setName('Movie'); $cat->save(); // create a new Article $art = new Article(); $art->setTitle('Avatar Makes Best Opening Weekend in the History'); $art->setCategory($cat); $art->setContent('With $232.2 million worldwide total, Avatar had one of the best-opening weekends in the history of cinema.'); $art->save(); // create a new Video $vid = new Video(); $vid->setTitle('Avatar Trailer'); $vid->setCategory($cat); $vid->setResourceLink('http://www.avatarmovie.com/index.html') $vid->save(); }}} If Propel stopped there, the `concrete_inheritance` behavior would only provide a shorcut to avoid repeating tags in the schema. But wait, there is more: the `Article` and `Video` classes actually extend the `Content` class: {{{ #!php getCategory()->getName(); } } echo $art->getCategoryName(); // 'Movie' echo $vid->getCategoryName(); // 'Movie' }}} And the true power of Propel's Concrete Table Inheritance is that every time you save an `Article` or a `Video` object, Propel saves a copy of the `title` and `category_id` columns in a `Content` object. Consequently, retrieving objects regardless of their child type becomes very easy: {{{ #!php find(); foreach ($conts as $content) { echo $content->getTitle() . "(". $content->getCategoryName() ")/n"; } // Avatar Makes Best Opening Weekend in the History (Movie) // Avatar Trailer (Movie) }}} The resulting relational model is denormalized - in other terms, data is copied across tables - but the behavior takes care of everything for you. That allows for very effective read queries on complex inheritance structures. Check out the brand new [wiki:Users/Documentation/1.5/Inheritance#ConcreteTableInheritance Inheritance Documentation] for more details on using and customizing this behavior. === Sortable Behavior === Have you ever enhanced a Propel Model to give it the ability to move up or down in an ordered list? The `sortable` behavior, new in Propel 1.5, offers exactly that... and even more. As usual for behaviors, activate `sortable` in your `schema.yml`: {{{ #!xml
}}} Then rebuild your model, and you're done. You have just created an ordered task list for users: {{{ #!php setTitle('Wash the dishes'); $t1->setUser($paul); $t1->save(); echo $t1->getRank(); // 1 $t2 = new Task(); $t2->setTitle('Do the laundry'); $t2->setUser($paul); $t2->save(); echo $t2->getRank(); // 2 $t3 = new Task(); $t3->setTitle('Rest a little'); $t3->setUser($john); $t3->save() echo $t3->getRank(); // 1, because John has his own task list // retrieve the tasks $allPaulsTasks = TaskPeer::retrieveList($scope = $paul->getId()); $allJohnsTasks = TaskPeer::retrieveList($scope = $john->getId()); $t1 = TaskPeer::retrieveByRank($rank = 1, $scope = $paul->getId()); $t2 = $t1->getNext(); $t2->moveUp(); echo $t2->getRank(); // 1 echo $t1->getRank(); // 2 }}} This new behavior is fully unit tested and very customizable. Check out all you can do with `sortable` in the [wiki:Users/Documentation/1.5/Behaviors/sortable sortable behavior documentation]. === Timestampable Behavior === This behavior is not new, since it was introduced in Propel 1.4. However, with the introduction of model queries, it gains specific query methods that will ease your work when retrieving objects based on their update date: {{{ #!php recentlyUpdated() // adds a minimum value for the update date ->lastUpdatedFirst() // orders the results by descending update date ->find(); }}} == Better `toArray()` == When you call `toArray()` on a model object, you can now ask for the related objects: {{{ #!php toArray($keyType = BasePeer::TYPE_COLNAME, $includeLazyLoadColumns = true, $includeForeignObjects = true); print_r($bookArray); => 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' ) ) }}} Only the related objects that were already hydrated appear in the result, so `toArray()` never issues additional queries. Together with the ability to return arrays instead of objects when using `PropelQuery`, this addition will help to debug and optimize model code. == Better Oracle Support == The Oracle adapter for the generator, the reverse engineering, and the runtime components have been greatly improved. This should provide an easier integration of Propel with an Oracle database. == Code Cleanup == === Directory Structure Changes === The organization of the Propel runtime and generator code has been reworked, in order to make navigation across Propel classes easier for developers. End users should see no difference, apart if your `build.properties` references alternate builder classes in the Propel code. In that case, you will need to update your `build.properties` with the new paths. For instance, a reference to: {{{ #!ini propel.builder.peer.class = propel.engine.builder.om.php5.PHP5PeerBuilder }}} Must be changed to: {{{ #!ini propel.builder.peer.class = builder.om.PHP5PeerBuilder }}} Browse the Propel generator directory structure to find the classes you need. === DebugPDO Refactoring === To allow custom connection handlers, the debug code that was written in the `DebugPDO` class has been moved to `PropelPDO`. The change is completely backwards compatible, but makes it easier to connect to a database without using PDO. During the change, the [wiki:Users/Documentation/1.5/07-Logging documentation about Propel logging and debugging features] was rewritten and should now be clearer. == propel-gen Script Modifications == The `propel-gen` script no longer requires a path to the project directory if you call it from a project directory. That means that calling `propel-gen` with a single argument defaults to expecting a task name: {{{ > cd /path/to/my/project > propel-gen reverse }}} By default, the `propel-gen` command called without a task name defaults to the `main` task (and builds the model, the SQL, and the generation). Note: The behavior of the `propel-gen` script when called with one parameter differs from what it used to be in Propel 1.4, where the script expected a path in every situation. So the following syntax won't work anymore: {{{ > propel-gen /path/to/my/project }}} Instead, use either: {{{ > cd /path/to/my/project > propel-gen }}} or: {{{ > propel-gen /path/to/my/project main }}} == License Change == Propel is more open-source than ever. To allow for an easier distribution, the open-source license of the Propel library changes from LGPL3 to MIT. This [http://en.wikipedia.org/wiki/MIT_License MIT License] is also known as the X11 License. This change removes a usage restriction enforced by the LGPL3: you no longer need to release any modifications to the core Propel source code under a LGPL compatible license. Of course, you still have the right to use, copy, modify, merge, publish, distribute, sublicense, and/or sell Propel. In other terms, you can do whatever you want with the Propel code, without worrying about the license, as long as you leave the LICENSE file within. == Miscellaneous == * Generated model classes now offer a `fromArray()` and a `toArray()` method by default. This feature existed before, but was disabled by default in the `build.properties`. The `addGenericAccessors` and `addGenericMutators` settings are therefore enabled by default in Propel 1.5. * You can now prefix all the table names of a database schema by setting the `tablePrefix` attribute of the `` tag. * The `addIncludes` build property introduced in Propel 1.4 is now set to `false` by default. That means that the runtime autoloading takes care of loading all classes at runtime, including generated Base classes. * A bugfix in the name generator for related object getter in tables with two foreign keys related to the same table may have introduced problems in applications relying on old (wrong) names. Check your generated base model classes for the `getXXXrelatedByYYY()` and modify the application code relying on it if it exists. A good rule of thumb to avoid problems in such case is to name your relations by using the `phpName` and `refPhpName` attributes in the `` element in the schema. * XSL transformation of your schemas is no longer enabled by default. Turn the `propel.schema.transform` setting to `true` in your `build.properties` to enable it again. This change removes the requirement on the libxslt extention for Propel. * `ModelObject::addSelectColumns()` now accepts an additional parameter to allow the use of table aliases * Added `ModelObject::clear()` to reinitialize a model object * Added `ModelObject::isPrimaryKeyNull()` method to check of an object was hydrated with no values (in case of a left join) * Added `Criteria::addSelectModifier($modifier)` to add more than one select modifier (e.g. 'SQL_CALC_FOUND_ROWS', 'HIGH_PRIORITY', etc.) * Added `PeerClass::addGetPrimaryKeyFromRow()` to retrieve the Primary key from a result row * Added a new set of constants in the generated Peer class to list column names without the table name (this is `BasePeer::TYPE_RAW_COLNAME`) * Removed references to Creole in the code (Propel uses PDO instead of Creole since version 1.3)