Skip to content
Browse files

Added documentation to Propel2

**Notes:**

- It's just a git clone from the propelorm.github.com repo;
- It's to ease the documentation update process during the development;
- It won't stay in this repo (will be removed when Propel2 will be
  stable);
- You are encouraged to update documentation with your commits.
  • Loading branch information...
1 parent 045b5e4 commit 1d891f0cb7ba2ff79ebb12d0a5a77e28d746bd71 @willdurand willdurand committed Nov 10, 2011
Showing with 14,863 additions and 0 deletions.
  1. +1 −0 documentation/.gitignore
  2. +6 −0 documentation/404.html
  3. +1 −0 documentation/CNAME
  4. +2 −0 documentation/_config.yml
  5. +21 −0 documentation/_includes/footer.html
  6. 0 documentation/_includes/ga.html
  7. +10 −0 documentation/_includes/header.html
  8. +17 −0 documentation/_includes/navbar.html
  9. +33 −0 documentation/_layouts/base.html
  10. +7 −0 documentation/_layouts/default.html
  11. +14 −0 documentation/_layouts/documentation.html
  12. +34 −0 documentation/_layouts/home.html
  13. +129 −0 documentation/behaviors/aggregate-column.markdown
  14. +91 −0 documentation/behaviors/alternative-coding-standards.markdown
  15. +249 −0 documentation/behaviors/archivable.markdown
  16. +74 −0 documentation/behaviors/auto-add-pk.markdown
  17. +204 −0 documentation/behaviors/delegate.markdown
  18. +221 −0 documentation/behaviors/i18n.markdown
  19. +358 −0 documentation/behaviors/nested-set.markdown
  20. +103 −0 documentation/behaviors/query-cache.markdown
  21. +132 −0 documentation/behaviors/sluggable.markdown
  22. +128 −0 documentation/behaviors/soft-delete.markdown
  23. +273 −0 documentation/behaviors/sortable.markdown
  24. +91 −0 documentation/behaviors/timestampable.markdown
  25. +263 −0 documentation/behaviors/versionable.markdown
  26. +225 −0 documentation/contribute.markdown
  27. +38 −0 documentation/cookbook/adding-additional-sql-files.markdown
  28. +48 −0 documentation/cookbook/copying-persisted-objects.markdown
  29. +173 −0 documentation/cookbook/customizing-build.markdown
  30. +42 −0 documentation/cookbook/dbdesigner.markdown
  31. +59 −0 documentation/cookbook/index.markdown
  32. +301 −0 documentation/cookbook/multi-component-data-model.markdown
  33. +130 −0 documentation/cookbook/namespaces.markdown
  34. +98 −0 documentation/cookbook/replication.markdown
  35. +158 −0 documentation/cookbook/runtime-introspection.markdown
  36. +189 −0 documentation/cookbook/symfony1/how-to-use-Propel i18n-behavior-with-sf1.4.markdown
  37. +195 −0 documentation/cookbook/symfony1/how-to-use-old-SfPropelBehaviori18n-with-sf1.4.markdown
  38. +12 −0 documentation/cookbook/symfony1/index.markdown
  39. +144 −0 documentation/cookbook/symfony1/init-a-Symfony-project-with-Propel-git-way.markdown
  40. BIN documentation/cookbook/symfony2/images/basic_form.png
  41. BIN documentation/cookbook/symfony2/images/many_to_many_form.png
  42. BIN documentation/cookbook/symfony2/images/many_to_many_form_with_existing_objects.png
  43. BIN documentation/cookbook/symfony2/images/one_to_many_form.png
  44. BIN documentation/cookbook/symfony2/images/one_to_many_form_with_collection.png
  45. +14 −0 documentation/cookbook/symfony2/index.markdown
  46. +452 −0 documentation/cookbook/symfony2/mastering-symfony2-forms-with-propel.markdown
  47. +407 −0 documentation/cookbook/symfony2/symfony2-and-propel-in-real-life.markdown
  48. +151 −0 documentation/cookbook/symfony2/the-symfony2-security-component-and-propel.markdown
  49. +363 −0 documentation/cookbook/symfony2/working-with-symfony2.markdown
  50. +30 −0 documentation/cookbook/user-contributed-behaviors.markdown
  51. +171 −0 documentation/cookbook/using-mssql-server.markdown
  52. +100 −0 documentation/cookbook/using-sql-schemas.markdown
  53. +194 −0 documentation/cookbook/working-with-advanced-column-types.markdown
  54. +117 −0 documentation/cookbook/working-with-existing-databases.markdown
  55. +432 −0 documentation/cookbook/writing-behavior.markdown
  56. +68 −0 documentation/css/base_syntax.css
  57. +322 −0 documentation/css/layout.css
  58. +121 −0 documentation/css/markdown.css
  59. +61 −0 documentation/css/mobile.css
  60. +19 −0 documentation/css/syntax.css
  61. +149 −0 documentation/documentation/01-installation.markdown
  62. +356 −0 documentation/documentation/02-buildtime.markdown
  63. +315 −0 documentation/documentation/03-basic-crud.markdown
  64. +399 −0 documentation/documentation/04-relationships.markdown
  65. +240 −0 documentation/documentation/05-validators.markdown
  66. +281 −0 documentation/documentation/06-transactions.markdown
  67. +415 −0 documentation/documentation/07-behaviors.markdown
  68. +434 −0 documentation/documentation/08-logging.markdown
  69. +498 −0 documentation/documentation/09-inheritance.markdown
  70. +357 −0 documentation/documentation/10-migrations.markdown
  71. +89 −0 documentation/documentation/index.markdown
  72. +723 −0 documentation/documentation/whats-new.markdown
  73. +81 −0 documentation/download.markdown
  74. BIN documentation/images/background.png
  75. BIN documentation/images/github.gif
  76. BIN documentation/images/info.png
  77. BIN documentation/images/propel-logo.png
  78. +60 −0 documentation/index.markdown
  79. +9 −0 documentation/js/ga.js
  80. +12 −0 documentation/js/jquery.tableofcontents.min.js
  81. +730 −0 documentation/reference/active-record.markdown
  82. +331 −0 documentation/reference/buildtime-configuration.markdown
  83. +16 −0 documentation/reference/index.markdown
  84. +1,255 −0 documentation/reference/model-criteria.markdown
  85. +311 −0 documentation/reference/runtime-configuration.markdown
  86. +470 −0 documentation/reference/schema.markdown
  87. +36 −0 documentation/support.markdown
