The PHP logo, by Colin Viebrock, licensed under CC BY-SA 4.0

Webserver in PHP

3 minutes to read, 691 words
Categories: web
Keywords: php scripting shell web

Disclaimer: This is only for development, nothing for production.

When writing some content for a website, it is always nice to be able to test changes locally before publishing them.

While it is easy to install nginx or Apache on a GNU/Linux distribution, there are a lot of flags and settings.

If PHP is part of the stack used for managing the website, one can avoid using those frameworks and use the integrated PHP webserver directly from the shell:

php -t <root directory> -S localhost:1313

While the program is running, it is possible to point a browser to localhost:1313.

If one would like to test the website from another device, like a phone, tablet, or other devices with a browser, one can simply use instead of localhost (and eventually check the firewall settings):

php -t <root directory> -S localhost:1313

And while the program is running, open `IP-of-your-pc:1313`on the browser of your device (the PC should be, of course, be reachable, thus devices need to be at least in the same network)

Custom 404 page

Contrary to most servers, when pointed to a non-existent page without extension (for example localhost:1313/non/existent), PHP will look for files like index.html and index.php in the given directory (thus localhost:1313/non/existent/index.html), and if not found one directory up, until it reaches the root directory.

This behavior is generally annoying, as it does not return a 404 error for a not existing page. When using a tool like linkchecker to ensure that at least all internal URLs are correct, this is problematic.

Notice that this does not happen if the URLs have a file extension, for example with localhost:1313/non/existent/file.html PHP will return immediately a generated 404 page.

I prefer URLs that do not have file extensions because the technology used to serve the content should be an implementation detail.

A link like, or let’s transpire how the page is created.

While it’s true that is not necessarily created or served by PHP, it

  • does not add any value for the end-user (does not matter if the extension is correct or not)

  • might make development more difficult (if one wants the extension to be correct, without breaking old URLs)

Also just looks cleaner, why should .html (or .php, …​ ) matter when navigating a website?

I’m not aware of an option for disabling this inconsistent behavior between URLs with and without the file extension. The desired behavior can be implemented by writing the routing functionality, and at that point it is also easy to use a customized 404 page:


if(!defined('STDERR')) define('STDERR', fopen('php://stderr', 'wb'));

// FIXME: check file is not oustide root director
function file_search(string $file_) : string {
    $file = rawurldecode($file_);
    if (is_file($file)){
        return $file; // file exists nothing else to do
    if (!str_ends_with($file, '/')) {
        $file = $file.'/';
    foreach (['index.html', 'index.php'] as $indexFile){
        $index = $file.$indexFile;
        if( is_file($index) ){
            return $index;
    return $file_; // nothing found return original value

$file = file_search($file_orig);

    // automatically parses file (html, php, ...)
    return false;
} else {
    // set 404 header and show customized page (no redirect)
    Header("HTTP/1.1 404 Not Found");
    include $_SERVER["DOCUMENT_ROOT"]."/404.html";
    return true;
Note 📝
if PHP8 is not avaiable, one should reimplement str_ends_with.


As this server is only for development purposes, it is also recommended to turn on all warnings and errors in a custom php.ini file

default_charset = "UTF-8"

error_reporting = E_ALL
display_errors = stderr
display_startup_errors = On

zend.assertions = 1 = On
assert.exception = On
assert.warning = On
assert.bail = On
assert.quiet_eval = Off

This file can be used with the --php-ini <filename> parameter, for example, like

php --php-ini php.ini -t <root directory> -S localhost:1313 router.php

Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me here.