Code snippets for symfony 1.x

Navigation

Auto-detect format (from URL and/or Acceptable content types)

As of Symfony 1.1, using alternative formats has become simple. However, automatic use of these formats is now disabled by default.

In the URL

Add support for sf_format in your routing factory :

/apps/myapp/config/factories.yml

all:
  routing:
    ...
    param:
      ...
      suffix: .:sf_format
 

This way, each time you go to http://mysite/module.format, the corresponding format will be detected and used.

Following URLs will automatically work and be rendered as XML : - http://mysite/module.xml # renders indexSuccess.xml.php - http://mysite/module/action.xml # renders actionSuccess.xml.php - http://mysite/module/action/param/value.xml # equals http://mysite/module/action.xml?param=value

If you want the suffix not to be mandatory (e.g. http://mysite/module/action == http://mysite/module/action.html) you'll have to add "sf_format: html" in you routing rules parameters. This is required if you want your old calls to link_to() and url_for() (who don't specifies sf_format parameter) still work.

/apps/myapp/config/routing.yml

default:
  url:   /:module/:action/*
 

myTemplate.php

link_to('hey !', 'module/action') // Produces an error !
link_to('hey !', 'module/action?sf_format=html') // Works, but hey, you'll have to add this to *all* your links :/
 

Add the default format :

/apps/myapp/config/routing.yml

default:
  url:   /:module/:action/*
  param: { sf_format: html }
 

myTemplate.php

link_to('hey !', 'module/action') // Works :)
link_to('hey !', 'module/action?sf_format=xml') // Works too, of course :)
 

Using the Accept HTTP header

We can use a filter, or plug to an even in the ProjectConfiguration class. My choice was a filter.

/apps/myapp/config/filters.yml

# insert your own filters here
detect_format:
  class: DetectFormatFilter
 

/lib/DetectFormatFilter.php

class DetectFormatFilter extends sfFilter
{
 
  public $default_formats = array(
    'html' => array('text/html', 'application/xhtml+xml'),
  );
 
  public function execute($filterChain)
  {
    $request = $this->context->getRequest();
 
    // Only if format is not already defined
    if (!$request->getRequestFormat()) {
 
      // Complete with the filter default formats
      foreach ($this->default_formats as $format => $mimeTypes) {
        foreach ($mimeTypes as $mimeType) {
          if (!$request->getFormat($mimeType)) {
            $request->setFormat($format, $mimeType);
          }
        }
      }
 
      // Detect format depending on acceptable contant types : first is prefered
      foreach ($request->getAcceptableContentTypes() as $mimeType) {
        if ($format = $request->getFormat($mimeType)) {
          $request->setRequestFormat($format);
          break;
        }
      }
 
    }
 
    // execute next filter
    $filterChain->execute();
  }
 
}
 

Take a look at the fact we automatically add "html" to the formats, attached to the two standard mime types. This could be done by adding the corresponding options to the request part of factories.yml, but I wanted this filter to be "ready-to-use" without touching other parts of configuration. "text/html" must be handled apart because as "html" is the default format, Symfony just does not handle it. So when we browse "Accept" headers, we will pass on "text/html", and just skip it because it's not configured. Without adding this part, "html" format would never been rendered, quite annoying isn't it ;)

With this configuration, you can just add the "Accept" header to say you want an XML content, and your ".xml.php" templates will then be rendered.

An interesting evolution of this filter would be to allow it to test if the format can be rendered, and if not, fall back to a default one.

Conclusion

It's quite easy to enable back auto-detection of the format. However, as Fabien says in the ticket 4920, Accept header is sometimes unreliable, and some badly configured browsers could make you think they prefer XML, which will not be the case of the user behind this browser ;)

That said, even if the URL suffix method is probably the best choice, this auto-detection method works and could be useful in some projects...

by Nicolas Chambrier on 2009-03-21, tagged accept  filter  format  routing 

Comments on this snippet

gravatar icon
#1 freakx0 on 2009-06-09 at 05:51

I tried your snipped, but i get an error through the routs of the sfguardplugin.

[Tue Jun 09 17:41:31 2009] [error] [client 127.0.0.1] The "/request_password.:sf_format" route has some missing mandatory parameters (:sf_format).

I don't know how to solve that?!

Anybody an idea?

gravatar icon
#2 Alban Browaeys on 2010-09-02 at 04:38

Here is an implementation of the use of the HTTP Accept header. It takes into account he qvalue, the level of priority , a bug in html browser (/) and if a template exists for the accept mimetype to choose the used format.

<pre> /* * Evolution of http://snippets.symfony-project.org/snippet/328 * from Nicolas Chambrier * Improvments based on 'Ajax Patterns and Best Practices' * by Christian Gross */

class CompareMimeTypes {

public function CalculateValue($value)
{
    if ($pos = strpos($value, &#039;;&#039;)) {
        $q     = (float) trim(substr($value, strpos($value, &#039;=&#039;) + 1));
        $value = substr($value, 0, $pos);
    } else {
        $q = 1;
    }
 
    $level = 0;
    if ($value == &quot;*/*&quot;) {
        $level = 1;
    } else if (preg_match(&#039;@/\*@&#039;, $value)) {
        $level = 2;
    } else if ($value == &quot;application/xhtml+xml&quot;) {
        $level = 4;
    } else {
        $level = 3;
    }
 
    return array($q, $level);
}
 
 
 
public function Compare($a, $b)
{
    list($qvalue_a, $level_a) = $this-&gt;CalculateValue($a);
    list($qvalue_b, $level_b) = $this-&gt;CalculateValue($b);
 
    if ($level_a &lt; $level_b) {
        return 1;
    } else if ($level_a &gt; $level_b) {
        return -1;
    } else {
        if ($qvalue_a &lt; $qvalue_b) {
            return 1;
        } else if ($qvalue_a &gt; $qvalue_b) {
            return -1;
        } else {
            return 1; // FIXME: hack to keep original order, might not be reliable if the elements are not sorted in sequence
            //return 0;
        }
    }
 
}
 

}

class DetectFormatByHTTPFilter extends sfFilter { public $default_formats = array( 'html' => array('text/html', 'application/xhtml+xml') );

public function templateExists()
{
 
 
    $moduleName = $this-&gt;context-&gt;getActionStack()-&gt;getLastEntry()-&gt;getModuleName();
    $actionName = $this-&gt;context-&gt;getActionStack()-&gt;getLastEntry()-&gt;getActionName();
 
 
    //We need to fetch the module&#039;s configuration to know which View class to use,
    // then we&#039;ll have access to information such as the extension
    $config = sfConfig::get(&#039;mod_&#039;.strtolower($moduleName).&#039;_view_class&#039;);
    if( empty($config) )
    {
        require($this-&gt;context-&gt;getConfigCache()-&gt;checkConfig(&#039;modules/&#039;.$moduleName.&#039;/config/module.yml&#039;, true));
        $config = sfConfig::get(&#039;mod_&#039;.strtolower($moduleName).&#039;_view_class&#039;,&#039;sf&#039;);
    }
    $class = $config.&#039;View&#039;;
    // FIXME : I do not know how to retrieve the view name so I hardcoded the Success one
    $view = new $class($this-&gt;context, $moduleName, $actionName, sfView::SUCCESS);
    $templateName = $view-&gt;getTemplate();
    if (!file_exists($this-&gt;context-&gt;getConfiguration()-&gt;getTemplateDir($moduleName, $templateName).DIRECTORY_SEPARATOR.$templateName)) return false;
 
    return true;
}
 
 
public function execute($filterChain)
{
    $request = $this-&gt;context-&gt;getRequest();
    // Only if format is not already defined
    if (!$request-&gt;getRequestFormat()) {
 
        // Complete with the filter default formats
        foreach ($this-&gt;default_formats as $format =&gt; $mimeTypes) {
            $request-&gt;setFormat($format, $mimeTypes);
        }
 
 
        // Detect format depending on acceptable contant types : first is prefered
        $header = $request-&gt;getHttpHeader(&#039;Accept&#039;);
        $values = array_filter(explode(&#039;,&#039;, $header));
        usort($values, array(new CompareMimeTypes(), &quot;Compare&quot;));
        foreach ($values as $value) {
            if (!$pos = strpos($value, &#039;;&#039;)) $pos = strlen($value);
            $mimeType = substr($value, 0, $pos);
            if ($mimeType == &#039;*/*&#039;) $mimeType = &#039;text/html&#039;; // in most case only html browser are bogus enough to send */*
            if ($format = $request-&gt;getFormat($mimeType)) {
                $request-&gt;setRequestFormat($format);
                if ($this-&gt;templateExists()) break;
            }
        }
    }
 
    // execute next filter
    $filterChain-&gt;execute();
}
 

}

</pre>