Archive for April, 2008

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.

Virtual Fun, Real Decay

It took me three stations, after I stepped into this metro, to react. There was a man lying on the floor, apparently sleeping. A tramp. In the middle of a wagon. At 10:30pm in Paris, France.

We live in a time of marvel. Just an hour before, I was showing off with my new iPhone to my friends. There are so many things you can do with this thing, they said. Look at how you can control it with your finger, they said. I wish I had one of my own, they said.

Just one hour after that, I stepped into a metro wagon in the Republique station. There was a man lying on the floor. And you know, this is kind of usual. Maybe that's the worst part of it. People are used to seeing tramps lying on the floor, especially at night. It's just normal.

For three stations, I kept saying to myself: You've got to do something. There is a man on the floor. We were about thirty in that wagon, and everybody seemed to ignore the man on the floor. Sure he looked filthy, and probably drunk, even if his face couldn't be seen. But who else would lie on a wagon floor, apart from a drunk tramp? And the people in the wagon were thinking, out loud: Can't this man go sleep somewhere else? This is the metro, for God's sake.

After three stations, I stood up and went to the man. I woke him up, talked to him, helped him out of the wagon. The guy had been beaten up and stolen. He had a broken arm and three broken ribs. Sure he was a tramp, I could tell that he hadn't had a shower in weeks. He couldn't say three words in a row. Was he asleep, drunk or mentally disordered? That doesn't make any difference. The guy was in desperate need for help.

You know what? I'm mad at all the people who entered the wagon before me. I'm mad at me, because it took me so long to react. I'm mad at all the people who stepped into the wagon after me and didn't react.

We live in a time of marvel. Except that I would give all the mobile Internet revolution to see this man among his family, not in danger, in good health, with a job and someone to look after him. I would give the entire symfony project for this man's broken arm.

Not that it's news. Just a sting to remember.

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.