Home

Awesome

PHP App Server

Create lightweight, installable applications written in HTML, CSS, Javascript, and PHP for the Windows, Mac, and Linux desktop operating systems.

Screenshot of a generated installer on Windows

What can be created with PHP App Server are real, installable software applications that take a fraction of the time to create when compared to traditional desktop application development. Go from idea/concept to full deployment in 1/10th the time and support every major desktop OS with just one code base.

PHP App Server Overview and Tutorial video

PHP App Server is a fully-featured and extensible web server written in PHP with custom features specially designed to be used in a traditional desktop OS environment. When the user runs the software via their Start menu, application launcher, etc., the software starts the server and then launches the user's preferred web browser to access the application. PHP powers the backend while the web browser handles all the nitty-gritty details of displaying the user interface. The ready-made installer scripts simplify the process of creating final release packages for delivery to your user's computer systems.

Donate Discord

Features

Getting Started

Download or clone the latest software release. When cloning, be sure to use a fork and create a branch for your app before beginning development. Doing so avoids accidentally overwriting your software whenever you fetch upstream updates for PHP App Server itself.

For the rest of this guide, a recent version of PHP is assumed to be installed. There are many ways to make that happen.

From a command-line, run the following to get a list of command-line options:

php server.php -?

Start a web server on port 9002 by running:

php server.php -port=9002

The directory structure of PHP App Server is as follows:

Create an index.php file in the 'www' directory:

<?php
	phpinfo();

Connect to the running server with your web browser at:

http://127.0.0.1:9002/

The output of phpinfo() is displayed in the browser and the result of the request is written to the command-line.

Change the URL to:

http://127.0.0.1:9002/api/v1/account/info

The same index.php file runs.

Rename or copy the index.php file to api.php and reload the page. Now api.php is being called. The virtual directory feature of PHP App Server is something that you might find useful as you develop your application.

Dual Document Roots

Installed software applications cannot write to 'www'. Applications are usually installed by a privileged user on the system but the person running the software will generally not have sufficient permissions to write to the 'www' directory. This is an important consideration to keep in mind while developing a software application using PHP App Server. Fortunately, there is a solution to this problem already built into the server: Dual document roots.

When PHP code is executing from the application's 'www' directory, it has access to five $_SERVER variables that are passed in by and are unique to the PHP App Server environment:

When a request is made to the web server, PHP App Server looks first for files in the application's 'www' directory. If it doesn't find a file there, it then checks for the file in the path specified by DOCUMENT_ROOT_USER.

Writing Secure Software

Writing a localhost server application that relies on a web browser can result in serious system security violations ranging from loss of data control to damaging the user's file system. As long as the application is written correctly, the web browser's policies will generally protect the user from malicious websites and users that attempt to access PHP App Server controlled content.

However, here are a few important, select security related items that all PHP App Server based software applications must actively defend against (in order of importance):

There are many other security considerations that are in the OWASP Top 10 list and the OWASP attacks list to also keep in mind, but those are the big ones.

Long-Running Processes

PHP App Server includes a powerful server extension and two SDKs to make starting, managing, and monitoring long-running processes easy and secure from both PHP and Javascript. Started processes run as the user that PHP App Server is running as but aren't limited by timeouts or memory limits like regular CGI/FastCGI requests are. Running processes can be actively monitored and even interacted with from the web browser via the included Javascript SDK.

Long-running Process Extension Overview video

Long-running scripts should ideally be stored in a 'scripts' subdirectory off the main PHP App Server 'support' directory. That way they are away from the main web root but the application can still find them via $_SERVER["PAS_ROOT"].

Here's an example of starting a PHP script called 'test.php' using the PHP SDK:

<?php
	$rootpath = str_replace("\\", "/", dirname(__FILE__));

	// Load the PHP App Server common functions.
	require_once $_SERVER["PAS_ROOT"] . "/support/process_helper.php";
	require_once $_SERVER["PAS_ROOT"] . "/support/pas_functions.php";

	$cmd = escapeshellarg(PAS_GetPHPBinary());
	$cmd .= " " . escapeshellarg(realpath($_SERVER["PAS_ROOT"] . "/support/scripts/test.php"));

	$options = array(
//		"rules" => array(
//			"start" => time() + 5,
//			"maxqueued" => 3
//		),
//		"stdin" => false,
//		"dir" => $_SERVER["PAS_ROOT"] . "/support/scripts/",
//		"env" => ProcessHelper::GetCleanEnvironment(),
//		"extraenv" => array("PASSWORD" => "supersecret"),
//		"extra" => array(
//			"title" => "Custom window title",
//			"inputmode" => "readline"
//		)
	);

	// Start the process.
	require_once $rootpath . "/support/pas_run_process_sdk.php";

	$rp = new PAS_RunProcessSDK();

	$result = $rp->StartProcess("demo", $cmd, $options);
	if (!$result["success"])  echo "An error occurred while starting a long-running process.";

	echo "Done.";
