What's new in Propel 1.4?
Propel 1.4 is a backwards compatible evolution of Propel 1.3. It offers lots of bugfixes, some very interesting new features, speed enhancements, and a very simple upgrade path: rebuild your model after updating Propel, and your application works as always. Except it works better...
Pre and Post Hooks For save() And delete() Methods
The save() and delete() methods of your generated objects are now easier to override. In fact, Propel looks for one of the following methods in your objects and executes them when needed:
<?php preInsert() // code executed before insertion of a new object postInsert() // code executed after insertion of a new object preUpdate() // code executed before update of an existing object postUpdate() // code executed after update of an existing object preSave() // code executed before saving an object (new or existing) postSave() // code executed after saving an object (new or existing) preDelete() // code executed before deleting an object postDelete() // code executed after deleting an object
So for instance, you can force the update of a created_at column in a book table before every insertion as follows:
<?php class Book extends BaseBook { public function preInsert(PropelPDO $con = null) { $this->setCreatedAt(time()); } }
Since this new feature adds a small overhead to write operations, you can deactivate it in your build properties by setting propel.addHooks to false.
# ------------------- # TEMPLATE VARIABLES # ------------------- propel.addHooks = false
Behaviors
You can now package and reuse behaviors across your models. For instance, keeping the date of creation of an object and the date of latest update is as simple as declaring the timestampable behavior in the table declaration:
<table name="book"> <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" /> <column name="title" type="VARCHAR" required="true" /> <behavior name="timestampable" /> </table>
Rebuild your model, and you now have two new columns in the book table automatically set to the creation and update dates:
<?php $b = new Book(); $b->setTitle('War And Peace'); $b->save(); echo $b->getCreatedAt(); // 2009-10-02 18:14:23 echo $b->getUpdatedAt(); // 2009-10-02 18:14:23 $b->setTitle('Anna Karenina'); $b->save(); echo $b->getCreatedAt(); // 2009-10-02 18:14:23 echo $b->getUpdatedAt(); // 2009-10-02 18:14:25
Propel behaviors occur at build time, so they have absolutely no overhead. They can modify the database schema, alter the methods generated by Propel, and add methods of their own in both the model and Peer classes. As a matter of fact, this new feature is so powerful that it requires a documentation on its own. Read it in the HowTos? section.
Propel 1.4 already bundles a few behaviors, like timestampable and soft_delete. Check the documentation linked above for more details.
Joins with multiple conditions
You can now create joins with any number of conditions and any comparator using the new Criteria::addMultipleJoin() method.
<?php $c = new Criteria(); $c->addMultipleJoin(array( array(ReaderFavoritePeer::BOOK_ID, BookOpinionPeer::BOOK_ID), array(ReaderFavoritePeer::READER_ID, BookOpinionPeer::READER_ID)) Criteria::INNER_JOIN);
// SQL result SELECT ... FROM reader_favorite INNER JOIN book_opinion ON (reader_favorite.BOOK_ID = book_opinion.BOOK_ID AND reader_favorite.READER_ID = book_opinion.READER_ID)
You can add a third operator to each join condition to allow complex joins:
<?php $c = new Criteria(); $c->addMultipleJoin(array( array(Book::USER_ID, UserPeer::ID)) array(UserPeer::RANK, 12, Criteria::GREATER_THAN), Criteria::LEFT_JOIN);
// SQL result SELECT ... FROM book LEFT JOIN user ON (book.USER_ID = user.ID AND user.RANK > 12)
Note that the former way to define a composite join using arrays as arguments of addJoin() is deprecated.
Full Query Logging
Propel can now log all the queries issued to the database in their exact syntax, including parameter binding. That means that you can easily copy a query from the Propel logs and execute it in your database to see the result immediately.
In addition, Propel can log the time and memory spent on every query, or only on slow query, depending on a treshold that you can customize.
Last but not least, you can check the number of executer queries and the SQL code of the latest executed query at runtime.
Propel 1.2 users may not find these features spectacular, but by switching from Creole to PDO, Propel lost its powerful logging abilities in version 1.3. Propel 1.4 logging is now on par with Propel 1.2 logging again.
All this is possible thanks to a new connection class, called DebugPDO. To enable it, just change the connection <classname> to 'DebugPDO' in the runtime-conf.xml file, as follows:
<?xml version="1.0"?> <config> <propel> <datasources default="bookstore"> <datasource id="bookstore"> <adapter>sqlite</adapter> <connection> <classname>DebugPDO</classname>
That's all. After this change, Propel logs all the queries executed at runtime in its default log file:
time: 0.002 sec | mem: 1.4 MB | INSERT INTO publisher (`ID`,`NAME`) VALUES (NULL,'William Morrow') time: 0.000 sec | mem: 1.4 MB | INSERT INTO author (`ID`,`FIRST_NAME`,`LAST_NAME`) VALUES (NULL,'J.K.','Rowling') time: 0.000 sec | mem: 1.6 MB | INSERT INTO book (`ID`,`TITLE`,`ISBN`,`PRICE`,`PUBLISHER_ID`,`AUTHOR_ID`) VALUES (NULL,'Harry Potter and the Order of the Phoenix','043935806X',10.99,53,58) time: 0.000 sec | mem: 1.7 MB | INSERT INTO review (`ID`,`REVIEWED_BY`,`REVIEW_DATE`,`RECOMMENDED`,`BOOK_ID`) VALUES (NULL,'Washington Post','2009-10-04',1,52) ... time: 0.001 sec | mem: 2.1 MB | SELECT bookstore_employee_account.EMPLOYEE_ID, bookstore_employee_account.LOGIN FROM `bookstore_employee_account` WHERE bookstore_employee_account.EMPLOYEE_ID=25
Check the Full Query Logging Documentation? for details on the DebugPDO documentation.
Generated __toString() in Base Object
Propel can generate a __toString() method for your Model objects if you define a column as primaryString in the schema:
<table name="book" phpName="Book"> .. <column name="title" type="varchar" size="125" primaryString="true" /> .. </table>
After a model build, the generated BaseBook class offers the following magic method:
<?php public function __toString() { return (string) $this->getTitle(); }
That means that the default string representation of Book objects is the value of the title column:
<?php $book = new book(); $book->setTitle('War And Peace'); echo $book; // 'War And Peace'
New Peer constant: OM_CLASS
Did you ever try to get the Propel Model class related to a Peer class? If you did, you probably noticed that the only available constant providing this information also contained package information:
<?php // in BaseBookPeer.php /** A class that can be returned by this peer. */ const CLASS_DEFAULT = 'bookstore.Book';
This can be useful if you don't use autoloading; but if you do, retrieving the actual Propel model class name involved some string operations to remove the package information. This is no more necessary in Propel 1.4, since the Peer classes offer a new constant:
<?php // in BaseBookPeer.php /** the related Propel class for this table */ const OM_CLASS = 'Book';
The generated Peer methods now use this constant and avoid string operations. Therefore, they should be slightly faster.
PropelPager Implements Countable and Iterator Interfaces
Did you know Propel offered a pagination utility class? It just got better, by now supporting the Countable and Iterator interfaces. That means that you can manipulate a PropelPager object as if it was an array:
<?php $c = new Criteria(); $c->add(BookPeer::AUTHOR, $authorId); $pager = new PropelPager($c, 'BookPeer', 'doSelect', $page = 1, $rowsPerPage = 20); if(count($pager)) // if the current page has results { foreach($pager as $book) // get pager results and iterate on them { echo $book->getTitle(); } }
Note that the PropelPager utility has a brand new documentation in the HowTos? section.
Better Introspection at Runtime
A few methods were added to the Map classes to ease runtime introspection, as well as a new RelationMap class to describe database relationships:
TableMap DatabaseMap::getTableByPhpName($name) // TableMap object by object model name, e.g. 'Book'
ColumnMap DatabaseMap::getColumn($name) // ColumnMap object by fully qualified name, e.g. book.AUTHOR_ID
Array TableMap::getPrimaryKeys() // List of the ColumnMap objects corresponding to the table primary keys
Array TableMap::getForeignKeys() // List of the ColumnMap objects corresponding to the table foreign keys
Array TableMap::getRelations() // List of the table relationships, as RelationMap objects
String TableMap::getPackage() // Package of the table
mixed ColumnMap::getDefaultValue() // Default value defined in the schema for this column
TableMap ColumnMap::getRelatedTable() // Related TableMap object by foreign key
ColumnMap ColumnMap::getRelatedColumn() // Related ColumnMap object by foreign key
RelationMap ColumnMap::getRelation() // Related RelationMap object
integer RelationMap::getType() // RelationMap::ONE_TO_MANY, RelationMap::MANY_TO_ONE, or RelationMap::ONE_TO_ONE
string RelationMap::getOnDelete() // ON DELETE directive, e.g. 'SET NULL'
TableMap RelationMap::getLocalTable() // Local TableMap object (the one bearing the fkey)
TabkeMap RelationMap::getForeignTable() // Foreign TableMap object
Array RelationMap::getColumnMappings() // List of local => foreign column for this relation, e.g array('book.PUBLISHER_ID' => 'publisher.ID')
Array RelationMap::getLocalColumns() // List of the ColumnMap objects on the local side of the relation
Array RelationMap::getForeignColumns() // List of the ColumnMap objects on the foreign side of the relation
Check the new Runtime Introspection? documentation in the HowTos? section for a complete description of the introspection API.
Also, it is now much easier to get the name of the Propel object class from the Peer class:
echo BookPeer::getOMClass() => 'bookstore.Book' echo BookPeer::getOMClass($withPrefix = false) => 'Book'
MapBuilders are gone
Runtime introspection used to rely on runtime builder classes for TableMap objects, called MapBuilders. These classes are not generated anymore by Propel. Instead, Propel generates TableMap classes that are easier to deal with.
The standard way to get a TableMap through the Peer classes works just the same as before:
<?php $bookTableMap = BookPeer::getTableMap();
But building TableMaps by hand is a lot easier:
<?php // Propel 1.3 way to initialize a TableMap $className = 'Book'; $mapBuilderClass = $className . 'MapBuilder'; $mapBuilder = new $mapBuilderClass(); if (!$mapBuilder->isBuilt()) { $mapBuilder->doBuild(); } $bookTableMap = $databaseMap->getTable('book'); // Propel 1.4 way to initialize a TableMap $className = 'Book'; $tableMapClass = $className . 'TableMap'; $bookTableMap = $databaseMap->addTableFromMapClass($tableMapClass);
Note that if you want to make sure that all the tables related to a given table have their TableMaps loaded, you just need to ask for the table relations:
<?php // build all the TableMap objects of tables related to Book $relations = $bookTableMap->getRelations();
New Build Options
You now have more control over the generated classes, through three new settings in build.properties:
propel.addValidateMethod = {true}|false
Whether to add validate() method to your classes. Set to false if you don't use Propel validation.
propel.addIncludes = {true}|false
Whether to add require statements on the generated stub classes. Set to false if you autoload every class at runtime.
propel.addHooks = {true}|false
Whether to support pre- and post- hooks on save() and delete() methods. Set to false if you never use these hooks for a small speed boost.
Foreign Key Retrievers Now Use Instance Pool
When you retrieve an object related to the current model object by a foreign key, you usually use a method generated by Propel, as follows:
<?php $author = $book->getAuthor(); // $author is an Author instance
Behind the curtain, this method uses the author_id property of the current book object, makes a query to the database for the related author record, and hydrates an Author object with the result. But what if you already have hydrated this author object earlier in the script? Thanks to instance pooling, Propel remembers the objects it has in memory, which can save a lot of database queries. For instance:
<?php foreach($author->getBooks() as $book) { echo $book->getTitle(); echo $book->getAuthor()->getName(); }
In Propel 1.3, this code would issue n+1 queries to the database (n being the number of books written by $author). But with Propel 1.4, this code only executes one query. As the getAuthor() method is called, Propel knows that the author of each book is already in memory, and therefore skips the database query and the hydration process altogether. hte result is a slight speed boost, and a decrease in the number of queries executed per page.
BasePeer::populateStmtValues() is now public
You can now use a SQL query of your own, or modify the one returned by BasePeer::createSelectSql(), and use the binding capabilities of Propel to populate a query with a set of values. Propel uses the PDO type of the column to do the binding, so this method may save you some hassle:
<?php $sql = BasePeer::createSelectSql($criteria, $params); $sql = "INSERT INTO temp_table_name $sql"; $stmt = $con->prepare($sql); BasePeer::populateStmtValues($stmt, $params, $dbMap, $db); $stmt->execute();
Documentation Reorganization
The Propel documentation now lies in the Subversion repository. It becomes easier to contribute and trace documentation changes, attach corrections to tickets, and bundle the Propel library as a self-containing package.
Of course, the documentation has been updated to reflect all the changes described above, and all the new features are fully documented.
Twice As Many Unit Tests
The Propel 1.4 development process introduced Test-Driven Development. As a consequence, the number of unit tests in the Propel codebase has dramatically increased. In fact, it has more than doubled since Propel 1.3. Propel is still far from a large unit test code coverage, but the stability and robustness of the Propel library get better every day.