Archive for the 'symfony' Category

Finding “Augmented” Propel Objects

Retrieving objects with Propel is easy with sfPropelFinderPlugin. And, since the addition of the with() method, you can hydrate related objects in a single query with no pain.

But there is still a use case where the sfPropelFinder can't help you, and this is when you need to add one or two columns from a related objects but not the whole object.

For instance, imagine that you have an Article and a Comment object, the Comment being related to the Article in the schema by a article_id column.

Until now, if you wanted to retrieve some comments and the related article title with no additiona query, you had to either get a resultset and parse it by hand, or hydrate both the Article and the Comment object:

$comment = sfPropelFinder::from('Comment')->
  findOne();
echo $comment->getArticle()->getTitle(); // requires an additional db query
$comment = sfPropelFinder::from('Comment')->
  with('Article')->
  findOne();
echo $comment->getArticle()->getTitle(); // no additional db query, but a whole object is hydrated just for one column


Today, the sfPropelFinder just made it simpler. You can add a single column from a related object with the withColumn() finder method, and retrieve it afterwards on any Propel object with the getColumn() method:

$comment = sfPropelFinder::from('Comment')->
  withColumn('Article_Title')->
  findOne();
echo $comment->getColumn('Article_Title'); // no additional db query, and only one more column fetched


Internally, the finder will look for a relation between the Comment and the Article classes in the TableMap, and add the related join in its internal Criteria. It will call the Criteria's addAsColumn() method and deal with the augmented resultset so that the additional column is available in the Comment objects.

This new method accepts calculated columns through raw SQL code, and alias names. That means that you can add a column and reuse it in an orderBy() clause:

$articles = sfPropelFinder::from('Article')->
  join('Comment')->
  withColumn('COUNT(comment.ID)', 'NbComments')->
  orderBy('NbComments')->
  find();


Not only will the resulting Article objects be ordered by number of comment, but each individual object will have access to its number of comments through getColumn(). All that with a single SQL query and a very short syntax.

So the number of situations where you need to get a resultset instead of an array of Propel objects has reduced again. the sfPropelFinder takes care of all the dirty job for you, and lets you focus on the design of your business model rather than the details. If you didn't test it already, it's time to get the HEAD SVN revision of sfPropelFinderPlugin and give it a try.

Analyze your data

If you have been running a website for some time, you probably have a lot of data waiting to be analyzed. I'm not talking about server logs - you should already have a tool to follow these. But the data from your users, recorded in the database, this is very valuable data that you can learn from.

If you included a created_at or an updated_at column in your tables, then you're almost done. All you have to do to analyze the data is to query your database. Then you can see how many forum messages were posted day by day since the last month. Or the progression of your demo application usage. The ideal would be to display the data in a nice line chart, and to provide navigation and filtering controls to easily browse your statistics.

You have the data, all you need is a tool to visualize it. This tool is now available as a symfony plugin. I have the pleasure to release today a plugin called sfStatsPlugin, which does exactly that. Based on a simple YAML configuration file, the plugin will take advantage of the methods from your Propel Object Model to display a nice line chart from Google Chart API or a jQuery flot. It is fully i18ned, compatible with symfony 1.0 and 1.1, and released under the MIT License, thanks to my company's sponsoring.

sfStatsPlugin

If you have some data waiting to be analyzed, download the plugin and try it right away. In minutes, you will understand how your site usage is evolving, with all the precision allowed by your object model structure.

Browsing your assets with style

I have the pleasure to introduce a new plugin called sfAssetsLibraryPlugin. See it as a media library on steroids.

State of the art

Many symfony projects use the sfMediaLibraryPlugin in their backend application. Until today, it was the only publicly available plugin for managing images and other assets in a symfony application via a web interface.

But this plugin had limited abilities in terms of file and folder management. You could rename a file, but that's about all. Worse, it didn't allow you to add metadata to your assets. Managing a large image library without ever dealing with copyright or legend is almost impossible.

Lastly, every CMS-like application needs a robust asset management utility, hooked into rich text editors, to allow writers to deal with images without pain. I saw numerous tweaked versions of the sfMediaLibrary made for that purpose, all reinventing the wheel of the 'advanced media library'.

One step forward

Today, my company releases a new media library plugin called sfAssetsLibraryPlugin. It is a complete rewrite of the initial media library plugin. It introduces a database layer in addition to the asset files stored under your web root, adds a ton of new file management features, and was built with customization in mind.