?>

Each process is given a tag, which allows multiple running processes to be grouped by tag. In the example above, the tag is called "demo". The Javacsript SDK can later be used to show only processes that use a specific tag:

<?php
	header("Content-Type: text/html; charset=UTF8");
?>
<!DOCTYPE html>
<html>
<body>
<?php
	$rootpath = str_replace("\\", "/", dirname(__FILE__));

	require_once $rootpath . "/support/pas_run_process_sdk.php";

	PAS_RunProcessSDK::OutputCSS();
	PAS_RunProcessSDK::OutputJS();
?>
<div id="terminal-manager"></div>

<script type="text/javascript">
// NOTE:  Always put Javascript RunProcesSDK and TerminalManager class instances in a Javascript closure like this one to limit the XSRF attack surface.
(function() {
	// Establish a new connection with a compatible WebSocket server.
	var runproc = new RunProcessSDK('<?=PAS_RunProcessSDK::GetURL()?>', false, '<?=PAS_RunProcessSDK::GetAuthToken()?>');

	// Debugging mode dumps incoming and outgoing packets to the web browser's debug console.
	runproc.debug = true;

	// Establish a new terminal manager instance.
	var elem = document.getElementById('terminal-manager');

	// Automatically attach to all channels with the 'demo' tag.
	var options = {
		tag: 'demo'
	};

	var tm = new TerminalManager(runproc, elem, options);
})();
</script>
</body>
</html>

The PHP SDK simplifies emitting the necessary CSS and Javscript dependencies into the HTML. The above code demonstrates setting up a WebSocket connection to the PHP App Server extension and connecting it to a TerminalManager instance to monitor for processes with the "demo" tag. TerminalManager is an included Javascript class that automatically creates and manages one or more ExecTerminals (also included) based on the input criteria. In this case, TerminalManager will automatically attach to any process created with a "demo" tag. An ExecTerminal looks like this:

A screenshot of an ExecTerminal

Each ExecTerminal wraps up a XTerm.js Terminal instance with additional features:

And more.

Note that TerminalManager and ExecTerminal are not required for managing long-running processes but they do handle quite a few common scenarios. The example code above only scratches the surface of what can be done.

Here is the full list of TerminalManager options:

The included XTerm PHP class offers seamless and simplified control over the output from a long-running script to the XTerm-compatible ExecTerminal in the browser. No need to remember ANSI escape codes. Here's an example script:

<?php
	if (!isset($_SERVER["argc"]) || !$_SERVER["argc"])
	{
		echo "This file is intended to be run from the command-line.";

		exit();
	}

	$rootpath = str_replace("\\", "/", dirname(__FILE__));

	require_once $rootpath . "/../xterm.php";

	for ($x = 0; $x < 5; $x++)
	{
		echo "Test:  " . ($x + 1) . "\n";

		sleep(1);
	}

	echo "That's boring.  Let's...";
	sleep(1);

	XTerm::SetItalic();
	echo "spice it up!\n";
	XTerm::SetItalic(false);
	sleep(1);

	$palette = XTerm::GetBlackOptimizedColorPalette();

	for ($x = 5; $x < 10; $x++)
	{
		$num = mt_rand(17, 231);
		XTerm::SetForegroundColor($palette[$num]);
		echo "Test:  " . ($x + 1) . " (Color " . $palette[$num] . ")\n";

		sleep(1);
	}
	XTerm::SetForegroundColor(false);

	XTerm::SetTitle("Changing the title...");
	usleep(250000);
	XTerm::SetTitle("Changing the title...like");
	usleep(250000);
	XTerm::SetTitle("Changing the title...like a");
	usleep(250000);
	XTerm::SetTitle("Changing the title...like a BOSS!");
	usleep(500000);

	echo "\n";
	echo "Enter some text:  ";
	$line = rtrim(fgets(STDIN));

	XTerm::SetBold();
	echo "Here's what you wrote:  " . $line . "\n\n";
	XTerm::SetBold(false);

	echo "[Switching to 'readline_secure' mode]\n\n";
	XTerm::SetCustomInputMode('readline_secure');

	echo "Enter some more text:  ";
	XTerm::SetColors(0, 0);
	$line = rtrim(fgets(STDIN));
	XTerm::SetColors(false, false);
	XTerm::SetCustomInputMode('readline');

	XTerm::SetBold();
	echo "Here's what you wrote:  " . $line . "\n\n";
	XTerm::SetBold(false);

	echo "Done.\n";
?>

Finally, if you use CubicleSoft Admin Pack or FlexForms to build your application, the PHP SDK includes native FlexForms integration (i.e. no need to write Javascript/HTML):

