Archive for August, 2008

Everybody Goes to Symfony Camp

And that includes me. I will be giving a presentation there, in two weeks from now, called “Developing for Developers - Usability Applied to Programming”, and illustrated by my recent work on DbFinderPlugin.

If you’d like to meet the best PHP5 developers in the world, or me, you should definitely go to the Symfony Camp on September 12th and 13th. The conference is in The Netherlands, not far from Amsterdam - that means not far from anybody in Europe. I heard there are some tickets left for the conference, but it won’t last long. The price is not free, but with the great people talking there, the tasty barbecue, the unique atmosphere and a huge lawn to put your tent in, it’s a bargain. Besides, they may fill the swimming pool this year.

We’ll have plenty of time to speak about symfony, plugins, documentation, the future and everything else. It’s a unique opportunity to meet in person all those who lead the symfony community. Also, there are one or two seats left for the training session, so if you want to become operational in symfony quickly, dive in.

One last world for those who expect drama: There Won’t Be Blood.

sfPropelFinder becomes DbFinder - Announcing 1.0 release

The sfPropelFinder plugin, which I've told you about a lot lately, has recently been renamed to DbFinder. This emphasizes the fact that the plugin is not Propel-specific anymore, and that you can use it with Doctrine without any change in the API.

Also, I have released a version 0.9 of the plugin today, which marks the 100% coverage of the API with both the Propel and the Doctrine adapters. That's right, now any piece of code using DbFinder will work seamlessly, whatever the ORM you use in symfony.

Take the following code, for instance:

// Look in the Article model
// For objects where the author object related to the article has $nickname for nickname
// Hydrated with related translation in the current culture and category
// And put the result into a pager implementing sfPager for easy display in a web page
$pager = DbFinder::from('Article')->
  where('Author.Nickname', $nickname)->
  with('I18n', 'Category')->
  paginate($currentPage = 1, $maxResultsPerPage = 10);


Getting the same result with either Propel or Doctrine takes considerably more code.

To be honest, the Doctrine coverage is only 99%, since there is still an issue with sfDoctrineFinder::withColumn() when dealing with a calculated column - and this is something that requires Doctrine 1.0 to be fixed. The current Doctrine adapter is based on sfDoctrinePlugin and Doctrine 0.11. But as soon as Doctrine 1.0 is released, withColumn() will be updated to work exactly the same as with Propel.

This release can be considered as a 1.0 beta 1 - meaning I'll probably not add more features before releasing a stable version. I'll work on performance and edge cases if bugs are reported, so you are encouraged to download the plugin, test it, and give me as much feedback as you can.

Propel 1.3 is out

The news has just hit the Propel home page: Propel long-awaited 1.3 version was released yesterday.

Not only is Propel 1.3 a lot faster than the previous version (thanks to PDO), it also fixes a handful of problems many of us had with Propel 1.2, and adds a few features as well. Here is a quick overview of interesting changes for symfony users:

  • Table and column identifiers are now quoted with ticks in generated SQL code
  • Ability to define your own method names for foreign key getters and setters (refPhpName)
  • Using clearSelectColumns() and addAsColumn() works
  • Fully functional doCount(), handling limit/offset, group by columns, etc.
  • Ability to define the primary key of a new object being inserted
  • Subsequent calls to retrieveByPK() or doSelect*() with the same parameters return the same object instances (a.k.a. "Object Instance Pooling")
  • Hangling of Master-Slave connections in a replicated environment
  • One-to-one relationships
  • Native nested sets implementation (no need for sfPropelActAsNestedSetBehaviorPlugin anymore)
  • doSelectJoinXXX() methods now default to a left join instead of an inner join
  • Ability to self-reference several times AND hydrate related objects

You can check the list of the 242 tickets fixed for this release in the Propel Trac.

It seems that Propel 1.3 will be the default Propel version for symfony 1.2 - Dustin just made the change this morning.

This is great news indeed, and it means that future applications built with symfony and Propel will be even faster and easier to build than the ones we currently know!

Tip: Clean up your symfony debug logs

The symfony web debug toolbar is awesome. You know when partials are executed, you know which queries the database receives, you know all about caching, filters, slots, decorators, and you can even add your own traces there.

But there is one thing that bothers me a lot: each database query executed by Creole displays two or three lines in the web debug toolbar:

{Creole} prepareStatement(): SELECT sf_blog_post.ID, sf_blog_post.AUTHOR_ID, sf_blog_post.TITLE, ...
{Creole} applyLimit(): SELECT sf_blog_post.ID, sf_blog_post.AUTHOR_ID, sf_blog_post.TITLE, ...
{Creole} executeQuery(): [0.70 ms] SELECT sf_blog_post.ID, sf_blog_post.AUTHOR_ID, sf_blog_post.TITLE, ...

