Archive for the 'php' Category

Database Session Handling and Garbage Collector

Database Session Handling and Garbage Collector

Note for self: When using a distributed session storage with symfony (sfMySQLSessionStorage, sfMemcacheSessionStorage, sfPDOSessionStorage, etc.), don’t forget to check that session.gc_probability must not be set to 0 in the php.ini, and that the line is not commented, which seems to be the case on some PHP distributions.

; Define the probability that the 'garbage collection' process is started
; on every session initialization.
; The probability is calculated by using gc_probability/gc_divisor,
; e.g. 1/100 means there is a 1% chance that the GC process starts
; on each request.

session.gc_probability = 1
session.gc_divisor     = 100

If set to 0, the session garbage collector will never run, and sessions will pile up forever in your storage facility - eventually inflating the storage space way too much for decent performance.

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…

Rails getting inspiration from symfony

Rails 2.0 improvements on fixtures look a lot like the standard symfony 1.0 fixtures features - apart from the many-to-many syntax. Take a look at this screencast in Railscast to see how revolutionary fixtures are in the latest Rails release.

I like looking at Rails, Django, Zend Framework and other good open-source projects to take inspiration. It doesn’t prevent me from thinking on my own, but it also prevents me from reinventing the wheel. It is nice to see that the Rails developers do the same and don’t keep on thinking that they invented the Best Thing Around.

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.

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.

Define “Framework”

Many people wonder what a framework is. Many people work with people who work with a framework, and yet these people just pretend to know what this is about.

The problem is, not only the concept of framework is not immediately understandable, but also that most of the explanations of the term “Framework” suck.

How they define it

Take the most famous framework, Ruby on Rails. They don’t bother to define what a framework is. People coming to Rails either already know what this is all about, or think that this is something important because Rails is famous.

The Django framework proposes this definition:

A high-level Web framework is software that eases the pain of building dynamic Web sites. It abstracts common problems of Web development and provides shortcuts for frequent programming tasks.

They complement this definition with a page-long list of the Django features. That’s the same approach as Rails: Understand what our framework does, you will understand what a framework is in general. Kind of biaised.

Wikipedia tries to be more specific and says:

A software framework is a reusable design for a software system (or subsystem). This is expressed as a set of abstract classes and the way their instances collaborate for a specific type of software.

That’s too many words. 9 out of 10 IT managers jumped to the .Net platform before reading the second sentence (and therefore misunderstood the true nature of a framework). Besides, it doesn’t explain what a framework does for you.

Let’s see how symfony defines frameworks:

A framework streamlines application development by automating many of the patterns employed for a given purpose. A framework also adds structure to the code, prompting the developer to write better, more readable, and more maintainable code. Ultimately, a framework makes programming easier, since it packages complex operations into simple statements.

Although this sentence was written by me, it is not much better than the former ones.

Framework newbies are still wondering whether a framework is good, if they can use it to go shopping, if it enhances the sexual activity, and if it is something convenient to talk about during lunch.

Enter geeks

One of the reasons why the concept of framework is not that easy to make clear is that it’s a geek matter. Frameworks are used by programmers who develop enough applications to become unsatisfied with a language alone.

These people are interested in stuff like design patterns, test-driven development, object-relational mapping, and other dirty words. They just don't speak the same language as you do. And try to get an astrophysicist talk clearly about Strings Theory. Very few people know enough about a subject to be able to explain it, and unfortunately these people often don’t know enough people who don’t know anything about this subject, so they are not used to making it accessible.

Let’s try

Now let’s try it ourselves. The idea is to define the term “Framework” in a single sentence, and to check that anybody with minimum knowledge of web applications can understand it.

“If god had a framework, he would build the world in 3 days.”

All right, marketing boy, this may make your girlfriend laugh, but it’s not going to help us a lot.

“Frameworks are a supplementary layer on top of a programming language that make development faster and more productive”.

