Front Controller

PHP frameworks these days are pretty complex.

I wanted something simple, that is not trying to copy and rename every PHP function. That actually lets you use bare PHP, but better organized.

This is pretty much "the smallest framework for me".

I use this approach at work.

Handling URLs

I like that URLs are hierarchic. Therefore, I design my websites according to URL structure.

I also want to handle GET and POST in different ways.

First, we will have to designate a central PHP script I call the "front controller". Every request passes through it. I call it app.php.

My websites are bilingual, french and english. We start with a rewrite rule in the .htaccess file :

RewriteEngine On
RewriteRule ^((En|Fr)(/.*)?)?$ app.php/$1

That way, URLs starting with En or Fr will be handled to app.php :

<?php
	ignore_user_abort(true);
	set_time_limit(120);
	
	function appautoload($class) {
		foreach (glob(__DIR__ . '/app/{model,vue,util}/' . $class . '.php', GLOB_BRACE) as $file)
			include $file;
	}
	spl_autoload_register('appautoload');
	
	@list(, $lang, $page) = $pi = explode('/', preg_replace('@/$@', '', @$_SERVER['PATH_INFO']));
	define('LANG', $lang ?? 'Fr');
	define('PAGE', $page ?? 'Profile');
	$subpages = array_slice($pi, 3);
	
	header('Content-Type: text/html; charset=UTF-8');
	
	$class = 'Vue' . PAGE;
	if (class_exists($class))
		(new $class())->handle($_SERVER['REQUEST_METHOD'], $subpages);
	else
		(new Vue404())->handle();
?>

This scripts sets up an autoload. Create the app folder and the model, vue and util subfolders. Autoloads allow you to use any PHP class you want without having to deal with includes.

The first URL component after the En/Fr tells us about the class to load. That class is instantiated and passed the method (GET/POST) and the rest of the path.

Vue

Every vue object inherits from the abstract Vue class. Put it in app/vue/Vue.php.

<?php
	abstract class Vue {
		final function handle($method = 'GET', $subpages = array()) {
			$args = array();
			foreach ($subpages as $i => $subpage) {
				if (method_exists($this, $method . '_')) {
					$method .= '_';
					$args = array_slice($subpages, $i);
					break;
				}
				$method .= '_' . $subpage;
			}
			if (method_exists($this, $method))
				return $this->$method(...$args);
			else
				return (new Vue404())->handle();
		}
	}
?>

The handle function tries to find a member function to handle the request.

The method starts with the method (GET/POST) followed by the other URL components.

A GET request to /En/Hello/Test/One/ loads class VueHello and calls the GET_Test_One() method on it.

You can handle different paths by naming your methods :

/En/Hello/ GET()
/En/Hello/Test/ GET_Test()
/En/Hello/Test/One/ GET_Test_One()

It is also possible to handle path components as arguments to the methods.

For example, the /En/Hello/Test/One/ path can be handled with any of these methods :

The first method to match will be used and the others are ignored.

404 Not Found

The above code relies on a Vue404 class :

<?php
	class Vue404 extends Vue {
		protected function GET() {
			header('HTTP/1.1 404 Not Found');
?>
<!doctype html>
<meta content=width=device-width name=viewport />
<title>404 Not Found</title>
<h1>404 Not Found</h1>
<?php
		}
	}
?>

Save it in app/vue/Vue404.php.

Vue example

<?php
	class VueHello extends Vue {
		protected function GET() {
?>
<!doctype html>
<meta content=width=device-width name=viewport />
<title>Hello</title>
<h1>Hello</h1>
<?php
		}
		protected function GET_Test() {
?>
<!doctype html>
<meta content=width=device-width name=viewport />
<title>Hello Test</title>
<h1>Hello Test</h1>
<?php
		}
		// I like to set the arguments as optional by default and then use isset() to see if they were provided
		protected function GET_Test_($one, $two = null) {
?>
<!doctype html>
<meta content=width=device-width name=viewport />
<title>Hello Test</title>
<h1>Hello Test</h1>
<p>You have entered : <?= htmlspecialchars($one) ?></p>
<?php if (isset($two)): ?>
<p>You have also entered : <?= htmlspecialchars($two) ?></p>
<?php endif; ?>
<?php
		}
	}
?>

Generating URLs

Now that URLs are handled, it is time to generate them.

For that, I have a URL helper class (app/util/URL.php).

<?php
	final class URL {
		// Adds a slash to every URL
		private static function AddSlash(&$pages) {
			// Remove nulls
			foreach ($pages as $i => $page) {
				if (!isset($page)) {
					$pages = array_slice($pages, 0, $i);
					break;
				}
			}
			$pages[] = '';
		}
		// Adds the language
		static function Full(...$pages) {
			array_unshift($pages, LANG);
			if (is_array($last = array_pop($pages))) {
				static::AddSlash($pages);
				$pages[] = $last;
			} else {
				$pages[] = $last;
				static::AddSlash($pages);
			}
			return static::App(...$pages);
		}
		static function FullHTML(...$args) {
			return htmlspecialchars(static::Full(...$args));
		}
		static function FullJS(...$args) {
			return json_encode(static::Full(...$args));
		}
		
		// Adds the script name and query string to URLs
		static function App(...$pages) {
			$qs = '';
			if (is_array($last = array_pop($pages)))
				$qs = '?' . http_build_query($last);
			else
				$pages[] = $last;
			if ($qs === '?')
				$qs = '';
			// Remove app.php from the URL, even if renamed or already removed
			return preg_replace('@/[^/]+\.php$@', '', $_SERVER['SCRIPT_NAME']) . '/' . implode('/', array_map('rawurlencode', $pages)) . $qs;
		}
		static function AppHTML(...$args) {
			return htmlspecialchars(static::App(...$args));
		}
		static function Root() {
			return static::App('');
		}
		
		// URLs with a domain name
		static function Domain(...$pages) {
			return (@$_SERVER['HTTPS'] ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . static::Full(...$pages);
		}
		static function DomainHTML(...$args) {
			return htmlspecialchars(static::Domain(...$args));
		}
		static function DomainJS(...$args) {
			return json_encode(static::Domain(...$args));
		}
		static function AppDomain(...$pages) {
			return (@$_SERVER['HTTPS'] ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . static::App(...$pages);
		}
		
		// Changes the language in the current URL
		static function LangHTML($lang) {
			$pages = explode('/', @$_SERVER['PATH_INFO']);
			array_shift($pages);
			$pages[0] = $lang;
			if (!isset($pages[1]))
				$pages[1] = '';
			if ($_GET)
				$pages[] = $_GET;
			return static::AppHTML(...$pages);
		}
	}
?>