Add files on-the-fly in the directory your're browsing, rename files and folders, move entire folders and their content alltogether, find assets by filename, upload date, author, description or copyright, integrate tightly with TinyMCE... That's only some of the features of the plugin.

Oh, and if you have an existing sfMediaLibrary, the new plugin will import your assets as soon as you call the bundled synchronize pake task.

Of course, you can change its look and feel (below is a screenshot of the plugin integrated in our backend) and add your own metadata columns with sfPropelAlternativeSchemaPlugin. Our implementation adds tagging abilities to assets, and it takes only a couple more lines in the application code, without ever touching to the plugin code. We made sure the plugin was very clean and easy to extend.

customized edit view

The plugin has currently been used in my company for several months, and even if tagged as beta, you can download it right away. Of course, it has I18n, unit tests, PHPDoc and a text documentation - you should consider these as the standards of symfony plugins. The sfAssetsLibraryPlugin is released under the MIT license, for your sharing pleasure.

Who did it

The plugin is the work of William Garcia, Gabriele Santini and myself, and it took us quite some time to finalize the plugin so it is truly customizable and powerful. We will keep on maintaining the plugin in the future, so expect more features in the upcoming months (drag and drop files to move them, for instance).

I want to express my warm thanks to both William and Gabriele for their work. I am very proud of the result, and I believe it is a huge step forward in the 'build your application with plugin legos' direction.

Of course, feedback is welcome. If you find a bug, please file a ticket in the symfony Trac with sfAssetsLibraryPlugin as component. And if you want to volunteer to enhance the plugin, file a ticket, too, and just wait for our green light to start working on a patch.

No one is irreplaceable

I'm officially retiring from the symfony project core team and documentation. My recent rant about the shift in symfony philosophy lead Fabien and I to agree that we disagree. The symfony project will have to find new documentation writers.

I've been working on the symfony project for three years now, and it's been a great adventure. Seeing what was originally a very specialized piece of work done by the head of a French Web Agency become one of the top web application frameworks in the whole world has just been amazing. Betting on Open-Source, dedicating time and money to develop and document an internal tool was a great move for Sensio. I learned a lot, thanks to Fabien and Sensio. I enjoyed writing the Askeet tutorial, the symfony book and all the other tutorials very much. I enjoyed writing plug-ins and applications, just for the pleasure to donate them. Now that Big Players recommend symfony for large-scale applications, a bright future opens for all the symfony developers out there.

I've also been battling for three years. I kept trying to put my two cents in the symfony development choices, even though I'm not a developer. I kept advocating simplicity of use over sophistication and code purity. I kept urging for smaller, more often releases. I kept giving the documentation writer's point of view, which is that if something is hard to explain, it is probably also hard to use. I kept asking for more methods, so that there could be More Than One Way To Do Things - one simple, and one powerful. To a certain point, some of my remarks have been taken into consideration, but it required a tremendous amount of energy and stubbornness. And even now, I'm reminded that these changes (like using YAML instead of XML for schemas for instance) were mistakes.

Today, my opinions are clearly not the ones of the "symfony core team". Important design choices are not discussed with the community, just like when symfony was only developed internally. 95% of the code base is still the result of a single man's work and decisions. The community is just there to provide support, do the beta testing and play with plugins. This is a big strength of the project (no dispersion, no time lost in sterile discussions), but it's also frustrating when somebody wants to get more involved.

I left Sensio last year partly because I couldn't get my views accepted. I'm leaving symfony today for the same reason. I regret it, because I would love to continue collaborating to the project, because I really like writing documentation, and because I love the open-source spirit and the symfony community. But the time I spend on symfony is now on my free time, and I can't just do what others decide. A hobby must be fun, not frustrating. I'm tired. Three years is long enough.

I also regret to leave a half-finished symfony 1.1 guidebook. But I heard that Sensio is writing a whole book about the new Form system (!), and I'm confident that they can dedicate internal resources to writing documentation. If you're interested in contributing, you should get in touch with Fabien - he's now the documentation lead (in addition to the rest).

To conclude, I'd like to quote a French guy named Talleyrand. He said: "War is much too serious a thing to be left to military men". I personally think that Programmatic API is much too serious a thing to be left to developers.

Is symfony 1.1 too verbose?