I have never needed the first two lines. Only the last one is useful. And the two other lines make the log confusing and too long for no reason. If, like me, you want to clean up your debug toolbar of all the prepareStatement(), applyLimit() and prepareCall() lines, and make it more readable, open the sfDebugConnection class located in symfony/lib/addon/creole/drivers/sfDebugConnection.php, and comment the following lines:

119 //$this->log("{sfCreole} prepareStatement(): $sql");

140 //$this->log("{sfCreole} applyLimit(): $sql, offset: $offset, limit: $limit");

205 //$this->log("{sfCreole} prepareCall(): $sql");

For the same purpose, you may want to remove the "{sfView} initialize view for ..." lines. In order to do so, open symfony/lib/view/sfView.class.php and comment:

303 // $context->getLogger()->info(sprintf('{sfView} initialize view for "%s/%s"', $moduleName, $actionName));

Now you have a fast reading execution log.

Eating My Own Dog Food

I spent the last three hours porting my sfSimpleBlog plugin to sfPropelFinder. While it was the occasion to polish the sfPropelFinder API and fix a bug, it was also a great pleasure to replace Propel Peer/Criteria code with finder one.

The blog plugin is up and running, the code is now much cleaner, and as a bonus, the query count has been reduced. If you want to test it, checkout the latest trunk version of the plugin (the plugin release system of the symfony project website doesn't seem to appreciate my PEAR package).

I find sfPropelFinder code to be naturally flowing. It cuts model classes sizes by 50%, it is much more readable, and makes a few custom model methods useless.

See for yourself. Here is the old PluginsfSimpleBlogPostPeer class, holding the methods required to retrieve blog posts, in the previous version:

class PluginsfSimpleBlogPostPeer extends BasesfSimpleBlogPostPeer
{
  public static function getRecentPager($max, $page)
  {
    $pager = new sfPropelPager('sfSimpleBlogPost', $max);
    $c = new Criteria();
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $pager->setCriteria($c);
    $pager->setPage($page);
    $pager->setPeerMethod('doSelectJoinAll');
    $pager->init();

    return $pager;
  }

  public static function getRecent($max = 10)
  {
    $c = new Criteria();
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $c->setLimit($max);

    return self::doSelectJoinAll($c);
  }

  public static function getTaggedPager($tag, $max, $page)
  {
    $pager = new sfPropelPager('sfSimpleBlogPost', $max);
    $c = new Criteria();
    $c->addJoin(sfSimpleBlogTagPeer::SF_BLOG_POST_ID, self::ID);
    $c->add(sfSimpleBlogTagPeer::TAG, $tag);
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $pager->setCriteria($c);
    $pager->setPage($page);
    $pager->setPeerMethod('doSelectJoinAll');
    $pager->init();

    return $pager;
  }

  public static function getTagged($tag, $max)
  {
    $c = new Criteria();
    $c->addJoin(sfSimpleBlogTagPeer::SF_BLOG_POST_ID, self::ID);
    $c->add(sfSimpleBlogTagPeer::TAG, $tag);
    $c->add(self::IS_PUBLISHED, true);
    $c->addDescendingOrderByColumn(self::CREATED_AT);
    $c->setLimit($max);

    return sfSimpleBlogPostPeer::doSelectJoinAll($c);
  }

  public static function retrieveByStrippedTitleAndDate($text, $date, $con = null)
  {
    if ($con === null)
    {
      $con = Propel::getConnection(self::DATABASE_NAME);
    }

    $criteria = new Criteria(sfSimpleBlogPostPeer::DATABASE_NAME);
    $criteria->add(sfSimpleBlogPostPeer::STRIPPED_TITLE, $text);
    if (sfConfig::get('app_sfSimpleBlog_use_date_in_url', false))
    {
      $criteria->add(sfSimpleBlogPostPeer::PUBLISHED_AT, $date);
    }

    $v = sfSimpleBlogPostPeer::doSelect($criteria, $con);

    return !empty($v)> 0 ? $v[0] : null;
  }
}


And here is the revised version. It is no longer a Peer class, but a Finder class extending DbFinder:

class PluginsfSimpleBlogPostFinder extends Dbfinder
{
  protected $class = 'sfSimpleBlogPost';

  public function recent()
  {
    return $this->
      with(sfConfig::get('app_sfSimpleBlog_user_class', 'sfGuardUser'))->
      where('IsPublished', true)->
      orderBy('CreatedAt', 'desc');
  }

  public function tagged($tag)
  {
    return $this->
      join('sfSimpleBlogTag')->
      where('sfSimpleBlogTag.Tag', $tag);
  }

  public function withNbComments()
  {
    return $this->
      leftJoin('sfSimpleBlogComment c')->
      withColumn('COUNT(sf_blog_comment.id)', 'NbComments')->
      where('c.IsModerated', false)->
      groupBy('c.SfBlogPostId');
  }

  public function findByStrippedTitleAndDate($text, $date)
  {
    $this->where('StrippedTitle', $text);
    if (sfConfig::get('app_sfSimpleBlog_use_date_in_url', false))
    {
      $this->where('PublishedAt', $date);
    }

    return $this->findOne();
  }
}


For those who followed my previous posts, you probably understand that the only thing that prevents sfSimpleBlog from working with Doctrine is the advance in the implementation of sfDoctrineFinder class. Once that is finished - and I'm progressing quite fast - sfSimpleBlogPlugin will be the first true ORM agnostic plugin.

Add request method requirement to routing in symfony 1.1

Doing rails-like RESTful resources is not natively possible with symfony, but with a bit of tweaking you can get closer to it.

The problem

Thanks to routing requirements, you can restrict the cases where a route matches a URL.

show_article:
  url: /article/:id
  params: { module: article, action: show }
  requirements: { id: \d+ }

What I sometimes miss, is the ability to add a HTTP request method requirement, i.e. making a route match only if the request is GET, or POST, or DELETE. This is really useful when designing RESTful resources, where a single external URL can match more than one resource, depending on the request method:

show_article:
  url: /article/:id
  params: { module: article, action: show }
  requirements: { id: \d+, sf_method: get }

update_article:
  url: /article/:id
  params: { module: article, action: update }
  requirements: { id: \d+, sf_method: post }

delete_article:
  url: /article/:id
  params: { module: article, action: delete }
  requirements: { id: \d+, sf_method: delete }

The solution

In order to allow symfony to do that, you need to modify two symfony classes: sfWebRequest and sfPatternRouting. As both of these classes are controlled by a factory in symfony 1.1, I recommend creating two custom classes, one for the request and one for the routing. Download these two classes here, and place them under the application lib/ folder. Now all you need is to edit your factories.yml to use these classes instead of the default ones:

all:
  request:
    class: myWebRequest
  routing:
    class: myPatternRouting

Clear the cache, and now you can add method requirements to your routing rules, as explained above.

In order to create a link to a route with a requirement other than sf_method: get, you must pass the sf_method parameter to url_for() explicitly (don't worry, it will not end up in the external URL):

echo url_for('article/show?id=1') => /article/1
echo url_for('article/update?id=1') => fails
echo url_for('article/update?id=1&sf_method=post') => /article/1
echo url_for('article/delete?id=1') => fails
echo url_for('article/delete?id=1&sf_method=post') => /article/1


What's next

Note that this is just a proof-of-concept, and to be perfectly RESTful, link_to() should generate a form doing a POST request when sf_method=post is passed. Also, the routing configuration handler could be modified to transform a single RESTful rule:

article:
  restful:
    base_url: /articles
    module:   article
    identifier: id

Into a list of normal rules:

# display a list of articles
list_article:
  url: /articles
  params: { module: article, action: index }
  requirements: { sf_method: get }

# display a single article
show_article:
  url: /articles/:id
  params: { module: article, action: show }
  requirements: { id: \d+, sf_method: get }

# display an empty form for a new article
new_article:
  url: /articles/new
  params: { module: article, action: new }
  requirements: { sf_method: get }

# handle the submission of a new article form
create_article:
  url: /articles/new
  params: { module: article, action: create }
  requirements: { sf_method: post }

# display a form to edit an existing article
edit_article:
  url: /articles/:id/edit
  params: { module: article, action: edit }
  requirements: { id: \d+, sf_method: get }

update_article:
  url: /articles/:id/edit
  params: { module: article, action: update }
  requirements: { id: \d+, sf_method: post }

delete_article:
  url: /articles/:id
  params: { module: article, action: delete }
  requirements: { id: \d+, sf_method: delete }

I leave that to your sagacity.

Admin Generator compatible with Propel and Doctrine

Just a quick note to mention a recent addition I made to the sfPropelFinderPlugin. It now features an admin generator theme, identical in functionality to the Propel and Doctrine admin generators, except... It uses DbFinder queries instead of Criteria or Doctrine_Query calls. See more in the Generator README file.

This has two implications:

  • Modules based on this generator are easier to customize, especially if you need to override methods of the action class. Instead of dealing with complicated Criterion conditions, you manipulate finder objects, with all the ease of use it implies.
  • Modules based on this generator are ORM agnostic, meaning they work both with Propel and Doctrine (actually, this is not entirely true, since sfDoctrineFinder doesn't implement all the features required by the DbFinder generator yet... but it will soon be true).

It makes the writing of ORM-agnostic plugins possible, especially for plugins like sfSimpleCMSPlugin or sfSimpleBlogPlugin who feature backend modules generated by symfony.

That's decided, the next version of the plugins I maintain will use DbFinder!