Archive for the 'plugins' Category

Propel is not hard anymore

The two most common criticisms I hear about about Propel are the difficulty to learn the Criteria object, and the amount of code required to do a custom Join. That, and the fact that Propel is slow--but Propel 1.3 will just leave every other ORM behind, so speed is not a good reason to ignore Propel, especially now.

The abilities of Propel are just amazing, but the API is far too Java-inspired for our hectic programming speed. Rapid Application Developers need simple APIs that allow them to go fast in the majority of the cases. That's the philosophy of the sfPropelFinderPlugin, that I already introduced in a past post. This symfony plugin (which doesn't depend on symfony - you can use it with Propel alone, provided you load the Propel classes by hand) recently got better.

No more Criterions

sfPropelFinder encapsulates Criteria calls, so you never need to manipulate the complex Propel syntax. Instead, the plugin proposes a syntax reminiscent of SQL to retrieve Propel objects:

$articles = sfPropelFinder::from('Article')->
  where('Title', 'foo')->
  _and('PublishedAt', '<', time())->
  _or('Title', 'like', 'bar%')->
  find();


If you already tried to do AND and OR queries with Criterion objects, you know how the above can be long and complicated to write. Here, the Propel finder does all the dirty job for you. Another relief is the use of the CamelCase version of the properties names, which is consistent with the Propel object getters. You no longer need to type infamous strings looking like ArticlePeer::PUBLISHED_AT; PublishedAt will do the trick.

Easy joins

Another recent improvement of the finder plugin is its ability to deal with related objects in joins and hydrating. The idea is that you define relationships in the schema, with foreign keys and references, but Propel keeps asking you to repeat them each time you want to join two objects in a Criteria addJoin(). Since you don't like to repeat yourself, sfPropelFinder will quietly check the Propel TableMap to explicit a join if you write:

$postsToRead = sfPropelFinder::from('Post')->
  join('Author')->
  where('Author_Name', '<>', 'Joe')->
  find();


Wait, there is more. The finder can understand complex joins between several tables. Imagine that an Author has a Status. If you want to get all Posts for the Authors with a given Status, just chain join() calls. You get the speed of ActiveRecord-like syntax with the robustness of Propel code behind.

$postsToRead = sfPropelFinder::from('Post')->
  join('Author')->join('Status')->
  where('Status_Name', 'polite')->
  find();


Reduce queries, but don't increase code in return

Lastly, if you rely on Propel's generated doSelectJoinXXX() methods to reduce query count, you are probably often frustrated that the method you need is never among the generated ones. And if you ever tried writing such a method yourself, dealing with composite resultsets and complex hydrating, you probably upgraded your database for more raw power instead of pulling your hair off.

Fortunately, sfPropelFinder offers a convenient with() method allowing you to list the objects you want to be hydrated together with the main object. The following code issues only one database query:

$latestPosts = sfPropelFinder::from('Post')->
  with('Author', 'Status', 'Category')->
  orderBy('PublishedAt', 'desc')->
  find(10);
foreach($postsToRead as $post)
{
  echo $post->getTitle(),
       $post->getCategory()->getName(),              // Post's Category is already hydrated: no query
       $post->getAuthor()->getName(),                // Post's Author is already hydrated: no query
       $post->getAuthor()->getStatus()->getName();   // Author's Status is already hydrated: no query
}


Conclusion

I hope you find this syntax easier to use than Propel's native methods. The sfPropelFinder plugin gets better and better every day thanks to the contributions of several developers, and is already a good alternative to Peer classes and Criteria calls. This should give you time to deal with the more important stuff.

Application Lego: Build a Wiki with Symfony in 20 Minutes

This tutorial shows how fast you can develop with symfony. It showcases symfony's admin generator capabilities, and makes great use of a couple of symfony plugins.

Installation

Start with an empty symfony sandbox. Then use the symfony command line to install two plugins from the symfony plugins repository. Open a terminal, go to the sf_sandbox repository and type:

> php symfony plugin-install http://plugins.symfony-project.com/sfPropelVersionableBehaviorPlugin
> php symfony plugin-install http://plugins.symfony-project.com/sfAdvancedAdminGeneratorPlugin

The first plugin requires one change in the default symfony configuration, since it is a behavior. A behavior is a model modification bringing additional capabilities to the model classes for which it is enabled. To activate behaviors in your symfony project, you need to change the last line of the config/propel.ini to:

propel.builder.addBehaviors = true

Model initialization

A wiki is basically a tool to manage articles and to keep every modification of these articles. This means that you need at least an article table to store the information of the articles. So define the following structure in the config/schema.yml file:

propel:
  article:
    id:         ~
    title:      varchar(255)
    body:       longvarchar
    version:    integer
    updated_at: ~

From this description, symfony can both initialize a database and an object model to map the table to an object-oriented API. You just need to type in the command line:

> php symfony propel-build-all

The Article model should be versionable, so that every modification to it creates a new version. Thanks to the plugin you just installed, this is as easy as adding this line at the end of the lib/model/Article.php model file:

sfPropelBehavior::add('Article', array('versionable'));

One last thing: clear the cache. Don't forget to do this every time you add a class

> php symfony clear-cache

Check that nothing is broken by browsing to the application's default page:

http://localhost/sf_sandbox/web/frontend_dev.php

Article edition interface

Use the command line to create a wiki module based on the Article model with the admin generator:

> php symfony propel-init-admin frontend wiki Article

Play with the apps/frontend/modules/wiki/config/generator.yml file until you get something satisfactory, or paste the following configuration:

 generator:
   class:              sfAdvancedAdminGenerator
   param:
     model_class:      Article
     theme:            default

     list:
       title:        List of Articles
       sort:         title
       click_action: show
       display:      [=title, updated_at]
       filters:      [title, updated_at]
       object_actions:
         _show:      { name: View Article }
         _edit:      { name: Edit Article }
     show:
       actions:
         _list:      { name: Back to the list }
         _edit:      { name: Edit Article }
     edit:
       fields:
         updated_at: { type: plain }
         version:    { type: plain }
         body:       { params: size=80x15 }
       actions:
         _save:      { name: Save modifications }
         _show:      { name: View Article }
         _list:      { name: Back to the list }
         _delete:    { name: Delete Article }

The results should be visible at the following URL. Go on, play with the interface, enter a few articles, try to edit one a few times - you will see that the version number automatically increases in the show view. That's one of the benefits offered by the sfPropelVersionableBehaviorPlugin.

http://localhost/sf_sandbox/web/frontend_dev.php/wiki

Note that the above file uses an extension for the symfony Admin Generator called sfAdvancedAdminGenerator - that's the purpose of the second plugin you installed. It offers a "show" view and separates the "create" view from the "edit" one. Apart from that, the syntax is a simple illustration of the Admin Generator capabilities - refer to the 'Admin Generator' Chapter in the symfony book for more information.

History of modifications on an article

A wiki keeps every revision of an article, and offers a list of past revisions. So you will add a history action to the wiki module. Edit the apps/frontend/modules/wiki/actions/actions.class.php class and add the following method:

public function executeHistory()
{
  $this->article = $this->getArticleOrCreate();
}


This method just reuses an existing method created by the Admin Generator. This getArticleOrCreate returns an Article object based on the id request parameter. If you are curious about how this method works, you will find the generated actions class in cache/frontend/dev/modules/autoWiki/actions/actions.class.php.

Now, to the history template. Create a apps/frontend/modules/wiki/templates/historySuccess.php file with the following content:

<?php use_stylesheet('/sf/sf_admin/css/main') ?>

<div id="sf_admin_container">
  <h1><?php echo sprintf('History of "%s" modifications', $article->getTitle()) ?></h1>
  <div id="sf_admin_content">

    <?php foreach ($article->getAllResourceVersions('desc') as $resourceVersion): ?>
    <div class="form-row">
      <?php echo sprintf("'%s', Version %d, updated on %s (%s)\n",
        link_to($resourceVersion->getTitle(), 'wiki/show?id='.$article->getId().'&version='.$resourceVersion->getNumber()),
        $resourceVersion->getNumber(),
        $resourceVersion->getCreatedAt(),
        $resourceVersion->getComment()
      ) ?>
    </div>
    <?php endforeach; ?>

    <ul class="sf_admin_actions">
      <li><?php echo button_to('Show Article', 'wiki/show?id='.$article->getId(), 'class=sf_admin_action_show') ?></li>
      <li><?php echo button_to('Edit Article', 'wiki/edit?id='.$article->getId(), 'class=sf_admin_action_edit') ?></li>
    </ul>
  </div>
</div>


The getAllResourceVersions() method is a model extension provided by the sfPropelVersionableBehaviorPlugin, which returns an array of ResourceVersion objects, each giving details about a particular revision.

To check the resulting HTML for the new action and template, just browse to:

http://localhost/sf_sandbox/web/frontend_dev.php/wiki/history/id/1

You should see the history of modifications of the article of id 1. Each revisions provides a link to the show view. For this link to display the correct version, you need to override one method of the actions class. Basically, if a version parameter is present in the request, this method forces the article to this version.

protected function getArticleOrCreate($id = 'id')
{
  $article = parent::getArticleOrCreate($id);
  if($this->getRequest()->hasParameter('version'))
  {
    $article->toVersion($this->getRequest()->getParameter('version'));
  }

  return $article;
}


That's about all. Oh, yes, you can plug the history page into your other generator's pages by modifying the generator.yml. For both the show and the edit view, add the following entry under the actions: key:

_history:   { name: History }

Conclusion

