JasonDaly.name

PHP, Ruby, Symfony, Rails, Doctrine, MooTools. Web Development.

Posts tagged with "symfony"

August 17, 2010

Caching Static Assets with Cache-Busting Support in Symfony 1.4

(Note: This technique can be applied anywhere. This article is specifically related to a Symfony 1.4 implementation)

As recommended in the Google Page Speed best practices, static resources should be cached locally in the browser. This means setting a far-future expiration date on filetypes such as .css and .js content. This first step is achieved rather simply by adding something like below to the application’s .htaccess file or virtualhost’s httpd.conf include.

<IfModule mod_expires.c>
  Header set cache-control: public
  ExpiresActive on
  ExpiresDefault                          "access plus 1 month"

 # Other ExpiresBy... declarations here...

 # css and javascript
  ExpiresByType text/css                  "access plus 1 month"
  ExpiresByType application/javascript    "access plus 1 month"
  ExpiresByType text/javascript           "access plus 1 month"
</IfModule>

FileETag None

Note the last line above removes ETags, since according to Yahoo ETags are not needed for static content with far-future expiries set1.

The changes above will cause CSS and Javascript files to be cached locally in the browser for 1 month from the first access time, causing future requests to be made without checking against the server for newer versions of these static resources. Though the caching desired is now in place, it’s not perfect since if these static resources are modified in any way, the cached local browser version of these files will not be invalidated.

Invalidating the Cache

Currently in the <head> of my application, default stylesheets are added and then all included stylesheets (including those added by the specific action being requested) have their <link ... /> tags constructed and appended to the DOM using the following code

  use_stylesheet('main.css');
  use_stylesheet('handheld.css', '', array('media' => 'handheld'));

  include_autoversioned('stylesheets');

The include_autoversioned() function is where the work is done which invalidates the browsers’ cache. This function is a wrapper of Symfony’s include_stylesheets() and include_javascripts(). I have added a Template.php file to my application’s lib/helpers/ directory which contains the function as shown below.

/**
 * Depending on the type of include requested (stylesheets or javascripts), for
 * each file already added to the response object, for each file, i.e. /css/base.css,
 * replaces it with a string containing the file's mtime, i.e. /css/base.1221534296.css.
 *  
 * @see http://stackoverflow.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files
 *  
 * @param $file  The file to be loaded.  Must be an absolute path (i.e.
 *               starting with slash)
 *               
 * @throws InvalidArgumentException when invalid type is requested
 *                
 * @return void (outputs string response directly)               
 */
function include_autoversioned($type = 'stylesheets'){
  if (!in_array(strtolower($type), array('stylesheets', 'javascripts'))) {
    throw new InvalidArgumentException(sprintf("\$type can only be 'stylesheets' or 'javascripts': '%s' was passed", $type));
  }

  $function = sprintf('get_%s', $type);
  $code = $function();
  unset($function);

  $code = preg_replace_callback('/(href|src)\=\"([^\"]+)\"/', function($matches){    
    if (strpos($matches[2], '/') !== 0 || !file_exists(sfConfig::get('sf_web_dir') . DIRECTORY_SEPARATOR . $matches[2])) {
      $path = $matches[2];
    } else {
      $mtime = filemtime(sfConfig::get('sf_web_dir') . DIRECTORY_SEPARATOR . $matches[2]);
      $path = preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $matches[2]);
    }

    return str_replace($matches[2], $path, $matches[0]);
  }, $code);

  echo $code;
}

(Note: The above code requires >= PHP5.3 due to the use of an anonymous function)

Also, again to the application’s .htaccess file or virtualhost’s httpd.conf include, the following must be added above the other RewriteRules that are in place for Symfony applications by default2.

# Cache-busing js and css files
RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]

To show what this does, let’s look at the <link ... /> tags generated using Symfony’s get_stylesheets in the <head>

<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" />
<link rel="stylesheet" type="text/css" media="handheld" href="/css/handheld.css" />

and compare this to the output generated by the new include_autoversioned('stylesheets')

<link rel="stylesheet" type="text/css" media="screen" href="/css/main.1282090705.css" />
<link rel="stylesheet" type="text/css" media="handheld" href="/css/handheld.1282076131.css" />

By modifying the filenames of these static resources by injecting a unix timestamp for the last-modified time of each file, whenever a change is made to one of these files, however small, it will cause the filename of that resource to change, effectively invalidating the local browser’s copy of that file. This guarantees that a browser will maintain a copy of a static resource for as long as can be reasonably expected, while always guaranteeing they immediately receive the latest changes to those resources as soon as they’re published.


  1. The .htaccess section was in part taken from http://github.com/paulirish/html5-boilerplate 

  2. Though modified considerably, the cache invalidation was inspired by this stackoverflow answer 

Tags: cache code php symfony symfony 1.4 tips php5.3

July 9, 2010

Symfony Functional Tests + PHP APC Cache Slam Warning

After implementing query caching using Doctrine’s Doctrine_Cache_Apc interface in a Symfony application I am working on, when running the application’s functional tests, warnings were returned intermittently in a few places