So this is something interesting for programmers, but what if I am a manager with three alternatives for a new website, built with either the ezPublish CMS, or the symfony framework, or the BEA Weblogic application server?

if($framework)
{
  Just::do($it);
}
else
{
  $it = strstr(fibonacci(75 * _SERVER['foo'])), strbin2hex('1101'));
  sort($it);
  if($it instanceof Crap)
  {
    thrown new CrapException('bummer!');
  }
  else
  {
    try_again();
  }
}


Hey spaghetti code man, I guess you learnt PHP development by hacking Wordpress and Gallery, didn’t you? Thanks, but you're not helping.

I can’t do this. Can you?

I guess I failed. Defining the word “Framework” in a single and simple sentence is just too hard for me tonight.

My only hope is that, as often in the web 2.0, the true value of this post will come from its comments, rather than from its body. Please help me: How would you define “Framework”?

The future of symfony routing

If you follow the symfony timeline, you probably saw quite a few changesets dedicated to the routing functionality lately. Fabien has been refactoring the routing to make it a normal object - not a singleton - in the trunk; and I have been working on expanding the route syntax in the routing branch.

If you look closely, it means that routing will be more powerful, easier to tweak, and faster.

With the new routing system, routes can use any separator between tokens. For instance, you can have such rules that were not possible before:

permalink:
  url: /:day-:month-:year/:title
  params: { module: content, action: permalink }

multiformat_list:
  url: /articles/list.:format
  params: { module: article, action: list, format: html }

The route syntax was originally a port of the Ruby in Rails routing system. That explains why tokens are prefixed by a colon (:), but that doesn't really make sense in PHP. That's why the new routing system now accepts tokens prefixed by a dollar sign ($), which will look more familiar to PHP developers:

permalink:
  url: /$day-$month-$year/$title
  params: { module: content, action: permalink }

multiformat_list:
  url: /articles/list.$format
  params: { module: article, action: list, format: html }

A new default rule was added to the routing.yml, and together with a hook in the controller, it allows for built-in support of PJS templates. That's right, the ability to include dynamically generated JavaScript files, available in symfony 1.0 with sfPJSPlugin, will be a native functionality of symfony 1.1. This implementation allows for caching of generated JavaScript files with the regular caching system.

<?php echo pjs_include_tag('foo/bar?id=12') ?>
// will generate in HTML
<script language="JavaScript" type="text/javascript" src="/js/foo/bar/id/12.pjs"></script>

There is more to come, but the limits are already pushed much further than where they used to be.

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 three Ajax requests to update three parts of a page, the web server will just hate you (servers have feelings, you know). But there can be cases where Ajax can take some burden off the server, where it can be an architecture choice rather than a pure UI choice.

Does it sound familiar?

For instance, take the now classic "add a comment" Ajax form. The user enters data in a form, submits it, and the result is sent to the client in XmlHttpRequest. There is an immediate benefit for the server here: It only has to send back the updated part of the page (in that case, the new comment) rather than the entire page. That represents a notable bandwidth and CPU economy.

Super caching

Another example is an "almost static page", which means that the page contents depend on the user session only for some limited parts. Think of a news website where the only session-dependent part is the name of the connected user displayed on the upper part of the window. If this element wasn't present, the page would be a perfect candidate for super caching.

The super caching is the action to store a copy of the HTML response somewhere under the web root of the server, so that next time the page is requested, the server sends the HTML response without even using PHP. This is very fast, and it can even be done by a lightweight and specialized server like lighthttpd. Symfony has a super caching solution in the form of a plugin, it is called sfSuperCachePlugin.

Ajax comes to the rescue

But, because of the session-dependent element, the page I talk about cannot benefit from the super caching. Can't it, really? What if the session dependent element was removed, and added to the static page afterwards, by the web browser? That's where Ajax comes in. It is a great replacement for iframes, because the Ajax response can be any JavaScript code, used to do some complex DOM modification, and that is more powerful than just replacing an element's innerHTML.