In about 50 lines and 20 minutes, you have a working wiki, with search, sorting and pagination capabilities. You can review (and reuse) any older revision of an article. Of course, this is not enough to publish your new app as a shiny open-source project - you should at least add user authentication through sfGuardPlugin and article formatting through markdown. Maybe you will need to write an additional 20 lines of code for that.

But that illustrates how to build up an application with symfony. The framework and the plugins provide the features, you just assemble them and glue them together according to your needs. Just like you used to assemble Legos to create a fire station. Since there are very few lines of code, the application is easy to review, maintain, and run.

sfPropelFinder is like jQuery for Propel

Are you tired of writing long Criteria definitions for simple queries? Do you sometimes wish that symfony had an ActiveRecord (even if it is not really possible in PHP5 as of now)? Do you feel that the world of Javascript has changed since jQuery came up? Do you like the way sfFinder and sfTestBrowser work?

Then take a look at this:

// With Peer and Criteria
$c = new Criteria()
$c->add(ArticlePeer::TITLE, '%world', Criteria::LIKE);
$c->add(ArticlePeer::IS_PUBLISHED, true);
$c->addAscendingOrderByColumn(ArticlePeer::CREATED_AT);
$articles = ArticlePeer::doSelect($c);

// with sfPropelFinder
$articles = sfPropelFinder::from('Article')->
  whereTitle('like', '%world')->
  whereIsPublished(true)->
  orderByCreatedAt()->
  find();


This example shows the philosophy of a new symfony plugin I just commited. Check it out if you're interested.

Live User Testing with sfSpyPlugin

Doing user testing is more or less like watching users interact with an application. sfSpyPlugin allows you do to it online and for free, instead of paying a lot for doing it for real.

User testing is usually a long and expensive process that few organizations can afford. You must recruit users, write scenarios, animate the tests, record browsing sessions, and visualize the tapes. What if you could just choose some of the users currently using the application online, start recording their actions, and playback their way through the application later? That's what the new sfSpyPlugin offers.

See it in action: One browser window is "spying" another browser window - but that could very well be two different computers. Click on the following link to see a 2 minutes screencast of the plugin in action.

http://screencast.com/t/tnhx8kBl5P1

The plugin displays a list of the online users on any symfony application. You can choose to watch online or record for later the actions of any user. The replay interface offers VCR-like controls to pause, restart, accelerate or slow down the playback. You can also organize your recordings by giving them a name. The interface should be pretty self-explanatory, and yet there is a complete installation and usage guide available at the symfony project wiki.

And if you are not of the point-and-click type, you can always trigger the recording from code, to watch how a certain user uses the application, or how a certain module of the application is used.

This way, you can watch and monitor how users interact with your application, or part of it. The only think you will miss is the voice of the user commenting what he/she is doing. I created this plugin so that you have no excuse to let live applications having a poor usability without doing anything to improve them...

Motilee: Symfony Powered Forum Engine Released Open Source

A new open-source project is born. It is called Motilee, and it is a forum engine written in PHP5 and symfony. Go take a look at the demo site and at the source code to see what it's like.

Motilee Forum

Motilee is a symfony application built entirely from plugins. Take a sandbox, pour in the sfSimpleForumPlugin, the sfGuardPlugin, the sfLucenePlugin, the sfFeed2Plugin and a few others, add some documentation, and mix the whole for a few days. There you go, a complete application comes out of the server. Download it and install it as you wish. Motilee is free to use and released under the MIT license.

Motilee focuses on user experience and tries to propose a new approach to online discussions. Don't focus too much on the graphical design, it will soon be reworked.

A stable release is yet to come, and the most important enhancements are already listed in the trac. Development is going pretty fast, and if you want to participate in the project, there is room for more developers. Drop me an email and I'll open you an account in the trac.

Customizing a plugin schema without getting your hands dirty

In my previous post, I exposed a first approach to making a plugin schema customizable. After some coding, I came up with a clean solution, in the shape of a plugin: the sfPropelAlternativeSchemaPlugin. It introduces a new syntax for propel database schemas. It's a little more verbose, but easier to read and totally extensible.

Here is an example of a schema with the new syntax:

connection:           propel
noXsd:                false
defaultIdMethod:      none
package:              lib.model

classes:
  Group:
    tableName:        ab_group
    package:          foo.bar.lib.model
    columns:
      id:
      name:           varchar(50)

  User:
    tableName:        cd_user
    isI18N:           true
    i18nTable:        cd_user_i18n
    columns:
      first_name:     { type: varchar, size: 255, default: "Anonymous" }
      last_name:      varchar(50)
      age:            { type: integer, required: true, index: true }
      ab_group_id:
      created_at:

  CdUserI18n:
    columns:
      description:    longvarchar

  EfArticle:
    columns:
      title:          { type: longvarchar, required: true, index: unique }
      stripped_title: { type: longvarchar, required: true, primaryKey: true, sequence: my_custom_sequence_name }
      user_id:
      my_group:       { type: integer, foreignTable: ab_group, foreignReference: id, onDelete: setnull }
      created_at:     timestamp
      updated_at:

  Article:
    tableName:        ij_article
    columns:
      title:          varchar(50)
      user_id:        { type: integer }
      created_at:
    foreignKeys:
      -
        foreignTable: cd_user
        onDelete:     cascade
        references:
          - { local: user_id, foreign: id }
    indexes:
      my_index:       [title, user_id]
    uniques:
      my_other_index: [created_at]

  AbGroupI18n:
    columns:
      motto:          longvarchar

