Using nginx rewrite's to remove the file extension and still work with PHP-FastCGI

I had been debating trying out nginx for a while, and at the end of last week a project I work on had it's web server go down. I decided this was a good time as any to play with nginx when getting the new box up.

One of the rules we had on the old Apache setup however, was that we re-wrote URL's to remove their .php extension. We achieved this thru a simple rule using mod_rewrite. The rule was:

RewriteCond /var/www/site/%{REQUEST_URI}.php -f
RewriteRule ^/(.+)$ /$1.php [L]

So wanting to re-implement this in nginx, I did a bit of reading of he nginx wiki, and while the examples on the rewrite module are limited, there was enough information to point out that the following would do what we wanted:

rewrite ^/([a-z]+)$ /$1.php last;

However, this rule doesn't play nicely with PHP-FastCGI.

I read a lot of articles, and most of these suggested that the best way to work around this, was to make all requests go to an intermediary PHP script, and have it handle all page requests. This seems a bit silly, and doesn't seem to solve the problem, but work around it.

So to get this to work, we had to look at how PHP-FastCGI works. The default setup for PHP-FastCGI looks something like this:

location ~ \.php$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass  127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /var/www/site$fastcgi_script_name;
}

So what this does, is when a page matches the .php extension, it will send it off to FastCGI for processing, and the second last line calculates the full path to the PHP script to run.

Now the problem with my rule is that $fastcgi_script_name is never going to contain the php script name, as because we are rewriting the URL, $fastcgi_script_name doesn't seem to update correctly.

Now, nginx does provide you with the set command in the config file, to set a variable, however, it appears you can't set all variables with it (probably a wise thing), but because we just use it in a string, we can just use another variable we have control over.

But we can't just do a rewrite and set the variable, we have to an if block.

After a bit of playing, this is what I came up with. It does the trick, and works as expected for us, but your mileage may vary.

location / {
    set $page_to_view "/index.php";
    try_files $uri $uri/ @rewrites;
    root   /var/www/site;
    index  index.php index.html index.htm;
}

location ~ \.php$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass  127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /var/www/site$page_to_view;
}

# rewrites
location @rewrites {
    if ($uri ~* ^/([a-z]+)$) {
        set $page_to_view "/$1.php";
        rewrite ^/([a-z]+)$ /$1.php last;
    }
}

This works by setting a new variable $page_to_view, and setting the default variable to "/index.php" (i.e. the page to view, if the request is for the root folder, e.g., http://example.nullis.net/).

Then when we get a page that means our rewrite condition, we set it to the page name we expect, and then this is used when setting the FastCGI script name. This means when they go to http://example.nullis.net/info, it actually gives you info.php, without having a proxy PHP page, or multiple rewrite rules.

Given I couldn't find a good example elsewhere, I figured it would be worth documenting this for other people. Let me know if you have a better way to go about this, as I'm not sure this is the best way, but it's definitely better.

If you do have suggestions, then please contact me.

Published: 15 May 2011 # — Tags: nginx, php, www