<?php
	// Admin Pack and FlexForms integration.
	require_once "support/pas_run_process_sdk.php";

	$contentopts = array(
		"desc" => "Showing all long-running processes with the 'demo' tag.",
		"fields" => array(
			array(
				"type" => "pas_run_process",
//				"debug" => true,
				"options" => array(
					"tag" => "demo"
				)
			)
		)
	);

	BB_GeneratePage("Process Demo", $menuopts, $contentopts);
?>

Creating Extensions

Writing an extension requires a little bit of knowledge about how PHP App Server works: Extensions are loaded early on during startup so they can get involved in the startup sequence if they need to (mostly just for security-related extensions). Once the web server has started, every web request walks through the list of extensions and asks, "Can you handle this request?" If an extension responds in the affirmative (i.e. returns true), then the rest of the request is passed off to the extension to handle.

Since extensions are run directly inline with the core server, they get a significant performance boost and can do things such as respond over WebSocket or start long-running processes that would normally be killed off after 30 seconds by the normal PHP path.

However, those benefits come with two major drawbacks. The first is that if an extension raises an uncaught exception or otherwise crashes, it takes the whole web server with it. The second is that making code changes to an extension requires restarting the web server to test the changes, which can be a bit of a hassle. In general, the normal 'www' path is sufficient for most needs and extensions are for occasional segments of specialized logic.

The included security token extension is an excellent starting point for building an extension that can properly handle requests. The security token extension is fairly short, well-commented, and works.

The server assumes that the filename is a part of the class name. Whatever the PHP file is named, the class name within has to follow suit, otherwise PHP App Server will fail to load the extension. Extension names should start with a number, which indicates the expected order in which to call the extension.

The variables available to normal PHP scripts are also available to extensions via the global $baseenv variable (e.g. $baseenv["DOCUMENT_ROOT_USER"] and $baseenv["PAS_USER_FILES"]). Please do not alter the $baseenv values as that will negatively affect the rest of the application.

Always use the ProcessHelper::StartProcess() static function when starting external, long-running processes inside an extension. The ProcessHelper class is designed to start non-blocking processes in the background across all platforms. Note that the preferred way to start long-running processes is to use the long-running processes extension.

Server Termination

For certain tasks, it is important to tell PHP App Server to exit. For example, when upgrading a PHP App Server application on Windows, PHP itself needs to be updated and therefore can't be running during the upgrade. It's also generally good behavior to exit an application not too long after the last browser tab is closed.

There are two available methods for triggering early termination of the server:

Example PHP code for the Exit App extension method:

<script type="text/javascript">
// NOTE:  Always put WebSocket class instances in a Javascript closure like this one to limit the XSRF attack surface.
(function() {
	function InitExitApp()
	{
		var ws = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/exit-app/');

		ws.addEventListener('open', function(e) {
			var msg = {
				authtoken: '<?=hash_hmac("sha256", "/exit-app/", $_SERVER["PAS_SECRET"])?>',
				delay: 3
			};

			ws.send(JSON.stringify(msg));
		});

		ws.addEventListener('close', function(e) {
			setTimeout(InitExitApp, 500);
		});
	}

	InitExitApp();
})();
</script>

Example PHP code for the header method:

<?php
	// User clicked an "Exit application" link or something.
	header("X-Exit-App: 3");
?>

The extension is a more reliable method of detecting that all browser tabs to the application have been closed. However, if the application is unable to support the extension for some reason, then use the header method instead. The header method is best used on pages where it makes sense (e.g. a page with upgrade information).

Pre-Installer Tasks

Before running the various scripts that generate installer packages, various files need to be created, renamed, and/or modified. Every file that starts with "yourapp" needs to be renamed to your application name, preferably restricted to all lowercase a-z and hyphens. This needs to be done so that updates to the software don't accidentally overwrite your work and so that any nosy users poking around the directory structure see the application's actual name instead of "yourapp".

The 'yourapp.phpapp' file is a PHP file that performs the actual application startup sequence of starting the web server (server.php) and then launching the user's web browser. There is an $options array in the file that should be modified for your application's needs:

The last three options are intended for highly specialized scenarios. Changing 'host' to something like "127.0.1.1" might be okay but don't use "0.0.0.0" or "::0", which binds the server publicly to the network interface. Binding to a specific 'port' number might seem like a good idea until users start complaining about error messages when they try to restart the application.

The 'quitdelay' option is interesting. The server portion of PHP App Server will stick around until 'quitdelay' minutes after the last client disconnects. The application should send a "heartbeat" request every five minutes to guarantee that the web server won't terminate itself before the user is finished using the application.

Installer Packaging

Each platform packaging tool has its own instructions:

There are some known packaging issues:

The installers and the server software have some interesting tales behind them. Maybe I'll share those stories one day. For now, enjoy building your next application in PHP App Server!