Refer to the plugin's README file for more information on installation and usage.

Customizing a plugin schema

In the evolution of symfony, plugins are an important step. But before we can think of applications made entirely with plugins - the 'Lego' dream - the framework must offer a way to customize plugin schemas.

Why plugin schemas must be configurable

More and more symfony plugins offer high-level features, even entire modules, based on a bundled schema. The sfGuardPlugin, for instance, defines several tables for user management. The sfSimpleForumPlugin defines tables for forums, topics and posts, in order to provide a complete forum packaged in one plugin.

The problem is that unlike all the other parts of a plugin, a plugin schema can't be customized. This is a very strong limitation, and it makes plugins impossible to use in many cases.

For instance, the sfGuardUser table doesn't have an email column. As its schema can't be customized, this means that if you want to use sfGuard and handle user emails (that should be about 99% of the cases), then you have to define a new table for user details with a 1:1 relationship to the sfGuardUser table. Bummer.

Worse, if you want to use a behavior, like for instance the sfPropelParanoidBehavior, on a plugin model, you simply can't. The said behavior needs an extra deleted_at column to be added to the table, and you can't add it to a plugin schema - or else it will be deleted the next time you update the plugin.

Using PHP in YAML files

But nothing is impossible with symfony. By using two powerful features of the symfony YAML parser, it is possible to make a schema more configurable.

First, you probably know that symfony sees YAML files as PHP, and therefore executes them before interpreting the YAML inside. This means that a YAML schema can include PHP code, and that gives a certain level of customization to schemas. For instance, look at how the sfSimpleBloginPlugin schema offers a way to change the connection and tables names:

// in plugins/sfSimpleBlogPlugin/config/schemaConfig.php
<?php

// Default values
$config = array(
  'connection'    => 'propel',
  'user_table'    => 'sf_guard_user',
  'user_id'       => 'id',
  'post_table'    => 'sf_blog_post',
  ...
);

// Check custom project values in my_project/config/sfBlogPlugin.yml
if(is_readable($config_file = sfConfig::get('sf_config_dir').'/sfBlogPlugin.yml'))
{
  $user_config = sfYaml::load($config_file);
  if(isset($user_config['schema']))
  {
    $config = array_merge($config, $user_config['schema']);
  }
}

// in plugins/sfSimpleBlogPlugin/config/schema.yml
<?php include('schemaConfig.php') ?>
<?php echo $config['connection'] ?>:
  _attributes:       { package: plugins.sfSimpleBlogPlugin.lib.model }
  <?php echo $config['post_table'] ?>:
    _attributes:     { phpName: sfSimpleBlogPost }
    id:              ~
    author_id:       { type: integer, foreignTable: <?php echo $config['user_table'] ?>, foreignReference: <?php echo $config['user_id'] ?>, onDelete: cascade  }
    title:           varchar(255)
    stripped_title:  { type: varchar(255), index: unique }
    extract:         longvarchar
    content:         longvarchar
    is_published:    { type: boolean, default: false }
    allow_comments:  { type: boolean, default: true }
    created_at:      ~
    ...


To customize table names, you just need to create a config/sfBlogPlugin.yml overriding the values of the $config array, like for instance:

// in config/sfBlogPlugin.yml
schema:
  connection: foo
  user_table: my_user
  post_table: bar_post


In a previous post, I explained how a custom config handler can make this task even easier.

This is more or less a hack, and it allows only the customization of predefined elements of the schema. Besides, PHP code makes the YAML schema less readable.

A YAML file is an array

A second property of YAML files in symfony is that, if they return a PHP array, then the YAML parser won't be invoked at all. Imagine the following YAML file:

foo: [bar1, bar2, bar3]
<?php return array('foo' => array('bar1', 'bar2'))?>


Since the YAML file returns an array, the YAML code is ignored. Everything happens as if the file had been:

foo: [bar1, bar2]


Combining this property and the power of sfToolkit::arrayDeepMerge(), it is possible to allow more configuration to plugin schemas. Look at how it is done in the sfSimpleForumPlugin:

// in plugins/sfSimpleForumPlugin/config/schemaConfig.php
<?php

function start_schema()
{
  ob_start();
}