$ [apc-warning] Potential cache slam averted for key ...

In APC >= 3.0, an apc.slam_defense configuration option was added in attempt to avoid repeated writes to the same APC cache key as might occur under very high traffic. This apc.slam_defense option was later removed due to having been deprecated in favor of apc.write_lock as of APC >= 3.0.11. This is enabled by default, so the first thing I tried in my development environment was disabling this option. A simple test case will still fail though with apc.write_lock disabled.

apc_store('my_key', 1);
apc_store('my_key', 2);

echo apc_fetch('my_key'); // outputs 1, not 2

One of the more recent comments in this PECL ticket for APC offers a patch that can be applied to the latest release of APC before compiling it. This patch re-introduces the previously removed apc.slam_defense option. For my development environment only (since the cache slam warnings thrown in this environment due to my functional tests are irrelevant), I fixed the cache slam warnings by following these steps

  1. Download a fresh copy of the latest release of APC (3.1.3p1)
  2. Apply this patch
  3. Build and install APC
  4. In php.ini, add the newly recognized apc.slam_defense=0

With apc.slam_defense in place and disabled, the test case above and my application’s functional tests run without any cache slam warnings.

2 notes Tags: php symfony code apc cache caching doctrine doctrine_cache

June 16, 2010

Symfony + sfController::getPresentationFor() + sfWebRequest::getRequestFormat()

There may be times when part or all of a text/plaintext version of an action’s template needs to be rendered for use from within another action who’s display format is set as text/html. For example, if there is a text/plaintext version of an order receipt that appears within an application, and that same version of the order receipt needs to be mailed to a user from within a separate action, we need to tell the order receipt to render in text/plaintext otherwise it will attempt to render in the format of the current action; in this case text/html. This is assuming the email is being sent as text/plaintext itself and the rendering action’s format is text/html.

Symfony offers a sfWebRequest::getPresentationFor() method which provides the functionality to render an action’s view from within another action, even if the calling action resides in a separate module. The trick is specifying the format for the other action while maintaining the requested format for the current action. The getPresentationFor() method signature is limiting in it’s support for specifying a format, so the only option appears to be as follows.

// In the current action
$format = sfContext::getInstance()->getRequest()->getRequestFormat();
sfContext::getInstance()->getRequest()->setRequestFormat('txt');
$otherActionContent = sfContext::getInstance()->getController()->getPresentationFor('moduleName', 'otherActionName');
sfContext::getInstance()->getRequest()->setRequestFormat($format);
unset($format);

The above code

  1. Temporarily retrieves the current action’s request format
  2. Sets the request format to the format required for our other action to render in
  3. Retrieves the properly rendered contents of the other action
  4. Resets the request format for the current action
  5. Unsets the temporary format storage

Tags: php symfony getPresentationFor sfwebrequest tips code

June 7, 2010

Symfony’s format_date(), l10n Support, and Custom Formatting

Symfony comes with some great i18n and l10n support. The DateHelper comes with functionality for templates to provide locale-specific date formats. There are a bunch of default formats that can be easily used as

format_date(strtotime('now'), 'P', 'en_US'); // Outputs 'Monday 7 June 2010'
format_date(strtotime('now'), 'P', 'fr_FR'); // Outputs 'Lundi 7 Juin 2010'

Some of the pre-defined patterns are constructed using special token patterns. These tokens can be passed to the 2nd parameter of the format_date() function above to generate custom formatted locale-specific date/time stamps.

format_date(strtotime('now'), 'EEE, MMMM dd, yyyy hh:mm a'); // Outputs 'Mon, June 07, 2010 12:40AM'

It doesn’t appear detailed information is available in any of the main documentation books or in the source code regarding the tokens available for date/time formatting, but I did come across a wiki page in the symfony trac.

1 note Tags: php symfony tips format_date l10n code

March 3, 2010

Symfony 1.4 + Doctrine 1.2 - Transactions

Hoping there is a simpler way that I am missing, here is how to begin a new transaction with Doctrine 1.2 through Symfony 1.4. It was the call to sfDoctrineDatabase::getDoctrineConnection() which I had to lookup in the docs1.

$connection = sfContext::getInstance()->getDatabaseManager()->getDatabase('default')->getDoctrineConnection();  
$connection->beginTransaction();    
try {
  // Do work for the transaction ...
  $connection->commit();
} catch (Exception $e) {
  $connection->rollback();
  // Handle exception ...
}                  

1 note Tags: symfony symfony 1.4 doctrine doctrine 1.2 transaction doctrine_connection

November 15, 2009

sfLoader::loadHelpers is now Deprecated (Symfony 1.3)

As of Symfony 1.3, the loadHelpers() method in the sfLoader class is deprecated.

The sfLoader::loadHelpers() method is deprecated. Please use the same method from sfApplicationConfiguration.

Though it only took 3 minutes to find the proper way to call the sfApplicationConfiguration instance, I thought I’d post it here for others.

sfContext::getInstance()->getConfiguration()->loadHelpers('Template');

Tags: deprecated 1.3 loadhelpers php symfony sfloader sfcontext