View
1 documentation/.gitignore
@@ -0,0 +1 @@
+_site/
View
6 documentation/404.html
@@ -0,0 +1,6 @@
+<script type="text/javascript">
+ var regex = /wiki|ticket/gi
+ if (window.location.href.match(regex)) {
+ window.location = window.location.href.replace(/www/gi, 'trac');
+ }
+</script>
View
1 documentation/CNAME
@@ -0,0 +1 @@
+www.propelorm.org
View
2 documentation/_config.yml
@@ -0,0 +1,2 @@
+pygments: true
+permalink: none
View
21 documentation/_includes/footer.html
@@ -0,0 +1,21 @@
+ <div class="bottom">
+ <p>
+ <ul>
+ <li class="element first">
+ <a class="link" href="/documentation/">Documentation</a>&nbsp;|&nbsp;
+ </li>
+ <li class="element">
+ <a class="link" href="/support.html">Support</a>&nbsp;|&nbsp;
+ </li>
+ <li class="element">
+ <a class="link" href="/download.html">Download</a>&nbsp;|&nbsp;
+ </li>
+ <li class="element">
+ <a class="link" href="/contribute.html">Contribute</a>&nbsp;|&nbsp;
+ </li>
+ <li class="last element">
+ <a class="link" href="http://propel.posterous.com/">Blog</a>
+ </li>
+ </ul>
+ </p>
+ </div>
View
0 documentation/_includes/ga.html
No changes.
View
10 documentation/_includes/header.html
@@ -0,0 +1,10 @@
+ <meta name="language" content="en" />
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
+ <!-- Stylesheets -->
+ <link rel="stylesheet" type="text/css" media="all" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset-min.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="/css/layout.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="/css/markdown.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="/css/base_syntax.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="/css/syntax.css" />
+ <link rel="stylesheet" type="text/css" media="only screen and (max-device-width: 480px)" href="/css/mobile.css" />
View
17 documentation/_includes/navbar.html
@@ -0,0 +1,17 @@
+ <ul class="topmenu">
+ <li class="element first">
+ <a class="link" href="/documentation/">Documentation</a>
+ </li>
+ <li class="element">
+ <a class="link" href="/support.html">Support</a>
+ </li>
+ <li class="element">
+ <a class="link" href="/download.html">Download</a>
+ </li>
+ <li class="element">
+ <a class="link" href="/contribute.html">Contribute</a>
+ </li>
+ <li class="last element">
+ <a class="link" href="http://propel.posterous.com/">Blog</a>
+ </li>
+ </ul>
View
33 documentation/_layouts/base.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
+ <head>
+ <title>{{ page.title }} - Propel</title>
+ {% include header.html %}
+ <!-- JavaScripts -->
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.js"></script>
+ <script type="text/javascript" src="/js/jquery.tableofcontents.min.js" charset="utf-8"></script>
+ </head>
+ <body>
+ <div id="fond">
+ <div class="layout">
+ <ul class="rootnav">
+ <li class="element">
+ <a class="link" href="/">Propel</a>
+ </li>
+ </ul>
+ {% include navbar.html %}
+ <div class="ribbon">
+ <a href="https://github.com/propelorm/Propel" rel="me">Fork me on GitHub</a>
+ </div>
+ <div class="content">
+ {{ content }}
+ <p class="fork_and_edit">
+ Found a typo ? Something is wrong in this documentation ? Just <a href="http://github.com/propelorm/propelorm.github.com/edit/master{{ page.url|replace:'.html','' }}.markdown">fork and edit</a> it !
+ </p>
+ </div>
+ </div>
+ {% include footer.html %}
+ </div>
+ <script type="text/javascript" src="/js/ga.js" charset="utf-8"></script>
+ </body>
+</html>
View
7 documentation/_layouts/default.html
@@ -0,0 +1,7 @@
+---
+layout: base
+---
+
+<div class="markdown">
+ {{ content }}
+</div>
View
14 documentation/_layouts/documentation.html
@@ -0,0 +1,14 @@
+---
+layout: base
+---
+
+<ul class="toc"></ul>
+<div class="markdown">
+ {{ content }}
+</div>
+
+<script type="text/javascript">
+ $(document).ready(function(){
+ $('.toc').tableOfContents(null, { startLevel: 2 });
+ });
+</script>
View
34 documentation/_layouts/home.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
+ <head>
+ <title>Propel - The Fast PHP5 ORM</title>
+ {% include header.html %}
+ </head>
+ <body class="home">
+ <div id="fond">
+ <div class="layout">
+ {% include navbar.html %}
+ <div id="banner">
+ <div id="text">
+ <h2 class="title">
+ <a href="/">Propel</a>
+ </h2>
+ <p class="title">
+ Smart, easy object persistence.
+ </p>
+ </div>
+ </div>
+ <div class="ribbon">
+ <a href="https://github.com/propelorm/Propel" rel="me">Fork me on GitHub</a>
+ </div>
+ <div class="content">
+ <div class="markdown">
+ {{ content }}
+ </div>
+ </div>
+ </div>
+ {% include footer.html %}
+ </div>
+ <script type="text/javascript" src="/js/ga.js" charset="utf-8"></script>
+ </body>
+</html>
View
129 documentation/behaviors/aggregate-column.markdown
@@ -0,0 +1,129 @@
+---
+layout: documentation
+title: Aggregate Column Behavior
+---
+
+# Aggregate Column Behavior #
+
+The `aggregate_column` behavior keeps a column updated using an aggregate function executed on a related table.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `aggregate_column` behavior to a table. You must provide parameters for the aggregate column `name`, the foreign table name, and the aggegate `expression`. For instance, to add an aggregate column keeping the comment count in a `post` table:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" autoIncrement="true" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="aggregate_column">
+ <parameter name="name" value="nb_comments" />
+ <parameter name="foreign_table" value="comment" />
+ <parameter name="expression" value="COUNT(id)" />
+ </behavior>
+</table>
+<table name="comment">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" autoIncrement="true" />
+ <column name="post_id" type="INTEGER" />
+ <foreign-key foreignTable="post" onDelete="cascade">
+ <reference local="post_id" foreign="id" />
+ </foreign-key>
+</table>
+{% endhighlight %}
+
+Rebuild your model, and insert the table creation sql again. The model now has an additional `nb_comments` column, of type `integer` by default. And each time an record from the foreign table is added, modified, or removed, the aggregate column is updated:
+
+{% highlight php %}
+<?php
+$post = new Post();
+$post->setTitle('How Is Life On Earth?');
+$post->save();
+echo $post->getNbComments(); // 0
+$comment1 = new Comment();
+$comment1->setPost($post);
+$comment1->save();
+echo $post->getNbComments(); // 1
+$comment2 = new Comment();
+$comment2->setPost($post);
+$comment2->save();
+echo $post->getNbComments(); // 2
+$comment2->delete();
+echo $post->getNbComments(); // 1
+{% endhighlight %}
+
+The aggregate column is also kept up to date when related records get modified through a Query object:
+
+{% highlight php %}
+<?php
+CommentQuery::create()
+ ->filterByPost($post)
+ ->delete():
+echo $post->getNbComments(); // 0
+{% endhighlight %}
+
+## Customizing The Aggregate Calculation ##
+
+Any aggregate function can be used on any of the foreign columns. For instance, you can use the `aggregate_column` behavior to keep the latest update date of the related comments, or the total votes on the comments. You can even keep several aggregate columns in a single table:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" autoIncrement="true" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="aggregate_column">
+ <parameter name="name" value="nb_comments" />
+ <parameter name="foreign_table" value="comment" />
+ <parameter name="expression" value="COUNT(id)" />
+ </behavior>
+ <behavior name="aggregate_column">
+ <parameter name="name" value="last_comment" />
+ <parameter name="foreign_table" value="comment" />
+ <parameter name="expression" value="MAX(created_at)" />
+ </behavior>
+ <behavior name="aggregate_column">
+ <parameter name="name" value="total_votes" />
+ <parameter name="foreign_table" value="comment" />
+ <parameter name="expression" value="SUM(vote)" />
+ </behavior>
+</table>
+<table name="comment">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" autoIncrement="true" />
+ <column name="post_id" type="INTEGER" />
+ <foreign-key foreignTable="post" onDelete="cascade">
+ <reference local="post_id" foreign="id" />
+ </foreign-key>
+ <column name="created_at" type="TIMESTAMP" />
+ <column name="vote" type="INTEGER" />
+</table>
+{% endhighlight %}
+
+The behavior adds a `computeXXX()` method to the `Post` class to compute the value of the aggregate function. This method, called each time records are modified in the related `comment` table, is the translation of the behavior settings into a SQL query:
+
+{% highlight php %}
+<?php
+// in om/BasePost.php
+public function computeNbComments(PropelPDO $con)
+{
+ $stmt = $con->prepare('SELECT COUNT(id) FROM `comment` WHERE comment.POST_ID = :p1');
+ $stmt->bindValue(':p1', $this->getId());
+ $stmt->execute();
+ return $stmt->fetchColumn();
+}
+{% endhighlight %}
+
+You can override this method in the model class to customize the aggregate column calculation.
+
+## Customizing The Aggregate Column ##
+
+By default, the behavior adds one columns to the model. If this column is already described in the schema, the behavior detects it and doesn't add it a second time. This can be useful if you need to use a custom `type` or `phpName` for the aggregate column:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" autoIncrement="true" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="nb_comments" phpName="CommentCount" type="INTEGER" />
+ <behavior name="aggregate_column">
+ <parameter name="name" value="nb_comments" />
+ <parameter name="foreign_table" value="comment" />
+ <parameter name="expression" value="COUNT(id)" />
+ </behavior>
+</table>
+{% endhighlight %}
View
91 documentation/behaviors/alternative-coding-standards.markdown
@@ -0,0 +1,91 @@
+---
+layout: documentation
+title: Alternative Coding Standards Behavior
+---
+
+# Alternative Coding Standards Behavior #
+
+The `alternative_coding_standards` behavior changes the coding standards of the model classes generated by Propel to match your own coding style.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `alternative_coding_standards` behavior to a table:
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="alternative_coding_standards" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, and you're ready to go. The code of the model classes now uses an alternative set of coding standards:
+
+{% highlight php %}
+<?php
+// in om/BaseBook.php
+ /**
+ * Set the value of [title] column.
+ *
+ * @param string $v new value
+ * @return Table4 The current object (for fluent API support)
+ */
+ public function setTitle($v)
+ {
+ if ($v !== null)
+ {
+ $v = (string) $v;
+ }
+
+ if ($this->title !== $v)
+ {
+ $this->title = $v;
+ $this->modifiedColumns[] = BookPeer::TITLE;
+ }
+
+ return $this;
+ }
+
+// instead of
+
+ /**
+ * Set the value of [title] column.
+ *
+ * @param string $v new value
+ * @return Table4 The current object (for fluent API support)
+ */
+ public function setTitle($v)
+ {
+ if ($v !== null) {
+ $v = (string) $v;
+ }
+
+ if ($this->title !== $v) {
+ $this->title = $v;
+ $this->modifiedColumns[] = BookPeer::TITLE;
+ }
+
+ return $this;
+ } // setTitle()
+{% endhighlight %}
+
+The behavior replaces tabulations by whitespace (2 spaces by default), places opening brackets on newlines, removes closing brackets comments, and can even strip every comments in the generated classes if you wish.
+
+## Parameters ##
+
+Each of the new coding style rules has corresponding parameter in the behavior description. Here is the default configuration:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="alternative_coding_standards">
+ <parameter name="brackets_newline" value="true" />
+ <parameter name="remove_closing_comments" value="true" />
+ <parameter name="use_whitespace" value="true" />
+ <parameter name="tab_size" value="2" />
+ <parameter name="strip_comments" value="false" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+You can change these settings to better match your own coding styles.
View
249 documentation/behaviors/archivable.markdown
@@ -0,0 +1,249 @@
+---
+layout: documentation
+title: Archivable Behavior
+---
+
+# Archivable Behavior #
+
+The `archivable` behavior gives model objects the ability to be copied to an archive table. By default, the behavior archives objects on deletion, acting as a replacement of the [`soft_delete`](./soft-delete) behavior, which is deprecated.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `archivable` behavior to a table:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="archivable" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has one new table, `book_archive`, with the same columns as the original `book` table. This table stores the archived `books` together with their archive date. To archive an object, call the `archive()` method:
+
+{% highlight php %}
+<?php
+$book = new Book();
+$book->setTitle('War And Peace');
+$book->save();
+// copy the current Book to a BookArchive object and save it
+$archivedBook = $book->archive();
+{% endhighlight %}
+
+The archive table contains only the freshest copy of each archived objects. Archiving an object twice doesn't create a new record in the archive table, but updates the existing archive.
+
+The `book_archive` table has generated ActiveRecord and ActiveQuery classes, so you can browse the archive at will. The archived objects have the same primary key as the original objects. In addition, they contain an `ArchivedAt` property storing the date where the object was archived.
+
+{% highlight php %}
+<?php
+// find the archived book
+$archivedBook = BookArchiveQuery::create()->findPk($book->getId());
+echo $archivedBook->getTitle(); // 'War And Peace'
+echo $archivedBook->getArchivedAt(); // 2011-08-23 18:14:23
+{% endhighlight %}
+
+The ActiveRecord class of an `archivable` model has more methods to deal with the archive:
+
+{% highlight php %}
+// restore an object to the state it had when last archived
+$book->restoreFromArchive();
+// find the archived version of an existing book
+$archivedBook = $book->getArchive();
+// populate a book based on an archive
+$book = new book();
+$book->populateFromArchive($archivedBook);
+{% endhighlight %}
+
+By default, an `archivable` model is archived just before deletion:
+
+{% highlight php %}
+<?php
+$book = new Book();
+$book->setTitle('Sense and Sensibility');
+$book->save();
+// delete and archive the book
+$book->delete();
+echo BookQuery::create()->count(); // 0
+// find the archived book
+$archivedBook = BookArchiveQuery::create()
+ ->findOneByTitle('Sense and Sensibility');
+{% endhighlight %}
+
+>**Tip**<br />The behavior does not take care of archiving the related objects. This may be surprising on deletions if the deleted object has 'ON DELETE CASCADE' foreign keys. If you want to archive relations, override the generated `archive()` method in the ActiveRecord class with your custom logic.
+
+To recover deleted objects, use `populateFromArchive()` on a new object and save it:
+
+{% highlight php %}
+<?php
+// create a new object based on the archive
+$book = new Book();
+$book->populateFromArchive($archivedBook);
+$book->save();
+echo $book->getTitle(); // 'Sense and Sensibility'
+{% endhighlight %}
+
+If you want to delete an `archivable` object without archiving it, use the `deleteWithoutArchive()` method generated by the behavior:
+
+{% highlight php %}
+<?php
+// delete the book but don't archive it
+$book->deleteWithoutArchive();
+{% endhighlight %}
+
+## Archiving A Set Of Objects ##
+
+The `archivable` behavior also generates an `archive()` method on the generated ActiveQuery class. That means you can easily archive a set of objects, in the same way you archive a single object:
+
+{% highlight php %}
+<?php
+// archive all books having a title starting with "war"
+$nbArchivedObjects = BookQuery::create()
+ ->filterByTitle('War%')
+ ->archive();
+{% endhighlight %}
+
+`archive()` returns the number of archived objects, and not the current ActiveQuery object, so it's a termination method.
+
+>**Tip**<br />Since the `archive()` method doesn't duplicate archived objects, it must iterate over the results of the query to check whether each object has already been archived. In practice, `archive()` issues 2n+1 database queries, where `n` is the number of results of the query as returned by a `count()`.
+
+As explained earlier, an `archivable` model is archived just before deletion by default. This is also true when using the `delete()` and `deleteAll()` methods of the ActiveQuery class:
+
+{% highlight php %}
+<?php
+// delete and archive all books having a title starting with "war"
+$nbDeletedObjects = BookQuery::create()
+ ->filterByTitle('War%')
+ ->delete();
+
+// use deleteWithoutArchive() if you just want to delete
+$nbDeletedObjects = BookQuery::create()
+ ->filterByTitle('War%')
+ ->deleteWithoutArchive();
+
+// you can also turn off the query alteration on the current query
+// by calling setArchiveOnDelete(false) before deleting
+$nbDeletedObjects = BookQuery::create()
+ ->filterByTitle('War%')
+ ->setArchiveOnDelete(false)
+ ->delete();
+{% endhighlight %}
+
+## Archiving on Insert, Update, or Delete ##
+
+As explained earlier, the `archivable` behavior archives objects on deletion by default, but insertions and updates don't trigger the `archive()` method. You can disable the auto archiving on deletion, as well as enable it for insertion and update, in the behavior `<parameter>` tags. Here is the default configuration:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="archivable">
+ <parameter name="archive_on_insert" value="false" />
+ <parameter name="archive_on_update" value="false" />
+ <parameter name="archive_on_delete" value="true" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+If you turn on `archive_on_insert`, a call to `save()` on a new ActiveRecord object archives it - unless you call `saveWithoutArchive()`.
+
+If you turn on `archive_on_update`, a call to `save()` on an existing ActiveRecord object archives it, and a call to `update()` on an ActiveQuery object archives the results as well. You can still use `saveWithoutArchive()` on the ActiveRecord class and `updateWithoutArchive()` on the ActiveQuery class to skip archiving on updates.
+
+Of course, even if `archive_on_insert` or any of the similar parameters isn't turned on, you can always archive manually an object after persisting it by simply calling `archive()`:
+
+{% highlight php %}
+<?php
+// create a new object, save it, and archive it
+$book = new Book();
+$book->save();
+$book->archive();
+{% endhighlight %}
+
+## Archiving To Another Database ##
+
+The behavior can use another database connection for the archive table, to make it safer. To allow cross-database archives, you must declare the archive schema manually in another XML schema, and reference the archive class on in the behavior parameter:
+
+{% highlight xml %}
+<database name="main">
+ <table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="archivable">
+ <parameter name="archive_class" value="MyBookArchive" />
+ </behavior>
+ </table>
+</database>
+<database name="backup">
+ <table name="my_book_archive" phpName="MyBookArchive">
+ <column name="id" required="true" primaryKey="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="archived_at" type="TIMESTAMP" />
+ </table>
+</database>
+{% endhighlight %}
+
+The archive table must have the same columns as the archivable table, but without autoIncrements, and without foreign keys.
+
+With this setup, the behavior uses `MyBookArchive` and `MyBookArchiveQuery` for all operations on archives, and therefore uses the `backup` connection.
+
+## Migrating From `soft_delete` ##
+
+If you use `archivable` as a replacement for the `soft_delete` behavior, here is how you should update your code:
+
+{% highlight php %}
+<?php
+// do a soft delete
+$book->delete(); // with soft_delete
+$book->delete(); // with archivable
+
+// do a hard delete
+// with soft_delete
+$book->forceDelete();
+// with archivable
+$book->deleteWithoutArchive();
+
+// find deleted objects
+// with soft_delete
+$books = BookQuery::create()
+ ->includeDeleted()
+ ->where('Book.DeletedAt IS NOT NULL')
+ ->find();
+// with archivable
+$bookArchives = BookArchiveQuery::create()
+ ->find();
+
+// recover a deleted object
+// with soft_delete
+$book->unDelete();
+// with archivable
+$book = new Book();
+$book->populateFromArchive($bookArchive);
+$book->save();
+{% endhighlight %}
+
+## Additional Parameters ##
+
+You can change the name of the archive table added by the behavior by setting the `archive_table` parameter. If the table doesn't exist, the behavior creates it for you.
+
+{% highlight xml %}
+<behavior name="archivable">
+ <parameter name="archive_table" value="special_book_archive" />
+</behavior>
+{% endhighlight %}
+
+>**Tip**<br />The `archive_table` and `archive_class` parameters are mutually exclusive. You can only use either one of the two.
+
+You can also change the name of the column storing the archive date:
+
+{% highlight xml %}
+<behavior name="archivable">
+ <parameter name="archived_at_column" value="archive_date" />
+</behavior>
+{% endhighlight %}
+
+Alternatively, you can disable the addition of an archive date column altogether:
+
+{% highlight xml %}
+<behavior name="archivable">
+ <parameter name="log_archived_at" value="false" />
+</behavior>
+{% endhighlight %}
View
74 documentation/behaviors/auto-add-pk.markdown
@@ -0,0 +1,74 @@
+---
+layout: documentation
+title: AutoAddPk Behavior
+---
+
+# AutoAddPk Behavior #
+
+The `auto_add_pk` behavior adds a primary key columns to the tables that don't have one. Using this behavior allows you to omit the declaration of primary keys in your tables.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `auto_add_pk` behavior to a table:
+
+{% highlight xml %}
+<table name="book">
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="auto_add_pk" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, and insert the table creation sql. You will notice that the `book` table has two columns and not just one. The behavior added an `id` column, of type integer and autoincremented. This column can be used as any other column:
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->save();
+echo $b->getId(); // 1
+{% endhighlight %}
+
+This behavior is more powerful if you add it to the database instead of a table. That way, it will alter all tables not defining a primary key column - and leave the others unchanged.
+
+{% highlight xml %}
+<database name="bookstore" defaultIdMethod="native">
+ <behavior name="auto_add_pk" />
+ <table name="book">
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ </table>
+</database>
+{% endhighlight %}
+
+You can even enable it for all your databases by adding it to the default behaviors in your `build.properties` file:
+
+{% highlight ini %}
+propel.behavior.default = auto_add_pk
+{% endhighlight %}
+
+## Parameters ##
+
+By default, the behavior adds a column named `id` to the table if the table has no primary key. You can customize all the attributes of the added column by setting corresponding parameters in the behavior definition:
+
+{% highlight xml %}
+<database name="bookstore" defaultIdMethod="native">
+ <behavior name="auto_add_pk">
+ <parameter name="name" value="identifier" />
+ <parameter name="autoIncrement" value="false" />
+ <parameter name="type" value="BIGINT" />
+ </behavior>
+ <table name="book">
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ </table>
+</database>
+{% endhighlight %}
+
+Once you regenerate your model, the column is now named differently:
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->setIdentifier(1);
+$b->save();
+echo $b->getIdentifier(); // 1
+{% endhighlight %}
View
204 documentation/behaviors/delegate.markdown
@@ -0,0 +1,204 @@
+---
+layout: documentation
+title: Delegate Behavior
+---
+
+# Delegate Behavior #
+
+The `delegate` behavior allows a model to delegate methods to one of its relationships. This helps to isolate logic in a dedicated model, or to simulate [class table inheritance](http://martinfowler.com/eaaCatalog/classTableInheritance.html).
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `delegate` behavior to a table. In the `<parameters>` tag, specify the table that the current table delegates to as the `to` parameter:
+
+{% highlight xml %}
+<table name="account">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="login" type="VARCHAR" required="true" />
+ <column name="password" type="VARCHAR" required="true" />
+ <behavior name="delegate">
+ <parameter name="to" value="profile" />
+ </behavior>
+</table>
+<table name="profile">
+ <column name="email" type="VARCHAR" />
+ <column name="telephone" type="VARCHAR" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The delegate `profile` table is now related to the `account` table using a one-to-one relationship. That means that the behavior creates a foreign primary key in the `profile` table. In fact, everything happens as if you had defined the following schema:
+
+{% highlight xml %}
+<table name="account">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="login" type="VARCHAR" required="true" />
+ <column name="password" type="VARCHAR" required="true" />
+</table>
+<table name="profile">
+ <column name="id" required="true" primaryKey="true" type="INTEGER" />
+ <column name="email" type="VARCHAR" />
+ <column name="telephone" type="VARCHAR" />
+ <foreign-key foreignTable="account" onDelete="setnull" onUpdate="cascade">
+ <reference local="id" foreign="id" />
+ </foreign-key>
+</table>
+{% endhighlight %}
+
+>**Tip**<br />If the delegate table already has a foreign key to the main table, the behavior doesn't recreate it. It allows you to have full control over the relatiosnhip between the two tables.
+
+In addition, the ActiveRecord `Account` class now provides integrated delegation capabilities. That means that it offers to handle directly the columns of the `Profile` model, while in reality it finds or create a related `Profile` object and calls the methods on this delegate:
+
+{% highlight php %}
+<?php
+$account = new Account();
+$account->setLogin('francois');
+$account->setPassword('S€cr3t');
+
+// Fill the profile via delegation
+$account->setEmail('francois@example.com');
+$account->setTelephone('202-555-9355');
+// same as
+$profile = new Profile();
+$profile->setEmail('francois@example.com');
+$profile->setTelephone('202-555-9355');
+$account->setProfile($profile);
+
+// save the account and its profile
+$account->save();
+
+// retrieve delegated data directly from the main object
+echo $account->getEmail(); // francois@example.com
+{% endhighlight %}
+
+Getter and setter methods for delegate columns don't exist on the main object ; the delegation is handled by the magical `__call()` method. Therefore, the delegation also works for custom methods in the delegate table.
+
+{% highlight php %}
+<?php
+class Profile extends BaseProfile
+{
+ public function setFakeEmail()
+ {
+ $n = rand(10e16, 10e20);
+ $fakeEmail = base_convert($n, 10, 36) . '@example.com';
+ $this->setEmail($fakeEmail);
+ }
+}
+
+$account = new Account();
+$account->setFakeEmail(); // delegates to Profile::setFakeEmail()
+{% endhighlight %}
+
+## Delegating Using a Many-To-One Relationship ##
+
+Instead of adding a one-to-one relationship, the `delegate` behavior can take advantage of an existing many-to-one relationship. For instance:
+
+{% highlight xml %}
+<table name="player">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="first_name" type="VARCHAR" />
+ <column name="last_name" type="VARCHAR" />
+</table>
+<table name="basketballer">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="points" type="INTEGER" />
+ <column name="field_goals" type="INTEGER" />
+ <column name="three_points_field_goals" type="INTEGER" />
+ <column name="player_id" type="INTEGER" />
+ <foreign-key foreignTable="player">
+ <reference local="player_id" foreign="id" />
+ </foreign-key>
+ <behavior name="delegate">
+ <parameter name="to" value="player" />
+ </behavior>
+</table>
+
+{% endhighlight %}
+
+In that case, the behavior doesn't modify the foreign keys, it just proxies method called on `Basketballer` to the related `Player`, or creates one if it doesn't exist:
+
+{% highlight php %}
+<?php
+$basketballer = new Basketballer();
+$basketballer->setPoints(101);
+$basketballer->setFieldGoals(47);
+$basketballer->setThreePointsFieldGoals(7);
+// set player identity via delegation
+$basketballer->setFirstName('Michael');
+$basketballer->setLastName('Giordano');
+// same as
+$player = new Player();
+$player->setFirstName('Michael');
+$player->setLastName('Giordano');
+$basketballer->setPlayer($player);
+
+// save basketballer and player
+$basketballer->save();
+
+// retrieve delegated data directly from the main object
+echo $basketballer->getFirstName(); // Michael
+{% endhighlight %}
+
+And since several models can delegate to the same player object, that means that a single player can have both basketball and soccer stats!
+
+>**Tip**<br />In this example, table delegation is used to implement [Class Table Inheritance](http://martinfowler.com/eaaCatalog/classTableInheritance.html). See how Propel implements this inheritance type, and others, in the [inheritance chapter](../documentation/09-inheritance.html).
+
+## Delegating To Several Tables ##
+
+Delegation allows to delegate to several tables. Just separate the name of the delegate tables by commas in the `to` parameter of the `delegate` behavior tag in your schema to delegate to several tables:
+
+{% highlight xml %}
+<table name="account">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="login" type="VARCHAR" required="true" />
+ <column name="password" type="VARCHAR" required="true" />
+ <behavior name="delegate">
+ <parameter name="to" value="profile, preference" />
+ </behavior>
+</table>
+<table name="profile">
+ <column name="email" type="VARCHAR" />
+ <column name="telephone" type="VARCHAR" />
+</table>
+<table name="preference">
+ <column name="preferred_color" type="VARCHAR" />
+ <column name="max_size" type="INTEGER" />
+</table>
+{% endhighlight %}
+
+Now the `Account` class has two delegates, that can be addressed seamlessly:
+
+{% highlight php %}
+<?php
+$account = new Account();
+$account->setLogin('francois');
+$account->setPassword('S€cr3t');
+
+// Fill the profile via delegation
+$account->setEmail('francois@example.com');
+$account->setTelephone('202-555-9355');
+// Fill the preference via delegation
+$account->setPreferredColor('orange');
+$account->setMaxSize('200');
+
+// save the account and its profile and its preference
+$account->save();
+{% endhighlight %}
+
+On the other hand, it is not possible to cascade delegation to yet another model. So even if the `profile` table delegates to another `detail` table, the methods of the `Detail` model won't be accessibe to the `Profile` objects.
+
+## Parameters ##
+
+The `delegate` behavior takes only one parameter, the list of delegate tables:
+
+{% highlight xml %}
+<table name="account">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="login" type="VARCHAR" required="true" />
+ <column name="password" type="VARCHAR" required="true" />
+ <behavior name="delegate">
+ <parameter name="to" value="profile, preference" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+Note that the delegate tables must exist, but they don't need to share a relationship with the main table (in which case the behavior creates a one-to-one relationship).
View
221 documentation/behaviors/i18n.markdown
@@ -0,0 +1,221 @@
+---
+layout: documentation
+title: I18n Behavior
+---
+
+# I18n Behavior #
+
+The `i18n` behavior provides internationalization to any !ActiveRecord object. Using this behavior, you can separate text data from the other data, and keep several translations of the text data for a single object. Applications supporting several languages always use the `i18n` behavior.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `i18n` behavior to a table. In the `<parameters>` tag, list the columns that need internationalization as the `i18n_columns` parameter:
+
+{% highlight xml %}
+<table name="item">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="name" type="VARCHAR" required="true" />
+ <column name="description" type="LONGVARCHAR" />
+ <column name="price" type="FLOAT" />
+ <column name="is_in_store" type="BOOLEAN" />
+ <behavior name="i18n">
+ <parameter name="i18n_columns" value="name, description" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The internationalized columns have now been moved to a new translation table called `item_i18n`; this new table contains a `locale` column, and shares a many-to-one relationship with the `item` table. In fact, everything happens as if you had defined the following schema:
+
+{% highlight xml %}
+<table name="item">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="price" type="FLOAT" />
+ <column name="is_in_store" type="BOOLEAN" />
+</table>
+<table name="item_i18n">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" />
+ <column name="locale" type="VARCHAR" size="5" required="true" primaryKey="true" />
+ <column name="name" type="VARCHAR" required="true" />
+ <column name="description" type="LONGVARCHAR" />
+ <foreign-key foreignTable="item" onDelete="setnull" onUpdate="cascade">
+ <reference local="id" foreign="id" />
+ </foreign-key>
+</table>
+{% endhighlight %}
+
+In addition, the ActiveRecord Item class now provides integrated translation capabilities.
+
+{% highlight php %}
+<?php
+$item = new Item();
+$item->setPrice('12.99');
+
+// add an English translation
+$item->setLocale('en_EN');
+$item->setName('Microwave oven');
+// same as
+$itemI18n = new ItemI18n();
+$itemI18n->setLocale('en_EN');
+$itemI18n->setName('Microwave oven');
+$item->addItemI18n($itemI18n);
+
+// add a French translation
+$item->setLocale('fr_FR');
+$item->setName('Four micro-ondes');
+
+// save the item and its translations
+$item->save();
+
+// retrieve text and non-text translations directly from the main object
+echo $item->getPrice(); // 12.99
+$item->setLocale('en_EN');
+echo $item->getName(); // Microwave oven
+$item->setLocale('fr_FR');
+echo $item->getName(); // Four micro-ondes
+{% endhighlight %}
+
+Getter an setter methods for internationalized columns still exist on the main object ; they are just proxy methods to the current translation object, using the same signature and phpDoc for better IDE integration.
+
+>**Tip**<br />Propel uses the [locale](http://en.wikipedia.org/wiki/Locale) concept to identify translations. A locale is a string composed of a language code and a territory code (such as 'en_EN' and 'fr_FR'). This allows different translations for two countries using the same language (such as 'fr_FR' and 'fr_CA');
+
+## Dealing With Locale And Translations ##
+
+If you prefer to deal with real translation objects, the behavior generates a `getTranslation()` method on the !ActiveRecord class, which returns a translation object with the required locale.
+
+{% highlight php %}
+<?php
+$item = new Item();
+$item->setPrice('12.99');
+
+// get the English translation
+$t1 = $item->getTranslation('en_EN');
+// same as
+$t1 = new ItemI18n();
+$t1->setLocale('en_EN');
+$item->addItemI18n($t1);
+
+$t1->setName('Microwave oven');
+
+// get the French translation
+$t2 = $item->getTranslation('fr_FR');
+
+$t2->setName('Four micro-ondes');
+
+// these translation objects are already related to the main item
+// and therefore get saved together with it
+$item->save(); // already saves the two translations
+{% endhighlight %}
+
+>**Tip**<br />Or course, if a translation already exists for a given locale, `getTranslation()` returns the existing translation and not a new one.
+
+You can remove a translation using `removeTranslation()` and a locale:
+
+{% highlight php %}
+<?php
+$item = ItemQuery::create()->findPk(1);
+// remove the French translation
+$item->removeTranslation('fr_FR');
+{% endhighlight %}
+
+## Querying For Objects With Translations ##
+
+If you need to display a list, the following code will issue n+1 SQL queries, n being the number of items:
+
+{% highlight php %}
+<?php
+$items = ItemQuery::create()->find(); // one query to retrieve all items
+$locale = 'en_EN';
+foreach ($items as $item) {
+ echo $item->getPrice();
+ $item->setLocale($locale);
+ echo $item->getName(); // one query to retrieve the English translation
+}
+{% endhighlight %}
+
+Fortunately, the behavior adds methods to the Query class, allowing you to hydrate both the `Item` objects and the related `ItemI18n` objects for the given locale:
+
+{% highlight php %}
+<?php
+$items = ItemQuery::create()
+ ->joinWithI18n('en_EN')
+ ->find(); // one query to retrieve both all items and their translations
+foreach ($items as $item) {
+ echo $item->getPrice();
+ echo $item->getName(); // no additional query
+}
+{% endhighlight %}
+
+In addition to hydrating translations, `joinWithI18n()` sets the correct locale on results, so you don't need to call `setLocale()` for each result.
+
+>**Tip**<br />`joinWithI18n()` adds a left join with two conditions. That means that the query returns all items, including those with no translation. If you need to return only objects having translations, add `Criteria::INNER_JOIN` as second parameter to `joinWithI18n()`.
+
+If you need to search items using a condition on a translation, use the generated `useI18nQuery()` as you would with any `useXXXQuery()` method:
+
+{% highlight php %}
+<?php
+$items = ItemQuery::create()
+ ->useI18nQuery('en_EN') // tests the condition on the English translation
+ ->filterByName('Microwave oven')
+ ->endUse()
+ ->find();
+{% endhighlight %}
+
+## Symfony Compatibility ##
+
+This behavior is entirely compatible with the i18n behavior for symfony. That means that it can generate `setCulture()` and `getCulture()` methods as aliases to `setLocale()` and `getLocale()`, provided that you add a `locale_alias` parameter. That also means that if you add the behavior to a table without translated columns, and that the translation table is present in the schema, the behavior recognizes them.
+
+So the following schema is exactly equivalent to the first one in this tutorial:
+
+{% highlight xml %}
+<table name="item">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="price" type="FLOAT" />
+ <column name="is_in_store" type="BOOLEAN" />
+ <behavior name="i18n">
+ <parameter name="locale_alias" value="culture" />
+ </behavior>
+</table>
+<table name="item_i18n">
+ <column name="id" type="INTEGER" required="true" primaryKey="true" />
+ <column name="name" type="VARCHAR" required="true" />
+ <column name="description" type="LONGVARCHAR" />
+</table>
+{% endhighlight %}
+
+Such a schema is almost similar to a schema built for symfony; that means that the Propel i18n behavior is a drop-in replacement for symfony's i18n behavior, keeping BC but improving performance and usability.
+
+## Parameters ##
+
+If you don't specify a locale when dealing with a translatable object, Propel uses the default English locale 'en_EN'. This default can be overridden in the schema using the `default_locale` parameter:
+
+{% highlight xml %}
+<table name="item">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="name" type="VARCHAR" required="true" />
+ <column name="description" type="LONGVARCHAR" />
+ <column name="price" type="FLOAT" />
+ <column name="is_in_store" type="BOOLEAN" />
+ <behavior name="i18n">
+ <parameter name="i18n_columns" value="name, description" />
+ <parameter name="default_locale" value="fr_FR" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+You can change the name of the locale column added by the behavior by setting the `locale_column` parameter. Also, you can change the table name and the phpName of the i18n table by setting the `i18n_table` and `i18n_phpname` parameters:
+
+{% highlight xml %}
+<table name="item">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="name" type="VARCHAR" required="true" />
+ <column name="description" type="LONGVARCHAR" />
+ <column name="price" type="FLOAT" />
+ <column name="is_in_store" type="BOOLEAN" />
+ <behavior name="i18n">
+ <parameter name="i18n_columns" value="name, description" />
+ <parameter name="locale_column" value="language" />
+ <parameter name="i18n_table" value="item_translation" />
+ <parameter name="i18n_phpname" value="ItemTranslation" />
+ </behavior>
+</table>
+{% endhighlight %}
View
358 documentation/behaviors/nested-set.markdown
@@ -0,0 +1,358 @@
+---
+layout: documentation
+title: NestedSet Behavior
+---
+
+# NestedSet Behavior #
+
+The `nested_set` behavior allows a model to become a tree structure, and provides numerous methods to traverse the tree in an efficient way.
+
+Many applications need to store hierarchical data in the model. For instance, a forum stores a tree of messages for each discussion. A CMS sees sections and subsections as a navigation tree. In a business organization chart, each person is a leaf of the organization tree. [Nested sets](http://en.wikipedia.org/wiki/Nested_set_model) are the best way to store such hierachical data in a relational database and manipulate it. The name "nested sets" describes the algorithm used to store the position of a model in the tree ; it is also known as "modified preorder tree traversal".
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `nested_set` behavior to a table:
+{% highlight xml %}
+<table name="section">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="nested_set" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has the ability to be inserted into a tree structure, as follows:
+
+{% highlight php %}
+<?php
+$s1 = new Section();
+$s1->setTitle('Home');
+$s1->makeRoot(); // make this node the root of the tree
+$s1->save();
+$s2 = new Section();
+$s2->setTitle('World');
+$s2->insertAsFirstChildOf($s1); // insert the node in the tree
+$s2->save();
+$s3 = new Section();
+$s3->setTitle('Europe');
+$s3->insertAsFirstChildOf($s2); // insert the node in the tree
+$s3->save();
+$s4 = new Section();
+$s4->setTitle('Business');
+$s4->insertAsNextSiblingOf($s2); // insert the node in the tree
+$s4->save();
+/* The sections are now stored in the database as a tree:
+ $s1:Home
+ | \
+$s2:World $s4:Business
+ |
+$s3:Europe
+*/
+{% endhighlight %}
+
+You can continue to insert new nodes as children or siblings of existing nodes, using any of the `insertAsFirstChildOf()`, `insertAsLastChildOf()`, `insertAsPrevSiblingOf()`, and `insertAsNextSiblingOf()` methods.
+
+Once you have built a tree, you can traverse it using any of the numerous methods the `nested_set` behavior adds to the query and model objects. For instance:
+
+{% highlight php %}
+<?php
+$rootNode = SectionQuery::create()->findRoot(); // $s1
+$worldNode = $rootNode->getFirstChild(); // $s2
+$businessNode = $worldNode->getNextSibling(); // $s4
+$firstLevelSections = $rootNode->getChildren(); // array($s2, $s4)
+$allSections = $rootNode->getDescendants(); // array($s2, $s3, $s4)
+// you can also chain the methods
+$europeNode = $rootNode->getLastChild()->getPrevSibling()->getFirstChild(); // $s3
+$path = $europeNode->getAncestors(); // array($s1, $s2)
+{% endhighlight %}
+
+The nodes returned by these methods are regular Propel model objects, with access to the properties and related models. The `nested_set` behavior also adds inspection methods to nodes:
+
+{% highlight php %}
+<?php
+echo $s2->isRoot(); // false
+echo $s2->isLeaf(); // false
+echo $s2->getLevel(); // 1
+echo $s2->hasChildren(); // true
+echo $s2->countChildren(); // 1
+echo $s2->hasSiblings(); // true
+{% endhighlight %}
+
+Each of the traversal and inspection methods result in a single database query, whatever the position of the node in the tree. This is because the information about the node position in the tree is stored in three columns of the model, named `tree_left`, `tree_left`, and `tree_level`. The value given to these columns is determined by the nested set algorithm, and it makes read queries much more effective than trees using a simple `parent_id` foreign key.
+
+## Manipulating Nodes ##
+
+You can move a node - and its subtree - across the tree using any of the `moveToFirstChildOf()`, `moveToLastChildOf()`, `moveToPrevSiblingOf()`, and `moveToLastSiblingOf()` methods. These operations are immediate and don't require that you save the model afterwards:
+
+{% highlight php %}
+<?php
+// move the entire "World" section under "Business"
+$s2->moveToFirstChildOf($s4);
+/* The tree is modified as follows:
+$s1:Home
+ |
+$s4:Business
+ |
+$s2:World
+ |
+$s3:Europe
+*/
+// now move the "Europe" section directly under root, after "Business"
+$s2->moveToFirstChildOf($s4);
+/* The tree is modified as follows:
+ $s1:Home
+ | \
+$s4:Business $s3:Europe
+ |
+$s2:World
+*/
+{% endhighlight %}
+
+You can delete the descendants of a node using `deleteDescendants()`:
+
+{% highlight php %}
+<?php
+// move the entire "World" section under "Business"
+$s4->deleteDescendants($s4);
+/* The tree is modified as follows:
+ $s1:Home
+ | \
+$s4:Business $s3:Europe
+*/
+{% endhighlight %}
+
+If you `delete()` a node, all its descendants are deleted in cascade. To avoid accidental deletion of an entire tree, calling `delete()` on a root node throws an exception. Use the `delete()` Query method instead to delete an entire tree.
+
+## Filtering Results ##
+
+The `nested_set` behavior adds numerous methods to the generated Query object. You can use these methods to build more complex queries. For instance, to get all the children of the root node ordered by title, build a Query as follows:
+
+{% highlight php %}
+<?php
+$children = SectionQuery::create()
+ ->childrenOf($rootNode)
+ ->orderByTitle()
+ ->find();
+{% endhighlight %}
+
+Alternatively, if you already have an existing query method, you can pass it to the model object's methods to filter the results:
+
+{% highlight php %}
+<?php
+$orderQuery = SectionQuery::create()->orderByTitle();
+$children = $rootNode->getChildren($orderQuery);
+{% endhighlight %}
+
+## Multiple Trees ##
+
+When you need to store several trees for a single model - for instance, several threads of posts in a forum - use a _scope_ for each tree. This requires that you enable scope tree support in the behavior definition by setting the `use_scope` parameter to `true`:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="body" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="nested_set">
+ <parameter name="use_scope" value="true" />
+ <parameter name="scope_column" value="thread_id" />
+ </behavior>
+ <foreign-key foreignTable="thread" onDelete="cascade">
+ <reference local="thread_id" foreign="id" />
+ </foreign-key>
+</table>
+{% endhighlight %}
+
+Now, after rebuilding your model, you can have as many trees as required:
+
+{% highlight php %}
+<?php
+$thread = ThreadQuery::create()->findPk(123);
+$firstPost = PostQuery::create()->findRoot($thread->getId()); // first message of the discussion
+$discussion = PostQuery::create()->findTree(thread->getId()); // all messages of the discussion
+PostQuery::create()->inTree($thread->getId())->delete(); // delete an entire discussion
+$firstPostOfEveryDiscussion = PostQuery::create()->findRoots();
+{% endhighlight %}
+
+## Using a RecursiveIterator ##
+
+An alternative way to browse a tree structure extensively is to use a [RecursiveIterator](http://php.net/RecursiveIterator). The `nested_set` behavior provides an easy way to retrieve such an iterator from a node, and to parse the entire branch in a single iteration.
+
+For instance, to display an entire tree structure, you can use the following code:
+
+{% highlight php %}
+<?php
+$root = SectionQuery::create()->findRoot();
+foreach ($root->getIterator() as $node) {
+ echo str_repeat(' ', $node->getLevel()) . $node->getTitle() . "\n";
+}
+{% endhighlight %}
+
+The iterator parses the tree in a recursive way by retrieving the children of every node. This can be quite effective on very large trees, since the iterator hydrates only a few objects at a time.
+
+Beware, though, that the iterator executes many queries to parse a tree. On smaller trees, prefer the `getBranch()` method to execute only one query, and hydrate all records at once:
+
+{% highlight php %}
+<?php
+$root = SectionQuery::create()->findRoot();
+foreach ($root->getBranch() as $node) {
+ echo str_repeat(' ', $node->getLevel()) . $node->getTitle() . "\n";
+}
+{% endhighlight %}
+
+## Parameters ##
+
+By default, the behavior adds three columns to the model - four if you use the scope feature. You can use custom names for the nested sets columns. The following schema illustrates a complete customization of the behavior:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="lft" type="INTEGER" />
+ <column name="rgt" type="INTEGER" />
+ <column name="lvl" type="INTEGER" />
+ <column name="thread_id" type="INTEGER" />
+ <column name="body" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="nested_set">
+ <parameter name="left_column" value="lft" />
+ <parameter name="right_column" value="rgt" />
+ <parameter name="level_column" value="lvl" />
+ <parameter name="use_scope" value="true" />
+ <parameter name="scope_column" value="thread_id" />
+ </behavior>
+ <foreign-key foreignTable="thread" onDelete="cascade">
+ <reference local="thread_id" foreign="id" />
+ </foreign-key>
+</table>
+{% endhighlight %}
+
+Whatever name you give to your columns, the `nested_sets` behavior always adds the following proxy methods, which are mapped to the correct column:
+
+{% highlight php %}
+<?php
+$post->getLeftValue(); // returns $post->lft
+$post->setLeftValue($left);
+$post->getRightValue(); // returns $post->rgt
+$post->setRightValue($right);
+$post->getLevel(); // returns $post->lvl
+$post->setLevel($level);
+$post->getScopeValue(); // returns $post->thread_id
+$post->setScopeValue($scope);
+{% endhighlight %}
+
+If your application used the old nested sets builder from Propel 1.4, you can enable the `method_proxies` parameter so that the behavior generates method proxies for the methods that used a different name (e.g. `createRoot()` for `makeRoot()`, `retrieveFirstChild()` for `getFirstChild()`, etc.
+
+{% highlight xml %}
+<table name="section">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="nested_set">
+ <parameter name="method_proxies" value="true" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+## Complete API ##
+
+Here is a list of the methods added by the behavior to the model objects:
+
+{% highlight php %}
+<?php
+// storage columns accessors
+int getLeftValue()
+$node setLeftValue(int $left)
+int getRightValue()
+$node setRightValue(int $right)
+int getLevel()
+$node setLevel(int $level)
+// only for behavior with use_scope
+int getScopeValue()
+$node setScopeValue(int $scope)
+
+// root maker (requires calling save() afterwards)
+$node makeRoot()
+
+// inspection methods
+bool isInTree()
+bool isRoot()
+bool isLeaf()
+bool isDescendantOf()
+bool isAncestorOf()
+bool hasParent()
+bool hasPrevSibling()
+bool hasNextSibling()
+bool hasChildren()
+int countChildren()
+int countDescendants()
+
+// tree traversal methods
+$node getParent()
+$node getPrevSibling()
+$node getNextSibling()
+array getChildren()
+$node getFirstChild()
+$node getLastChild()
+array getSiblings($includeCurrent = false, Criteria $c = null)
+array getDescendants(Criteria $c = null)
+array getBranch(Criteria $c = null)
+array getAncestors(Criteria $c = null)
+
+// node insertion methods (require calling save() afterwards)
+$node addChild($node)
+$node insertAsFirstChildOf($node)
+$node insertAsLastChildOf($node)
+$node insertAsPrevSiblingOf($node)
+$node insertAsNextSiblingOf($node)
+
+// node move methods (immediate, no need to save() afterwards)
+$node moveToFirstChildOf($node)
+$node moveToLastChildOf($node)
+$node moveToPrevSiblingOf($node)
+$node moveToNextSiblingOf($node)
+
+// deletion methods
+$node deleteDescendants()
+
+// only for behavior with method_proxies
+$node createRoot()
+$node retrieveParent()
+$node retrievePrevSibling()
+$node retrieveNextSibling()
+$node retrieveFirstChild()
+$node retrieveLastChild()
+array getPath()
+{% endhighlight %}
+
+The behavior also adds some methods to the Query classes:
+
+{% highlight php %}
+<?php
+// tree filter methods
+query descendantsOf($node)
+query branchOf($node)
+query childrenOf($node)
+query siblingsOf($node)
+query ancestorsOf($node)
+query rootsOf($node)
+// only for behavior with use_scope
+query treeRoots()
+query inTree($scope = null)
+coll findRoots()
+// order methods
+query orderByBranch($reverse = false)
+query orderByLevel($reverse = false)
+// termination methods
+$node findRoot($scope = null)
+coll findTree($scope = null)
+{% endhighlight %}
+
+Lastly, the behavior adds a few methods to the Peer classes:
+
+{% highlight php %}
+<?php
+$node retrieveRoot($scope = null)
+array retrieveTree($scope = null)
+int deleteTree($scope = null)
+// only for behavior with use_scope
+array retrieveRoots(Criteria $c = null)
+{% endhighlight %}
+
+## TODO ##
+
+* InsertAsParentOf
View
103 documentation/behaviors/query-cache.markdown
@@ -0,0 +1,103 @@
+---
+layout: documentation
+title: Query Cache Behavior
+---
+
+# Query Cache Behavior #
+
+The `query_cache` behavior gives a speed boost to Propel queries by caching the transformation of a PHP Query object into reusable SQL code.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `query_cache` behavior to a table:
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="query_cache" />
+</table>
+{% endhighlight %}
+
+After you rebuild your model, all the queries on this object can now be cached. To trigger the query cache on a particular query, just give it a query key using the `setQueryKey()` method. The key is a unique identifier that you can choose, later used for cache lookups:
+
+{% highlight php %}
+<?php
+$title = 'War And Peace';
+$books = BookQuery::create()
+ ->setQueryKey('search book by title')
+ ->filterByTitle($title)
+ ->findOne();
+{% endhighlight %}
+
+The first time Propel executes the termination method, it computes the SQL translation of the Query object and stores it into a cache backend (APC by default). Next time you run the same query, it executes faster, even with different parameters:
+
+{% highlight php %}
+<?php
+$title = 'Anna Karenina';
+$books = BookQuery::create()
+ ->setQueryKey('search book by title')
+ ->filterByTitle($title)
+ ->findOne();
+{% endhighlight %}
+
+>**Tip**<br />The more complex the query, the greater the boost you get from the query cache behavior.
+
+## Parameters ##
+
+You can change the cache backend and the cache lifetime (in seconds) by setting the `backend` and `lifetime` parameters:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="query_cache">
+ <parameter name="backend" value="custom" />
+ <parameter name="lifetime" value="600" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+To implement a custom cache backend, just override the generated `cacheContains()`, `cacheFetch()` and `cacheStore()` methods in the Query object. For instance, to implement query cache using Zend_Cache and memcached, try the following:
+
+{% highlight php %}
+<?php
+class BookQuery extends BaseBookQuery
+{
+ public function cacheContains($key)
+ {
+ return $this->getCacheBackend()->test($key);
+ }
+
+ public function cacheFetch($key)
+ {
+ return $this->getCacheBackend()->load($key);
+ }
+
+ public function cacheStore($key, $value)
+ {
+ return $this->getCacheBackend()->save($key, $value);
+ }
+
+ protected function getCacheBackend()
+ {
+ if (self::$cacheBackend ### null) {
+ $frontendOptions = array(
+ 'lifetime' => 7200,
+ 'automatic_serialization' => true
+ );
+ $backendOptions = array(
+ 'servers' => array(
+ array(
+ 'host' => 'localhost',
+ 'port' => 11211,
+ 'persistent' => true
+ )
+ )
+ );
+ self::$cacheBackend = Zend_Cache::factory('Core', 'Memcached', $frontendOptions, $backendOptions);
+ }
+
+ return self::$cacheBackend;
+ }
+}
+{% endhighlight %}
View
132 documentation/behaviors/sluggable.markdown
@@ -0,0 +1,132 @@
+---
+layout: documentation
+title: Sluggable Behavior
+---
+
+# Sluggable Behavior #
+
+The `sluggable` behavior allows a model to offer a human readable identifier that can be used for search engine friendly URLs.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `sluggable` behavior to a table:
+{% highlight xml %}
+<table name="post">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="sluggable" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has an additional getter for its slug, which is automatically set before the object is saved:
+
+{% highlight php %}
+<?php
+$p1 = new Post();
+$p1->setTitle('Hello, World!');
+$p1->save();
+echo $p1->getSlug(); // 'hello-world'
+{% endhighlight %}
+
+By default, the behavior uses the string representation of the object to build the slug. In the example above, the `title` column is defined as `primaryString`, so the slug uses this column as a base string. The string is then cleaned up in order to allow it to appear in a URL. In the process, blanks and special characters are replaced by a dash, and the string is lowercased.
+
+>**Tip**<br />The slug is unique by design. That means that if you create a new object and that the behavior calculates a slug that already exists, the string is modified to be unique:
+
+{% highlight php %}
+<?php
+$p2 = new Post();
+$p2->setTitle('Hello, World!');
+$p2->save();
+echo $p2->getSlug(); // 'hello-world-1'
+{% endhighlight %}
+
+The generated model query offers a `findOneBySlug()` method to easily retrieve a model object based on its slug:
+
+{% highlight php %}
+<?php
+$p = PostQuery::create()->findOneBySlug('hello-world');
+{% endhighlight %}
+
+## Parameters ##
+
+By default, the behavior adds one columns to the model. If this column is already described in the schema, the behavior detects it and doesn't add it a second time. The behavior parameters allow you to use custom patterns for the slug composition. The following schema illustrates a complete customization of the behavior:
+
+{% highlight xml %}
+<table name="post">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="url" type="VARCHAR" size="100" />
+ <behavior name="sluggable">
+ <parameter name="slug_column" value="url" />
+ <parameter name="slug_pattern" value="/posts/{Title}" />
+ <parameter name="replace_pattern" value="/[^\w\/]+/u" />
+ <parameter name="replacement" value="-" />
+ <parameter name="separator" value="/" />
+ <parameter name="permanent" value="true" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+Whatever `slug_column` name you choose, the `sluggable` behavior always adds the following proxy methods, which are mapped to the correct column:
+
+{% highlight php %}
+<?php
+$post->getSlug(); // returns $post->url
+$post->setSlug($slug); // $post->url = $slug
+{% endhighlight %}
+
+The `slug_pattern` parameter is the rule used to build the raw slug based on the object properties. Any substring enclosed between brackets '{}' is turned into a getter, so the `Post` class generates slugs as follows:
+
+{% highlight php %}
+<?php
+protected function createRawSlug()
+{
+ return '/posts/' . $this->getTitle();
+}
+{% endhighlight %}
+
+Incidentally, that means that you can use names that don't match a real column phpName, as long as your model provides a getter for it.
+
+The `replace_pattern` parameter is a regular expression that shows all the characters that will end up replaced by the `replacement` parameter. In the above example, special characters like '!' or ':' are replaced by '-', but not letters, digits, nor '/'.
+
+The `separator` parameter is the character that separates the slug from the incremental index added in case of non-unicity. Set as '/', it makes `Post` objects sharing the same title have the following slugs:
+
+{% highlight text %}
+'posts/hello-world'
+'posts/hello-world/1'
+'posts/hello-world/2'
+...
+{% endhighlight %}
+
+A `permanent` slug is not automatically updated when the fields that constitute it change. This is useful when the slug serves as a permalink, that should work even when the model object properties change. Note that you can still manually change the slug in a model using the `permanent` setting by calling `setSlug()`;
+
+## Further Customization ##
+
+The slug is generated by the object when it is saved, via the `createSlug()` method. This method does several operations on a simple string:
+
+{% highlight php %}
+<?php
+protected function createSlug()
+{
+ // create the slug based on the `slug_pattern` and the object properties
+ $slug = $this->createRawSlug();
+ // truncate the slug to accomodate the size of the slug column
+ $slug = $this->limitSlugSize($slug);
+ // add an incremental index to make sure the slug is unique
+ $slug = $this->makeSlugUnique($slug);
+
+ return $slug;
+}
+
+protected function createRawSlug()
+{
+ // here comes the string composition code, generated according to `slug_pattern`
+ $slug = 'posts/' . $this->cleanupSlugPart($this->getTitle());
+ // cleanupSlugPart() cleans up the slug part
+ // based on the `replace_pattern` and `replacement` parameters
+
+ return $slug;
+}
+{% endhighlight %}
+
+You can override any of these methods in your model class, in order to implement a custom slug logic.
View
128 documentation/behaviors/soft-delete.markdown
@@ -0,0 +1,128 @@
+---
+layout: documentation
+title: SoftDelete Behavior
+---
+
+# SoftDelete Behavior #
+
+The `soft_delete` behavior overrides the deletion methods of a model object to make them 'hide' the deleted rows but keep them in the database. Deleted objects still don't show up on select queries, but they can be retrieved or undeleted when necessary.
+
+**Warning**: This behavior is deprecated due to limitations that can't be fixed. Use the [`archivable`](archivable.html) behavior instead.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `soft_delete` behavior to a table:
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="soft_delete" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has one new column, `deleted_at`, that stores the deletion date. Select queries don't return the deleted objects:
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->save();
+$b->delete();
+echo $b->isDeleted(); // false
+echo $b->getDeletedAt(); // 2009-10-02 18:14:23
+$books = BookQuery::create()->find(); // empty collection
+{% endhighlight %}
+
+Behind the curtain, the behavior adds a condition to every SELECT query to return only records where the `deleted_at` column is null. That's why the deleted objects don't appear anymore upon selection.
+
+_Warning_gg Deleted results may show up in related results (i.e. when you use `joinWith()` on a query and point to a `soft_delete` model). This is something that can't be fixed, and a good reason to use the `archivable` behavior instead.
+
+You can include deleted results in a query by calling the `includeDeleted()` filter:
+
+{% highlight php %}
+<?php
+$book = BookQuery::create()
+ ->includeDeleted()
+ ->findOne();
+echo $book->getTitle(); // 'War And Peace'
+{% endhighlight %}
+
+You can also turn off the query alteration for the next query by calling the static method `disableSoftDelete()` on the related Query object:
+
+{% highlight php %}
+<?php
+BookQuery::disableSoftDelete();
+$book = BookQuery::create()->findOne();
+echo $book->getTitle(); // 'War And Peace'
+{% endhighlight %}
+
+Note that `find()` and other selection methods automatically re-enable the `soft_delete` filter, so `disableSoftDelete()` is really a single shot method. You can also enable the query alteration manually by calling the `enableSoftDelete()` method on Query objects.
+
+>**Tip**<br />`ModelCriteria::paginate()` executes two queries, so `disableSoftDelete()` doesn't work in this case. Prefer `includeDeleted()` in queries using `paginate()`.
+
+If you want to recover a deleted object, use the `unDelete()` method:
+
+{% highlight php %}
+<?php
+$book->unDelete();
+$books = BookQuery::create()->find();
+$book = $books[0];
+echo $book->getTitle(); // 'War And Peace'
+{% endhighlight %}
+
+If you want to force the real deletion of an object, call the `forceDelete()` method:
+
+{% highlight php %}
+<?php
+$book->forceDelete();
+echo $book->isDeleted(); // true
+$books = BookQuery::create()->find(); // empty collection
+{% endhighlight %}
+
+The query methods `delete()` and `deleteAll()` also perform a soft deletion, unless you disable the behavior on the peer class:
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->save();
+
+BookQuery::create()->delete($b);
+$books = BookQuery::create()->find(); // empty collection
+// the rows look deleted, but they are still there
+BookQuery::disableSoftDelete();
+$books = BookQuery::create()->find();
+$book = $books[0];
+echo $book->getTitle(); // 'War And Peace'
+
+// To perform a true deletion, disable the softDelete feature
+BookQuery::disableSoftDelete();
+BookQuery::create()->delete();
+// Alternatively, use forceDelete()
+BookQuery::create()->forceDelete();
+{% endhighlight %}
+
+## Parameters ##
+
+You can change the name of the column added by the behavior by setting the `deleted_column` parameter:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="my_deletion_date" type="TIMESTAMP" />
+ <behavior name="soft_delete">
+ <parameter name="deleted_column" value="my_deletion_date" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->save();
+$b->delete();
+echo $b->getMyDeletionDate(); // 2009-10-02 18:14:23
+$books = BookQuery::create()->find(); // empty collection
+{% endhighlight %}
View
273 documentation/behaviors/sortable.markdown
@@ -0,0 +1,273 @@
+---
+layout: documentation
+title: Sortable Behavior
+---
+
+# Sortable Behavior #
+
+The `sortable` behavior allows a model to become an ordered list, and provides numerous methods to traverse this list in an efficient way.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `sortable` behavior to a table:
+{% highlight xml %}
+<table name="task">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="sortable" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has the ability to be inserted into an ordered list, as follows:
+
+{% highlight php %}
+<?php
+$t1 = new Task();
+$t1->setTitle('Wash the dishes');
+$t1->save();
+echo $t1->getRank(); // 1, the first rank to be given (not 0)
+$t2 = new Task();
+$t2->setTitle('Do the laundry');
+$t2->save();
+echo $t2->getRank(); // 2
+$t3 = new Task();
+$t3->setTitle('Rest a little');
+$t3->save()
+echo $t3->getRank(); // 3
+{% endhighlight %}
+
+As long as you save new objects, Propel gives them the first available rank in the list.
+
+Once you have built an ordered list, you can traverse it using any of the methods added by the `sortable` behavior. For instance:
+
+{% highlight php %}
+<?php
+$firstTask = TaskQuery::create()->findOneByRank(1); // $t1
+$secondTask = $firstTask->getNext(); // $t2
+$lastTask = $secondTask->getNext(); // $t3
+$secondTask = $lastTask->getPrevious(); // $t2
+
+$allTasks = TaskQuery::create()->findList();
+// => collection($t1, $t2, $t3)
+$allTasksInReverseOrder = TaskQuery::create()->orderByRank('desc')->find();
+// => collection($t3, $t2, $t2)
+{% endhighlight %}
+
+The results returned by these methods are regular Propel model objects, with access to the properties and related models. The `sortable` behavior also adds inspection methods to objects:
+
+{% highlight php %}
+<?php
+echo $t2->isFirst(); // false
+echo $t2->isLast(); // false
+echo $t2->getRank(); // 2
+{% endhighlight %}
+
+## Manipulating Objects In A List ##
+
+You can move an object in the list using any of the `moveUp()`, `moveDown()`, `moveToTop()`, `moveToBottom()`, `moveToRank()`, and `swapWith()` methods. These operations are immediate and don't require that you save the model afterwards:
+
+{% highlight php %}
+<?php
+// The list is 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little
+$t2->moveToTop();
+// The list is now 1 - Do the laundry, 2 - Wash the dishes, 3 - Rest a little
+$t2->moveToBottom();
+// The list is now 1 - Wash the dishes, 2 - Rest a little, 3 - Do the laundry
+$t2->moveUp();
+// The list is 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little
+$t2->swapWith($t1);
+// The list is now 1 - Do the laundry, 2 - Wash the dishes, 3 - Rest a little
+$t2->moveToRank(3);
+// The list is now 1 - Wash the dishes, 2 - Rest a little, 3 - Do the laundry
+$t2->moveToRank(2);
+{% endhighlight %}
+
+By default, new objects are added at the bottom of the list. But you can also insert them at a specific position, using any of the `insertAtTop()`, `insertAtBottom()`, and `insertAtRank()` methods. Note that the `insertAtXXX` methods don't save the object:
+
+{% highlight php %}
+<?php
+// The list is 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little
+$t4 = new Task();
+$t4->setTitle('Clean windows');
+$t4->insertAtRank(2);
+$t4->save();
+// The list is now 1 - Wash the dishes, 2 - Clean Windows, 3 - Do the laundry, 4 - Rest a little
+{% endhighlight %}
+
+Whenever you `delete()` an object, the ranks are rearranged to fill the gap:
+
+{% highlight php %}
+<?php
+$t4->delete();
+// The list is now 1 - Wash the dishes, 2 - Do the laundry, 3 - Rest a little
+{% endhighlight %}
+
+>**Tip**<br />You can remove an object from the list without necessarily deleting it by calling `removeFromList()`. Don't forget to `save()` it afterwards so that the other objects in the lists are rearranged to fill the gap.
+
+## Multiple Lists ##
+
+When you need to store several lists for a single model - for instance, one task list for each user - use a _scope_ for each list. This requires that you enable scope support in the behavior definition by setting the `use_scope` parameter to `true`:
+
+{% highlight xml %}
+<table name="task">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="user_id" required="true" type="INTEGER" />
+ <foreign-key foreignTable="user" onDelete="cascade">
+ <reference local="user_id" foreign="id" />
+ </foreign-key>
+ <behavior name="sortable">
+ <parameter name="use_scope" value="true" />
+ <parameter name="scope_column" value="user_id" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+Now, after rebuilding your model, you can have as many lists as required:
+
+{% highlight php %}
+<?php
+// test users
+$paul = new User();
+$john = new User();
+// now onto the tasks
+$t1 = new Task();
+$t1->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
+{% endhighlight %}
+
+The generated methods now accept a `$scope` parameter to restrict the query to a given scope:
+
+{% highlight php %}
+<?php
+$firstPaulTask = TaskQuery::create()->findOneByRank($rank = 1, $scope = $paul->getId()); // $t1
+$lastPaulTask = $firstTask->getNext(); // $t2
+$firstJohnTask = TaskPeer::create()->findOneByRank($rank = 1, $scope = $john->getId()); // $t1
+{% endhighlight %}
+
+Models using the sortable behavior with scope benefit from one additional Query methods named `inList()`:
+
+{% highlight php %}
+<?php
+$allPaulsTasks = TaskPeer::create()->inList($scope = $paul->getId())->find();
+{% endhighlight %}
+
+## Parameters ##
+
+By default, the behavior adds one columns to the model - two if you use the scope feature. If these columns are already described in the schema, the behavior detects it and doesn't add them a second time. The behavior parameters allow you to use custom names for the sortable columns. The following schema illustrates a complete customization of the behavior:
+
+{% highlight xml %}
+<table name="task">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="my_rank_column" required="true" type="INTEGER" />
+ <column name="user_id" required="true" type="INTEGER" />
+ <foreign-key foreignTable="user" onDelete="cascade">
+ <reference local="user_id" foreign="id" />
+ </foreign-key>
+ <behavior name="sortable">
+ <parameter name="rank_column" value="my_rank_column" />
+ <parameter name="use_scope" value="true" />
+ <parameter name="scope_column" value="user_id" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+Whatever name you give to your columns, the `sortable` behavior always adds the following proxy methods, which are mapped to the correct column:
+
+{% highlight php %}
+<?php
+$task->getRank(); // returns $task->my_rank_column
+$task->setRank($rank);
+$task->getScopeValue(); // returns $task->user_id
+$task->setScopeValue($scope);
+{% endhighlight %}
+
+The same happens for the generated Query object:
+
+{% highlight php %}
+<?php
+$query = TaskQuery::create()->filterByRank(); // proxies to filterByMyRankColumn()
+$query = TaskQuery::create()->orderByRank(); // proxies to orderByMyRankColumn()
+$tasks = TaskQuery::create()->findOneByRank(); // proxies to findOneByMyRankColumn()
+{% endhighlight %}
+
+>**Tip**<br />The behavior adds columns but no index. Depending on your table structure, you might want to add a column index by hand to speed up queries on sorted lists.
+
+## Complete API ##
+
+Here is a list of the methods added by the behavior to the model objects:
+
+{% highlight php %}
+<?php
+// storage columns accessors
+int getRank()
+$object setRank(int $rank)
+// only for behavior with use_scope
+int getScopeValue()
+$object setScopeValue(int $scope)
+
+// inspection methods
+bool isFirst()
+bool isLast()
+
+// list traversal methods
+$object getNext()
+$object getPrevious()
+
+// methods to insert an object in the list (require calling save() afterwards)
+$object insertAtRank($rank)
+$object insertAtBottom()
+$object insertAtTop()
+
+// methods to move an object in the list (immediate, no need to save() afterwards)
+$object moveToRank($rank)
+$object moveUp()
+$object moveDown()
+$object moveToTop()
+$object moveToBottom()
+$object swapWith($object)
+
+// method to remove an object from the list (requires calling save() afterwards)
+$object removeFromList()
+{% endhighlight %}
+
+Here is a list of the methods added by the behavior to the query objects:
+
+{% highlight php %}
+<?php
+query filterByRank($order, $scope = null)
+query orderByRank($order, $scope = null)
+$object findOneByRank($rank, $scope = null)
+coll findList($scope = null)
+int getMaxRank($scope = null)
+bool reorder($newOrder) // $newOrder is a $id => $rank associative array
+// only for behavior with use_scope
+array inList($scope)
+{% endhighlight %}
+
+The behavior also adds a few methods to the Peer classes:
+
+{% highlight php %}
+<?php
+int getMaxRank($scope = null)
+$object retrieveByRank($rank, $scope = null)
+array doSelectOrderByRank($order, $scope = null)
+bool reorder($newOrder) // $newOrder is a $id => $rank associative array
+// only for behavior with use_scope
+array retrieveList($scope)
+int countList($scope)
+int deleteList($scope)
+{% endhighlight %}
View
91 documentation/behaviors/timestampable.markdown
@@ -0,0 +1,91 @@
+---
+layout: documentation
+title: Timestampable Behavior
+---
+
+# Timestampable Behavior #
+
+The `timestampable` behavior allows you to keep track of the date of creation and last update of your model objects.
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `timestampable` behavior to a table:
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <behavior name="timestampable" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has two new columns, `created_at` and `updated_at`, that store a timestamp automatically updated on save:
+
+{% highlight php %}
+<?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
+{% endhighlight %}
+
+The object query also has specific methods to retrieve recent objects and order them according to their update date:
+
+{% highlight php %}
+<?php
+$books = BookQuery::create()
+ ->recentlyUpdated() // adds a minimum value for the update date
+ ->lastUpdatedFirst() // orders the results by descending update date
+ ->find();
+{% endhighlight %}
+
+You can use any of the following methods in the object query:
+
+{% highlight php %}
+<?php
+// limits the query to recent objects
+ModelCriteria recentlyCreated($nbDays = 7)
+ModelCriteria recentlyUpdated($nbDays = 7)
+// orders the results
+ModelCriteria lastCreatedFirst() // order by creation date desc
+ModelCriteria firstCreatedFirst() // order by creation date asc
+ModelCriteria lastUpdatedFirst() // order by update date desc
+ModelCriteria firstUpdatedFirst() // order by update date asc
+{% endhighlight %}
+
+>**Tip**<br />You may need to keep the update date unchanged after an update on the object, for instance when you only update a calculated row. In this case, call the `keepUpdateDateUnchanged()` method on the object before saving it.
+
+
+## Parameters ##
+
+You can change the name of the columns added by the behavior by setting the `create_column` and `update_column` parameters:
+
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" primaryString="true" />
+ <column name="my_create_date" type="TIMESTAMP" />
+ <column name="my_update_date" type="TIMESTAMP" />
+ <behavior name="timestampable">
+ <parameter name="create_column" value="my_create_date" />
+ <parameter name="update_column" value="my_update_date" />
+ </behavior>
+</table>
+{% endhighlight %}
+
+{% highlight php %}
+<?php
+$b = new Book();
+$b->setTitle('War And Peace');
+$b->save();
+echo $b->getMyCreateDate(); // 2009-10-02 18:14:23
+echo $b->getMyUpdateDate(); // 2009-10-02 18:14:23
+$b->setTitle('Anna Karenina');
+$b->save();
+echo $b->getMyCreateDate(); // 2009-10-02 18:14:23
+echo $b->getMyUpdateDate(); // 2009-10-02 18:14:25
+{% endhighlight %}
View
263 documentation/behaviors/versionable.markdown
@@ -0,0 +1,263 @@
+---
+layout: documentation
+title: Versionable Behavior
+---
+
+# Versionable Behavior #
+
+The `versionable` behavior provides versioning capabilities to any ActiveRecord object. Using this behavior, you can:
+
+* Revert an object to previous versions easily
+* Track and browse history of the modifications of an object
+* Keep track of the modifications in related objects
+
+## Basic Usage ##
+
+In the `schema.xml`, use the `<behavior>` tag to add the `versionable` behavior to a table:
+{% highlight xml %}
+<table name="book">
+ <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
+ <column name="title" type="VARCHAR" required="true" />
+ <behavior name="versionable" />
+</table>
+{% endhighlight %}
+
+Rebuild your model, insert the table creation sql again, and you're ready to go. The model now has one new column, `version`, which stores the version number. It also has a new table, `book_version`, which stores all the versions of all `Book` objects, past and present. You won't need to interact with this second table, since the behavior offers an easy-to-use API that takes care of all verisoning actions from the main ActiveRecord object.
+
+{% highlight php %}
+<?php
+$book = new Book();
+
+// automatic version increment
+$book->setTitle('War and Peas');
+$book->save();
+echo $book->getVersion(); // 1
+$book->setTitle('War and Peace');
+$book->save();
+echo $book->getVersion(); // 2
+
+// reverting to a previous version
+$book->toVersion(1);
+echo $book->getTitle(); // 'War and Peas'
+// saving a previous version creates a new one
+$book->save();
+echo $book->getVersion(); // 3
+
+// checking differences between versions
+print_r($book->compareVersions(1, 2));
+// array(
+// 'Title' => array(1 => 'War and Peas', 2 => 'War and Pace'),
+// );
+
+// deleting an object also deletes all its versions
+$book->delete();
+{% endhighlight %}
+
+## Adding details about each revision ##
+
+For future reference, you probably need to record who edited an object, as well as when and why. To enable audit log capabilities, add the three following parameters to the `<behavior>` tag:
+
+{% highlight xml %}