function end_schema()
{
  $tables = array(
    'connection'       => 'propel',
    'user_table'       => 'sf_guard_user',
    'user_id'          => 'id',
    'forum_table'      => 'sf_simple_forum_forum',
    'topic_table'      => 'sf_simple_forum_topic',
    'post_table'       => 'sf_simple_forum_post',
    ...
  );

  $custom_fields = array();

  // Check custom project values in my_project/config/sfBlogPlugin.yml
  if(is_readable($config_file = sfConfig::get('sf_config_dir').'/sfSimpleForumPlugin-schema-custom.yml'))
  {
    $user_config = sfYaml::load($config_file);
    if(isset($user_config['tables']))
    {
      $tables = array_merge($tables, $user_config['tables']);
    }

    if(isset($user_config['custom_fields']))
    {
      $custom_fields = $user_config['custom_fields'];
    }

  }

  $yaml_schema = ob_get_clean();
  foreach ($tables as $key => $value)
  {
    $yaml_schema = str_replace('%'.$key.'%', $value, $yaml_schema);
  }

  $schema = sfYaml::load($yaml_schema);
  $schema[$tables['connection']] = sfToolkit::arrayDeepMerge($schema[$tables['connection']], $custom_fields);

  return $schema;
}

// in plugins/sfSimpleForumPlugin/config/schema.yml
<?php include_once('schemaConfig.php') ?>
<?php start_schema() ?>
%connection%:
  ...
  %topic_table%:
    _attributes:   { phpName: sfSimpleForumTopic, package: plugins.sfSimpleForumPlugin.lib.model }   
    id:
    title:         varchar(255)
    is_sticked:    { type: boolean, default: false }
    is_locked:     { type: boolean, default: false }
    forum_id:      { type: integer, foreignTable: %forum_table%, foreignReference: id, onDelete: cascade }
    created_at:
    updated_at:
    # performance enhancers
    latest_post_id: { type: integer, foreignTable: %post_table%, foreignReference: id, onDelete: setnull }
    user_id:       { type: integer, foreignTable: %user_table%, foreignReference: %user_id%, onDelete: setnull }
    stripped_title: varchar(255)
    nb_posts:      { type: bigint, default: 0 }
    nb_views:      { type: bigint, default: 0 }
  ...
<?php return end_schema() ?>


Not only is the schema more readable (the embedded PHP statements were replaced by tokens), it is now possible to add new fields to the schema:

// in config/sfSimpleForumPlugin-schema-custom.yml
tables:
  topic_table: my_topic_table
custom_fields:
  my_topic_table:
    deleted_at: timestamp


At last, it makes it possible to extend existing plugin schemas... That is, if the plugin planned for it.

What's next?

This feature is so crucial for plugins adoption that it should, in fact, be built-in the symfony plugin system instead of being implemented in each plugin. Users should be able to add a YAML file in the project's config/ directory for each of the schemas provided by the installed plugins, following a naming convention similar to the one I exposed in the sfSimpleForumPlugin.

Of course, this will never work for plugins that use the XML syntax for schemas. Unless we tell the propel tasks to transform every XML schema to YAML, to allow for customization, before transforming them back to XML, which is the only format Propel understands. But to me, the ability to be customized should clearly promote the YAML schema syntax as the only standard for symfony plugins. Future versions of symfony should drop support for the legacy XML schema syntax.

There is another step that could be taken towards standardization... The schema customization exposed in the sfSimpleForumPlugin example uses two different techniques to change table names and add fields. But if the YAML schema syntax was a little different, all this could be done simply through one YAML file. I'm thinking about a new syntax that would mix the current symfony schema syntax and the Doctrine schema syntax. Imagine that the topic table above is defined as follows:

connection: propel
objects:
  sfSimpleForumTopic:
    tableName: sf_simple_forum_topic
    package: plugins.sfSimpleForumPlugin.lib.model
    columns:
      id:
      title:         varchar(255)
      is_sticked:    { type: boolean, default: false }
      is_locked:     { type: boolean, default: false }
      forum_id:      { type: integer, foreignClass: sfSimpleForumForum, foreignReference: id, onDelete: cascade }
      created_at:
      updated_at:
      # performance enhancers
      latest_post_id: { type: integer, foreignClass: sfSimpleForumPost, foreignReference: id, onDelete: setnull }
      user_id:       { type: integer, foreignClass: sfGuardUser, foreignReference: id, onDelete: setnull }
      stripped_title: varchar(255)
      nb_posts:      { type: bigint, default: 0 }
      nb_views:      { type: bigint, default: 0 }
  ...


If this is automatically merged with user-defined schemas, it would be very easy to change table names or add custom fields, for instance with this file:

connection: my_connection
objects:
  sfSimpleForumTopic:
    tableName: my_topic_table
    columns:
      deleted_at: timestamp


Conclusion