Among the remarks I have about symfony 1.1, the most recurring one is the shift of philosophy between the 1.0 and 1.1 syntax. If symfony 1.0 syntax was made to write code fast, I believe it is not the case anymore with symfony 1.1, which is designed primarily for extensibility.

The result is that a symfony 1.1 application looks a lot more like a Java program. I tend to agree that Object-Orientation is a good thing because it forces you to organize your code in a modular way. But when object-orientation makes you need to keep a symfony book aside at all times and multiply the number of LOC by two, I think it's a dead end.

Disclaimer: I'm not a developer by training. I don't have much experience in web development. I started learning PHP about three years ago, and I don't think that code purity is something you should stick to. I believe in code smell, though, but that's another matter. My main interest is usability in general.

Example #1 - Tasks

Let's see how the new symfony philosophy applies to the new command line utility framework: the symfony tasks. This is a very powerful addition to symfony 1.1, made to allow an easy parsing of CLI arguments and options, and to write CLI scripts in an extensible way.

Here is a typical task initialization method, taken from sfLogRotateTask (for details about the syntax, please refer to the symfony 1.1 documentation):

protected function configure()
{
  $this->addArguments(array(
    new sfCommandArgument('application', sfCommandArgument::REQUIRED, 'The application name'),
    new sfCommandArgument('env', sfCommandArgument::REQUIRED, 'The environment name'),
  ));

  $this->addOptions(array(
    new sfCommandOption('history', null, sfCommandOption::PARAMETER_REQUIRED, 'The maximum number of old log files to keep', 10),
    new sfCommandOption('period', null, sfCommandOption::PARAMETER_REQUIRED, 'The period in days', 7),
  ));

  ...
}


To be able to write a task by yourself, you need to know what an sfCommandArgument is, what an sfCommandOption is, and you need to know their constants as well. That means that the number of things to learn is quite important. And why do we need to instantiate a new object to set arguments, since the only thing that addArguments() accepts is an array of sfCommandArgument objects? Because, sometime, you will take advantage of the fact that you can pass a subclass of sfCommandArgument to extend the task framework.

Developed according to the symfony 1.0 style, this would probably give:

protected function configure()
{
  $this->addArgument('application', true, 'The application name');
  $this->addArgument('env', true, 'The environment name');

  $this->addOption('history', null, true, 'The maximum number of old log files to keep', 10);
  $this->addOption('period', null, true, 'The period in days', 7);

  ...
}


Do you see the difference? The latter is maybe not as easy to extend, but for 90% of the cases, faster to write. Besides, the addArgument() method does not prevent you from using the long version with addArguments(), so you get both extensibility and brevity.

Note: By reading the above code, you don't know what the null and true parameters stand for. It is a matter of taste, and longer to write, but I'd rather have my method calls look like the following when the method takes many arguments. That way, every method call in your application is a useful reminder of the API.

protected function configure()
{
  $this->addArgument('application', $required = true, $help = 'The application name');
  $this->addArgument('env', $required = true, $help = 'The environment name');

  $this->addOption('history', $shortcut = null, $required = true, $help = 'The maximum number of old log files to keep', $default = 10);
  $this->addOption('period', $shortcut = null, $required = true, $help = 'The period in days', $default = 7);

  ...
}


Fabien added an addArgument() method to sfTask not so long ago, but it just seemed useless to him to duplicate the method signature. I think he did that just to make me stop yelling. You, developer, should learn The Right Way of Doing Things, and the right way is the longer way. If you can't understand or learn the longer way, bummer, you can't use symfony.

Example #2 - Events

The event system is another great addition to symfony 1.1. It extends the principle of mixin to allow more runtime class modifications. Once again, the symfony book is up to date on this part, so you should read the related chapter if you need to understand the syntax.

So here is how to allow a class to be extensible by way of a generic __call() in symfony 1.1:

public function __call($method, $arguments)
{
  $event = $this->dispatcher->notifyUntil(new sfEvent($this, 'foo.method_not_found', array(
    'method'    => $method,
    'arguments' => $arguments
  )));
  if (!$event->isProcessed())
  {
    throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
  }

  return $event->getReturnValue();
}


And here is how you register a new bar() method with this system:

$dispatcher = sfContext::getInstance()->getEventDispatcher();
$dispatcher->connect('foo.method_not_found', array('myClass', 'listenToFooMethodNotFound'));

