Routing with PHP's built in server


PHP has a built in development server, which is very handy during development. However, I had some difficulties to get it working correctly. After a lot of debugging, and reading bug reports and the php documentation I learned some things:

  • It can serve static files
  • It has some uncommon rules how to set some $_SERVER variables when the url path contains a dot. (The reason for this is probably that a dot in the url path indicates that the resource is a file rather than a dynamically generated document). And this is not documented very clearly!

If you're setup is not working but you don't have time to read the article, try my gist for a quick example.

Document root and router file

Besides the address to listen on, there are two options you can provide to the server:

  1. the document root (-t option)
  2. the router script (the last argument on the command line)

Example:

php -t public -S 127.0.0.1:8080 router.php

The document root is the base to serve static files. If no router script is provided, it is also the starting point for php to look for a index.php file. Intuitively, I'd say that in this case the two following commands do the same thing:

# a)
php -t public 127.0.0.1:8080

# b)
php -r public 127.0.0.1:8080 public/index.php

This is not the case. In a), if you request a "directory" url such as /some/path or /some/path/, then it falls back to an index.html or index.php in that directory. If such an index file is not found, it goes up one level and looks in the parent directory for an index file.

If you request a "file" url -- and this is the case if the url path contains a dot (!) -- then php will serve exactly that file if it exists. If it doesn't exist, it returns with 404 Not Found.

It case b), your router script (public/index.php) will be called before anything else. In this case, YOU can decide if the url is a file or not. If you return false, php will look for a file as before. However, there are still some rather strange things (IMHO) you will encounter if the url path contains a dot.

If php thinks your url is a file...

I've started a server with a router script (just like in b) above), which just dumps the $_SERVER array. Interestingly, some variables differ, depending on whether or not the url path contains a dot:

$ curl 127.0.0.1:8080/products.json > /tmp/withdot
$ curl -# 127.0.0.1:8080/products > /tmp/nodot
$ diff -u /tmp/nodot /tmp/withdot
--- /tmp/nodot  2016-04-16 23:27:10.172478506 +0200
+++ /tmp/withdot  2016-04-16 23:26:46.037368239 +0200
@@ -2,20 +2,19 @@
 (
     [DOCUMENT_ROOT] => /tmp/php/public
-    [REQUEST_URI] => /products
+    [REQUEST_URI] => /products.json
     [REQUEST_METHOD] => GET
-    [SCRIPT_NAME] => /index.php
-    [SCRIPT_FILENAME] => /tmp/php/public/index.php
-    [PATH_INFO] => /products
-    [PHP_SELF] => /index.php/products
+    [SCRIPT_NAME] => /products.json
+    [SCRIPT_FILENAME] => /tmp/php/public/public/index.php
+    [PHP_SELF] => /products.json
 )

I've removed the not so interesting values from the diff. But what we see is:

(The server was started with php -t public -S 127.0.0.1:8080 public/index.php)

  1. SCRIPT_NAME is set to /index.php only in the /products url. For /products.json url, it is set to exactly the same value as REQUEST_URI.
  2. PATH_INFO is completely missing when requesting /products.json.
  3. PHP_SELF contains both, the script name and the path for the /products url: /index.php/products, but not so for /products.json.
  4. SCRIPT_FILENAME doubles my document root dir "public" for the url /products.json. No idea why...

It looks like PHP has already decided that the requested URL has to be a file, and set the script name to the url path. In my opinion this is counter intuitive if we explicitly provide a router file.

As this values are important for any routing component, having the right settings is important.

The solution (for a "common" project layout)

Modern PHP web frameworks / applications normally have a layout similar to this one:

project/
├── public          <-- the document root
│   └── index.php   <-- the entry point for the app
├── src
└── test

The (production) web server is then configured to "rewrite" all requests which don't match an actual file to the index.php file. So our goal is to use the development server in a similar way.

The solution consists of the following two points:

  • always provide a router script when starting the server
  • in the router script, reset / set the values SCRIPT_NAME, PATH_INFO, and PHP_SELF, maybe also SCRIPT_FILENAME.

I prefer to do this in a if block, only if we run in cli-server mode:

// public/index.php

if (php_sapi_name() === 'cli-server') {
    if (is_file($_SERVER['DOCUMENT_ROOT'].'/'.$_SERVER['SCRIPT_NAME'])) {
        // probably a static file...
        return false;
    }
    $_SERVER['SCRIPT_NAME'] = '/index.php';
    // if needed, set PATH_INFO based on other values
}

If you start the server from the document root, and the router script is also located in this directory, then the SCRIPT_FILENAME value is correct. If not, you may also want to set this value.

Another, maybe cleaner solution would be to write a route script, which is only used for the dev server, and not in production environment. With that solution, your normal entry script would not need to be updated only to fix routing in development environments.

You can find this solution as a gist on github.

Author: Claudio Kressibucher
Tags: PHP, Web-Applikationen, Server