Concretely, that's how you would design your pages to take advantage of the super cache:

  • The page is designed without session dependent element

  • The first time the page is requested, it is stored in the super cache

  • The page contains a static call to another action in Ajax.

For instance, if you use the jQuery Javascript framework, the end of the page can show something like:

<script>
$().ready(function () {
  $.getScript('/path/to/javascript/action');
});
</script>


The /path/to/javascript/action action gets the user's name from the session and database, and sends it back to the browser as a piece of JavaScript modifying the DOM of the static page to include the user's name.

But wait a minute. Modifying a page after it is loaded with JavaScript, isn't that just what unobtrusive behaviours do? That's true, the sfUJSPlugin is designed exactly with this process in mind. Build the static, session-independent, accessible version first, and add the dynamic, session-dependent, highly interactive sugar in JavaScript afterwards. Or, to put it differently, design fast pages first, add the performance penalty afterwards. There is no more limit to the number of pages you can put in cache - even the most session-dependent pages can benefit from super cache.

Pros and cons

The performance advantage is not huge in symfony, because the real cost of a request is the framework initialization. Whether you send one page with cached fragments or two pages with only one using symfony, you will always have to initialize symfony once. But using the solution described here will at least save you the time of deserialization of a complex response from the cache, and a better logic in your design.

One drawback of these techniques is that the load taken off the server ends up being transferred to the client. The web browser has more to do, and the full response to a request takes more exchanges with the server to display - in short, the answer is somehow slower for the end user. Besides, developers tend to forget accessibility when they code Ajax interactions, so the pages have to be though carefully.

Conclusion

To conclude, Ajax can make your website faster because it allows you to use super caching in pages that normally couldn't benefit from it. Symfony has already all the tools to put this idea into practice (namely sfSuperCachePlugin and sfUJSPlugin), so you should never have to buy a new server again.

Using the configuration cache in a symfony plugin

In a plugin that I recently published, the sfSimpleBlogPlugin, I used a new feature of the symfony framework that allows dynamic registration of a configuration handler. Why is this interesting? Because it allows plugin authors to use custom YAML files for the configuration of their plugin, and to take advantage of the configuration cache system to parse this YAML file only once in production.

Let's take an example. Your FooBar plugin can be controlled by a FooBar.yml file located in the application's config/ folder. This YAML file contains parameters for the plugin and is environment dependent:

all:
  lorem: ipsum

dev:
  lorem: dolor

What would be interesting would be to be able to read these parameters from the code, using the sfConfig registry. That's quite simple. In the plugin's config.php (which is executed during each request), write the following code:

$foobar_config_file = sfConfig::get('sf_app_config_dir_name').'/FooBar.yml';
sfConfigCache::getInstance()->registerConfigHandler($foobar_config_file, 'sfDefineEnvironmentConfigConfigHandler', array (
  'prefix' => 'foobar_',
));
include(sfConfigCache::getInstance()->checkConfig($foobar_config_file));


Now, to get the value of the lorem parameter from everywhere in the code, just type:

$lorem = sfConfig::get('foobar_lorem');


The very interest about using a config handler rather than parsing the YAML file directly is that the sfConfigCache::checkConfig() method will process the file and put the processed PHP code in cache. Performancewise, this is the good way to have a custom configuration file controlling a plugin.

As the sfConfigCache::registerConfigHandler() method is only available as of symfony r3703, this will only work with the latest trunk version.

Build your own feed aggregator with symfony

With the help of the sfFeed2 plugin and the sfWebBrowser plugin, symfony makes the creation of a feed aggregator a breeze. Let's see what it would take to create the core of a Google Reader-like.

Fetching feeds