The current YAML syntax for schemas (which I designed myself) was a first step, but it needs to evolve. To be able to fully replace XML schemas, and to allow compatibility between Propel and Doctrine, it is probably necessary to change it - but preserve backward compatibility, of course.

Developing an extension of the sfPropelDatabaseSchema class to implement both the new syntax and the ability to customize schemas is possible, but before getting myself into it, I wonder: Would anybody else benefit from this?

Understanding Behaviors

When you happen to write twice the same method for two classes of the Propel object model, it is time to think about symfony Behaviors. Behaviors offer a simple way to extend several model classes the same way, by altering existing methods or adding new ones. Using existing behaviors is quite simple: follow the instructions written in the README file bundled with the behavior, and you're done. But if you want to create a new behavior, you need to understand how they work.

I'm paranoid, how are you?

To illustrate the process of creating a behavior, let's start with an already extended model. For instance, let's imagine that for security reasons, the records of the article table must not be removed from the database. The $article->delete() method must still mark records so that they are not returned by a call to ArticlePeer::doSelect(), but the underlying data must not be erased. This is how you could extend the Propel model to implement this rule:

// in lib/model/Article.php
class Article extends BaseArticle()
{
  public function delete($con = null)
  {
    $this->setDeletedAt(time());
    $this->save($con);
  }
}

// in lib/model/ArticlePeer.php
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    $criteria->add(self::DELETED_AT, null, Criteria::ISNULL);
   
    return parent::doSelectRS($criteria, $con);
  }
}


Of course, that implies adding a new timestamp field called deleted_at to the article table.

Note: The reason why we extend doSelectRS() instead of doSelect() is because the former is used not only by doSelect(), but also by doCount().

The combination of a new field and altered methods gives to the Article object a "paranoid" behavior. For now, the word "behavior" just refers to a set of methods.

Enter Mixins

Now, imagine that you need to keep also the deleted records of the comment table. Instead of copying the two methods above in the Comment and CommentPeer classes, which would not be D.R.Y., you should refactor the code used more than once in a new class, and inject it via the Mixins system. You should be familiar with the concept of Mixins and the sfMixer class to understand the following, so refer to Chapter 17 of the symfony book if you wonder what this is about.

The first step is to remove any code from the model classes, and to add hooks to allow them to be extended.

// Step 1
// in lib/model/Article.php
class Article extends BaseArticle()
{
  public function delete($con = null)
  {
    foreach (sfMixer::getCallables('Article:delete:pre') as $callable)
    {
      $ret = call_user_func($callable, $this, $con);
      if ($ret)
      {
        return;
      }
    }
   
    return parent::delete($con);
}

// in lib/model/ArticlePeer.php
class ArticlePeer extends BaseArticlePeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('ArticlePeer:doSelectRS:doSelectRS') as $callable)
    {
      call_user_func($callable, 'ArticlePeer', $criteria, $con);
    }
   
    return parent::doSelectRS($criteria, $con);
  }
}

// in lib/model/Comment.php
class Comment extends BaseComment()
{
  public function delete($con = null)
  {
    foreach (sfMixer::getCallables('Comment:delete:pre') as $callable)
    {
      $ret = call_user_func($callable, $this, $con);
      if ($ret)
      {
        return;
      }
    }
   
    return parent::delete($con);
}

// in lib/model/CommentPeer.php
class CommentPeer extends BaseCommentPeer()
{
  public function doSelectRS(Criteria $criteria, $con = null)
  {
    foreach (sfMixer::getCallables('CommentPeer:doSelectRS:doSelectRS') as $callable)
    {
      call_user_func($callable, 'CommentPeer', $criteria, $con);
    }
   
    return parent::doSelectRS($criteria, $con);
  }
}


Next, you must put the behavior code in a new class, save this class in a directory where it can be autoloaded:

// Step 2
// In lib/ParanoidBehavior.php
class ParanoidBehavior
{
  public function preDelete($object, $con)
  {
    $object->setDeletedAt(time());
    $object->save($con);
   
    return true;
  }
 
  public function doSelectRS($class, Criteria $criteria, $con = null)
  {
    $criteria->add(constant("$class::DELETED_AT"), null, Criteria::ISNULL);
  }
}


Finally, you must register the methods of the new ParanoidBehavior class on the hooks of the Article and Comment classes:

// Step 3
// in config/config.php
sfMixer::register('Article:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('ArticlePeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));
sfMixer::register('Comment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('CommentPeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));


By the power of Mixins, the code of the behavior can be reused across several model objects.

But the task of adding hooks to the model classes and registering the methods make this process longer than a simple copy of the code... This is where the symfony Behaviors come as a great help.

Add model hooks automatically

Symfony can add hooks to the model automatically. To enable these hooks, set the AddBehaviors property to true in the propel.ini file, as follows:

propel.builder.AddBehaviors = true     // Default value is false