class myClass
{
  public function listenToFooMethodNotFound(sfEvent $event)
  {
    $parameters = $event->getParameters();
    switch($method = $parameters['method'])
    {
      case 'bar':
        $this->bar($event, $parameters['arguments'])
        return true;
      default:
        return false;
    }
  }

  protected static function bar($event, $arguments = array())
  {
    // ...

    $event->setReturnValue($result);
  }
}


As I wrote the documentation for this feature, I couldn't refrain from thinking this could be a lot easier. With symfony 1.0 philosophy, I guess you would only need to write:

public function __call($method, $arguments)
{
  return $this->dispatcher->handle($this, 'my.class.event.name', $method, $arguments);
}


And you would probably register a new method this way:

$dispatcher = sfContext::getInstance()->getEventDispatcher();
$dispatcher->addMethod('foo.method_not_found', array('myClass', 'bar'));

class myClass
{
  public function bar($object, $arguments = array())
  {
    // ...

    return $result;
  }
}


You can guess how the handle() and addMethod() methods of sfEventDispatcher could do all the dirty job for you. That would save you a copy/paste from the first version of the code, would be less error prone, and after all, if the program has less lines, then it's cheaper to maintain.

But, according to Fabien, that would be hardcoding the way you throw an exception in __call(), and this addition would only be useful in a very minority of cases. While the fact that you pass to notify() an sfEvent instance instead of a simple list of parameters is, once again, based on the idea that you will sometime find it convenient to subclass sfEvent. Which I think will never happen before symfony 1.2 at least.

Example #3 - Forms

Don't get me started on forms. I already wrote a post about sfForms to say that it missed an important part, and that code usability had been left aside. I still think so. Fabien doesn't.

Oh, by the way, do you know that the First Project Tutorial in symfony 1.1 showcases the new Form framework? If that doesn't discourage newcomers, I think nothing will. I wish good luck to the one who will try to make this tutorial newbie-friendly.

Conclusion

Symfony 1.1 adds a ton of wonderful features to symfony 1.0. From a technical point of view, this is a more complete, more coherent, more beautiful piece of code than symfony 1.0 was. It kicks ass and I believe that it's more powerful than any other PHP framework out there.

On the other hand, I think that symfony 1.1 is more verbose, and therefore more error-prone, than symfony 1.0. It forces you to constantly look at the book for syntax description. The book cannot describe everything - it is just a guide on how to use the framework. It should remain as small as possible to keep readable. The book cannot go into the details of sfCommandArgument::REQUIRED or sfWidgetFormSchema. That means the book will not be enough to use symfony 1.1: You will need to know the full API doc as well, even for the basic uses. Fabien knows the API by heart, so he doesn't need any documentation. But you, my friend, you will sweat a lot to learn all the changes.

The heart of the problem is that, to use symfony 1.1, you don't just need to learn the usage guidelines, you also need to understand how it works inside.

I disagree with the philosophy shift symfony is taking. I think it should stick with the "KISS" motto, and it could do so without sacrificing extensibility. If the price to pay is a little more code in the symfony base, then symfony should pay this price - not the symfony users.

I'm tired of writing a book that looks more and more like an API documentation. I don't feel like explaining the core symfony classes to developers who just wants to use the framework to build an application. I think that The Definitve Guide To Symfony doesn't really make any sense anymore. You want to know how to use the framework? Well, read the API doc and guess. After all, that's what all the other open-source projects do.

Maybe I'm the only one thinking that. But, as I said at the beginning, I'm no developer.

Twitter Said To Drop Rails: So What?

Twitter is said to rewrite parts of their application with something else than RoR to solve their performance/availability issues. Is this good news? Is this a reason for Code Ignitor fans and Django preachers to just celebrate? Should the symfony community see this as a wonderful opportunity?

Not at all. There will be two major consequences, both bad for all frameworks out there. Firstly, IT managers will move away from open-source web application frameworks, because from their point of view, if the most well-known open-source web application framework can't handle the traffic, no other can. Secondly, the frameworks communities look dumber than ever, all fighting with zealot blindness and non-professional arguments. I don't see any enterprise decision maker pick a tool if this tools' support relies on a bunch of guys who just think Their Way Is The Best Way No Doubt About That.