First of all, you'll have to fetch feeds from the Internet. It is strongly recommended to browse feeds in an asynchronous way, i.e. not when the user requests the page showing the aggregated feeds. There are two obvious reasons why you wouldn't want a synchronous process:

  • Distant servers providing the feeds that you want to fetch would receive one request per request on your server. That's a nasty trick to play to other service providers, and it can corrupt the distant server's statistics.

  • If you have to fetch a dozen URLs per request, then the response time might exceed the server timeout.

So you have to fetch feeds, store them somewhere (in your filesystem or in a database), and keep them for later. I choose to store them in the disk, which gives me an occasion to use the sfFileCache class. Here is the code that I write in a batch process:

<?php

define('SF_ROOT_DIR',    realpath(dirname(__file__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG',       true);

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

// Put the URLs of the feeds you want to fetch in an array
$urls = array(
  'http://api.flickr.com/services/feeds/photos_public.gne?format=rss',
  'http://del.icio.us/rss/popular',
  'http://feeds.feedburner.com/TechCrunch',
  'http://www.symfony-project.com/weblog/rss'
);

// Fetch the feeds
$feeds = array();
foreach($urls as $url)
{
  try
  {
    $feeds[] = sfFeedPeer::createFromWeb($url);
    echo "fetched feed ".$url."\n";
  }
  catch(Exception $e)
  {
    echo "error fetching feed ".$url.": ".$e."\n";
  }
}

// Aggregate the feeds
$aggregated_feeds = sfFeedPeer::aggregate($feeds, array('limit' => 10));

// Cache the results
$f = new sfFileCache(sfConfig::get('sf_data_dir').'/feed');
$f->set('feeds', '', serialize($aggregated_feeds));


The interesting part of the batch is the use of the sfFeed2 plugin classes, made simple by the sfFeedPeer utility methods:

  • sfFeedPeer::createFromWeb() takes an URL as parameter, makes a request to this URL, decodes the response and populates a sfFeed object accordingly. It relies on the sfWebBrowser plugin for the HTTP request. It can recognize feeds of various formats (Atom1, RSS0.92, RSS1, RSS2).

  • sfFeedPeer::aggregate() takes an array of sfFeed objects and returns a single feed, in which all feed items are aggregated and ordered chronologically. The second parameter is an array of options, that I use here to limit the number of items present in the resulting feed.

Then I serialize the sfFeed object containing the aggregated items and store it in the disk (under the data/ directory, to make it environment-independent) using the sfFileCache class.

I execute the batch once to test it and to generate the first version of the data/feed/feeds.cache file; as it needs to run periodically, I also add the following command to my crontab:

30 1 * * * cd /path/to/my/project && php batch/fetch_feeds.php

Displaying a feed

That's it for the first part. Now, what happens when a user makes a request to my application for the page showing the aggregated feeds? If this action is called feed/show, it can look like:

public function executeShow()
{
  $f = new sfFileCache(sfConfig::get('sf_data_dir').'/feed');
  $this->feed = unserialize($f->get('feeds', '', true));
}


The last thing I'll do is to display the details of each item, in feed/templates/showSuccess.php:

<?php echo use_helper('Text', 'Date') ?>
<?php foreach($feed->getItems() as $item): ?>
<div class="post">
  <h2><?php echo link_to(truncate_text(strip_tags($item->getTitle()), 40), $item->getLink()) ?></h2>
    Posted on <?php echo format_date($item->getPubDate(), "EEEE d MMMM 'at' h:ma ") ?>
    by <?php echo link_to($item->getFeed()->getTitle(), $item->getFeed()->getLink()) ?>
  <div class="summary"><?php echo truncate_text($item->getDescription(), 300) ?></div>
</div>
<?php endforeach; ?>


That's where I'm glad that the sfFeed and sfFeedItem classes provided by the sfFeed2 plugin have the same accessors whatever the format of the feed (Atom/Rss/etc). It makes the display of a feed item details very simple.

If you want to see the result, check the "outside" columns of the symfony community page.

« Previous PageNext Page »