You need to rebuild the model for the hooks to be inserted in the generated model classes:

$ php symfony propel-build-model


The hooks are added to the Base classes, the ones under the lib/model/om/ directory. For instance, here is an extract of the generated BaseArticlePeer class with behaviors hooks enabled:

public static function doSelectRS(Criteria $criteria, $con = null)
{
  foreach (sfMixer::getCallables('BaseArticlePeer:doSelectRS:doSelectRS') as $callable)
  {
    call_user_func($callable, 'BaseArticlePeer', $criteria, $con);
  }
 
  // Rest of the code
}


That's almost exactly the same hook as the one we added by hand to the custom ArticlePeer during step 1. The difference is that the registered hook name is BaseArticlePeer:doSelectRS:doSelectRS instead of ArticlePeer:doSelectRS:doSelectRS. So you can remove the code added to the custom classes during Step 1. This means that when Behaviors are enabled in the propel.ini, you no longer need to add hooks manually inside your model classes.

As the name of the hooks changed (they are now all prefixed by Base), we need to change the way the methods of the Paranoid behavior are registered in Step 3. But before doing so, have a look at the complete list of hooks added:

// Hooks added to the base object class
[className]:delete:pre     // before deletion
[className]:delete:post    // after deletion
[className]:save:pre       // before save
[className]:save:post      // after save
[className]:[methodName]   // inside __call() (allows for new methods)
// Hooks added to the base Peer class
[PeerClassName]:doSelectRS:doSelectRS
[PeerClassName]:doSelectJoin:doSelectJoin
[PeerClassName]:doSelectJoinAll:doSelectJoinAll
[PeerClassName]:doSelectJoinAllExcept:doSelectJoinAllExcept
[PeerClassName]:doUpdate:pre
[PeerClassName]:doUpdate:post
[PeerClassName]:doInsert:pre
[PeerClassName]:doInsert:post

Note: As of symfony 1.0, there is only one hook related to the doSelect methods, instead of the four hooks described above. This explains why some behaviors work only with symfony 1.1 and not with symfony 1.0, which has an incomplete support for behaviors.

Adding new methods

One of the hooks should get a closer look: the one allowing for new methods in the object class. When Behaviors are enables in propel.ini, all the generated base object classes contain a __call() method similar to this one:

// in lib/model/om/BaseArticle.php
public function __call($method, $arguments)
{
  if (!$callable = sfMixer::getCallable('BaseArticle:'.$method))
  {
    throw new sfException(sprintf('Call to undefined method BaseArticle::%s', $method));
  }

  array_unshift($arguments, $this);

  return call_user_func_array($callable, $arguments);
}


As explained in Chapter 17 of the symfony book, a hook placed in a __call() makes the addition of new methods at runtime possible. For instance, if you want to add an undelete() method to the Article class to allow to reset the deleted_at flag, start by adding it to the Behavior class:

// In lib/ParanoidBehavior.php
public function undelete($object, $con)
{
  $object->setDeletedAt(null);
  $object->save($con);
}


Then, register the new method as follows:

// in config/config.php
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
// other hooks


Now, every call to $article->undelete() will make a call to ParanoidBehavior::undelete($article).

Note: Unfortunately, as of PHP 5, static method calls cannot be caught by a __call(). This means that symfony behaviors are not able to add new methods to the Peer classes.

Register hooks in a single step

You still need to rewrite the other hook registrations to use the Base hook names, both for the Article and Comment classes. That would give something like:

// Step 3
// in config/config.php
sfMixer::register('BaseArticle:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseArticle:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseArticlePeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));

sfMixer::register('BaseComment:undelete', array('ParanoidBehavior', 'undelete'));
sfMixer::register('BaseComment:delete:pre', array('ParanoidBehavior', 'preDelete'));
sfMixer::register('BaseCommentPeer:doSelectRS:doSelectRS', array('ParanoidBehavior', 'doSelectRS'));


But this code is not very D.R.Y., since you need to repeat the whole list of methods for each class. Imagine the pain if the behavior provided several dozens methods! It would be much more efficient if you could separate the registration process in two phases:

  1. Register methods of the behavior into a list of class-agnostic hooks.
  2. For each class, transform the class-agnostic hooks into real hooks and register them using the mixins system.

Symfony provides a utility class, called sfPropelBehavior, which does this job for you. Here is how the Step 3 can be rewritten to take advantage of this class:

// Phase 1
// in config/config.php
sfPropelBehavior::registerMethods('paranoid', array(
  array('ParanoidBehavior', 'undelete')
));
sfPropelBehavior::registerHooks('paranoid', array(
  ':delete:pre'                => array('ParanoidBehavior', 'preDelete'),
  'Peer:doSelectRS:doSelectRS' => array('ParanoidBehavior', 'doSelectRS')
));

// Phase 2
// in lib/model/Article.php
sfPropelBehavior::add('Article', array('paranoid'));

// in lib/model/Comment.php
sfPropelBehavior::add('Comment', array('paranoid'));


Both the registerMethods and registerHooks methods expect a hook list name as first parameter. This name is then used as a shortcut when the behavior methods are added to the model classes. Notice how the hook names used when calling registerHooks don't contain any reference to a specific model class (the BaseArticle part of the hook name was removed).

Also, you don't need to specify a method name for the methods added by way of registerMethods. The name of the method in the behavior class is used by default.

It's only when the sfPropelBehavior::add() statement is executed that hooks are really registered against the sfMixer class... with a real hook name. As the first parameter of this call is a model class name, the sfPropelBehavior has all the elements to recreate the complete hook names (in this case, by concatenating the string Base with the model class name and the behavior hook name).

Packaging a behavior into a plug-in

To package the behavior into a truly reusable piece of code, the best is to create a plugin.

There is a non-written convention about behavior plugin names. They must be prefixed with 'Propel' since they work only for this ORM, and they must end with 'BehaviorPlugin'. So a good name for our Paranoid behavior could be 'myPropelParanoidBehaviorPlugin'.

As of now, there are only two files to put in the plugin: the ParanoidBehavior class, and the code written in config/config.php to register the behavior methods and hooks. Chapter 17 explains how to organize these files in a plugin tree structure:

plugins/
  myPropelParanoidBehaviorPlugin/
    lib/
      ParanoidBehavior.php    // the class containing methods to be mixed in
    config/
      config.php              // the registration of the behavior methods

The config.php file of every plugin installed in a project is executed at each request, so this is the perfect place to register behavior methods.

To complete the plugin, you must add a README file at the root of the plugin's directory, with installation and usage instructions. The best behaviors also bundle unit tests.

Eventually, add a package.xml (either manually or by way of sfPackageMakerPlugin), package the plugin with PEAR, and you are ready to reuse it. You can also post it in the symfony website.

Passing a parameter to a behavior

A well-designed behavior doesn't rely on hard coded values. In the example of the Paranoid behavior above, the deleted_at column name is hard coded and should be transformed into a parameter.

To pass a parameter to a behavior, use an associative array as the second parameter of the call to sfPropelBehavior::add() instead of a regular array, as follows:

sfPropelBehavior::add('Article', array('paranoid' => array(
  'column' => 'deleted_at'
)));

Then, to get the value of this parameter in the behavior class, you must use the `sfConfig` registry. The parameter is stored in a `sfConfig` key composed like this:

    'propel_behavior_' . [BehaviorName] . '_' . [ClassName] . '_' . [ParameterName]
    // in the example above, get the 'deleted_at' value by calling
    sfConfig::get('propel_behavior_paranoid_Article_column')

The problem is that the behavior methods don't use only column names. They use the various versions of these names according to the operation to achieve:

    Format name                 | Example       | Used in
    ----------------------------|---------------|-----------
    `BasePeer::TYPE_FIELDNAME`  | `deleted_at`  | schema.yml
    `BasePeer::TYPE_COLNAME`    | `DeletedAt`   | Method names
    `BasePeer::TYPE_PHPNAME`    | `DELETED_AT`  | Criteria parameters
   
So the behavior class will need a way to translate a field name from one format to the other. Fortunately, the generated Base Peer class of every model provides a static `translateFieldName()` method. Its syntax is quite simple:

[code]
// translateFieldName($name, $origin_format, $dest_format)
// for instance
$name = ArticlePeer::translateFieldName('
deleted_at', BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME);


So you are now ready to rewrite the ParanoidBehavior class to take the column parameter into account:

class sfPropelParanoidBehavior
{
  public function preDelete($object, $con = null)
  {
    $class = get_class($object);
    $peerClass = get_class($object->getPeer());

    $columnName = sfConfig::get('propel_behavior_paranoid_'.$class.'_column', 'deleted_at');
    $method = 'set'.call_user_func(array($peerClass, 'translateFieldName'), $columnName, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_PHPNAME);
    $object->$method(time());
    $object->save();

    return true;
  }
 
  public function doSelectRS($class, $criteria, $con = null)
  {
    $columnName = sfConfig::get('propel_behavior_paranoid_'.$class.'_column', 'deleted_at');
    $criteria->add(call_user_func(array($class, 'translateFieldName'), $columnName, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME), null, Criteria::ISNULL);
  }
}


Conclusion

Propel Behaviors are nothing more than a set of predefined hooks, and a helper class designed to facilitate the registration of several hooks in a single statement. If you understand Mixins, it shouldn't be too hard to author your own behaviors. Make sure you check the existing behavior plugins before starting your own: they are practical examples of the behaviors syntax.

When Ajax can speed up your site

Many people think that adding Ajax interactions to a web application can cripple a website's performance. Of course, if you add remote periodical executers everywhere, or if you make