Developers around the world really need to realize that there must not be only one solution. This is called a monopoly. Having many frameworks out there leaves more room for innovation, doesn't concentrate the future of web developement in too few hands. Besides, with millions of web sites out there, the cake is big enough for everybody to get a large piece. If there was only one way to build web apps - say symfony, for instance - then developers would be trapped. And for the cases where symfony would not be the best tool (I can see plenty), there would not be any alternative.

Doubt if the fuel of faith. There are good ideas to grab in every project. You shouldn't rely on the will of a single framework architect for your whole business model. And eventually, success is contagious. So please show your happyness when other frameworks get positive reviews, not the opposite.

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.

A small symfony for a fast response

Sometimes, the price of a request when dealing with a symfony application can be overwhelming. But instead of getting back to spaghetti PHP, maybe you can get a handful of symfony features for a share of its initialization time.

The features without the cost

I met this case when designing a feature-rich Content Management System that made a heavy use of the cache - and of the Super Cache. Basically, symfony was able to compute very complex pages and serve them as static page, that means very fast.

The Super Cache was a very efficient performance enhancer, for a small cost. Imagine that you set the lifetime of the super cache to 10 seconds; when a server is under a heavy load, a given page is only calculated once every 10 seconds, even if requested 500 times in between. With these figures, activating the Super Cache roughly multiplies your site's responsiveness by 500.

Design with speed in mind

But "with great power comes great responsibility", or so they say. To be able to use page caching with layout, and therefore the Super Cache plugin, the application had to be designed very carefully.

The most forbidden thing when you want to use page cache with layout are parts of the page that depend on the session - think about a header saying "Hello, John Doe" when you are connected, even if the rest o the page is completely session-independent. Unfortunately, that's always what the client wants on every page. So I had to find a solution to make the page cacheable without losing the basic user customization.

Ajax to the rescue

As explained in a previous post, you can always defer the customization of the page to the next request, by serving a session-independent page that calls an Ajax action in the background to retrieve the data necessary to change the user name in the header.

But symfony does not really fit for the second request. The cost of the initialization of a symfony request is not to neglect, and counts for 90% of the response time in very small requests - such as getting a username from the database based on a key.

Bitter swift symfony

That's when the idea of a "small symfony" comes. Wouldn't it be great if you could get access to the model layer, the configuration, the autoloading, the user object, the helpers, and keep a MVC separation, without initializing the whole framework?

That would indeed give a boost to any application designed according to the principle exposed in the quoted article. But wait a minute, we already know of "lightweight actions". They are called "components" in symfony. The only problem is that they cannot be called from the outside.

Inside out

Can't they? I'm not so sure. Imagine a script lying under your web root folder, a "lightweight front controller", with the following code:

define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'prod');
define('SF_DEBUG',       false);

require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');

$module = $_GET['module'];
unset($_GET['module']);
$action = $_GET['action'];
unset($_GET['action']);
sfLoader::loadHelpers('Partial');
include_component($module, $action, $_GET);


It looks very much like a regular symfony front controller script, except that it lacks the final sfController::dispatch() line. And indeed, it does not initialize the filter chain, handle validation nor output escaping. It just initializes the smallest part of symfony required to execute a "lightweight action"

I saved this script under web/component.php. Now, my Ajax calls can be made to the following URI:

http://mysite/component.php?module=foo&action=bar&key1=value1

The server will then return the result of the execution of an

include_component('foo', 'bar', array(
'key1' =&gt; 'value1'
));


Does it work?

Now I can execute a component from the client side. The component architecture offers native View/Controller separation, and the configuration initialization brings autoloading, database access, and more. It does work perfectly, but is it fast? Speed tests show that not launching the filter chain saves about 40% to 50% of the cost of a symfony initalization. This means that you can multiply the number of requests that your server can handle by two - for very simple requests.

Be aware that this trick can only be used in some very particular cases, and only for very light requests. It may jeopardize security, and will often prove to be very limited. But for a page split between a session-independent part and a small session-dependent action, it does the trick.

Before we leave

The great thing about this article is that the trick it exposes is not its best part. For the original need of including session-dependent data into a generated CMS page, Ajax is not the only solution. JavaScript alone can do all the job on the client side, and so your server will never need to embed user data in the page. Ok, it will need to do it once, after which the client keeps the data in a cookie, and a JavaScript executed at page load inserts this data into the page - on the client side. And now every CMS page can be cacheable with its layout.

« Previous PageNext Page »