May 30, 2020

5 min read

Building a Minimalist MVC Framework in PHP from Scratch

Autour d’un point (cut) by František Kupka
Autour d’un point (cut) by František Kupka

Routing

Routing should be as lucid and declarative as possible. Potentially, we can have a lot of routes and if we had to write too much code the routing would easily become a mess.

// mvc/Dispatcher.phpclass Router {
// 'METHOD path' => action
private $routing = [];
}
[
'/' => function() { ...},
'POST /abc'=> function() { ... },
'/abc'=> function() { … },
'/abc/{id}'=> function($params) { … },
'GET /abc/{id}/xyz'=> function($params) { … },
'/abc/{id1}/xyz/{id2}'=> function($params) { … }
]
// mvc/Dispatcher.phpclass Router {
private $routing = [];
function addRouting($pattern, $action) {
$this->routing[$pattern] = $action;
}
}
// mvc/Dispatcher.phpclass Router {
private $routing = [];
function addRouting($pattern, $action) { … } function route($method, $path, $params) {
$path = "{$method} /{$path}";
foreach ($this->routing as $pattern => $handler) {
// route parameters as regex
$patternParams = $this->patternParams($pattern);
if (!empty($patternParams)) {
$pattern = $this->withParams($pattern);
}
// add GET into the pattern if necessary
$pattern = $this->withMethod($pattern);
// if the request matches, $params array is filled
if ($this->requestMatches(
$pattern, $path, $patternParams, $params)) {
$handler($params); // execute action
return;
}
}
http_response_code(404);
$this->route['/']([]);
}
}
// mvc/Dispatcher.phpclass Dispatcher {
private $router;
function __construct() {
$this->router = new Router();
}
function dispatch() {
$this->router->route(
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI'],
$_REQUEST);
}
}
// mvc/Dispatcher.phpclass Dispatcher {
private $router;
function __construct() { … } function dispatch() { … } function routing($pattern, $action) {
$this->router->addRouting($pattern, $action);
return $this;
}
}
// index.php(new Dispatcher())
->routing('/hello/{user}', function($params) {
echo "Hello, {$params['user']}!";
})
->dispatch();
curl http://localhost:8080/hello/user123
Hello, user123!

View and Model

The very first idea behind PHP was to have a simple language for templating. Nowadays, PHP is used for almost everything, but templating. Until now: we will create a simple HTML template in PHP, just like this:

// views/layouts/html.php<!doctype html>
<html lang="en">
<head>
<title>Minimalist MVC Framework</title>
<meta charset="utf-8">
</head>
<body>
<main>
<?= $this->content() ?>
</main>
<footer>
&copy; <?= date('Y') ?> by PHP
</footer>
</body>
</html>
// views/layouts/xml.php<?php
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
echo $this->content();
// views/hello.php<h1>Hello, <?= $this->user ?>!</h1>
<p>Make yourself at home.</p>
// mvc/ModelView.phpclass ModelView {
private $name;
private $model;
private $type;
public function __construct($name, $model, $type = 'html') {
$this->name = $name;
$this->model = $model;
$this->type = $type;
}
final function render() {
switch ($this->type) {
case 'xml':
header('Content-type: text/xml; charset=UTF-8');
break;
case 'json':
header("Content-Type: application/json; charset=UTF-8");
break;
default:
header("Content-Type: text/html; charset=UTF-8");
}
require_once "./layout/{$this->type}.php";
}
final function content() {
ob_start();
require_once "./views/{$this->name}.php";
$out = ob_get_contents();
ob_end_clean();
return $out;
}
function __get($key) {
return isset($this->model[$key])
? $this->model[$key]
: "__{$key}__";
}
}

Controller

The role of a Controller is to validate user input, call the domain logic, fill the View Model and render the View. The Controller is an abstract class meant to be extended by custom actions controllers:

// mvc/Controller.phpabstract class Controller {
private $model = [];
function render($name, $type = 'html') {
return new ModelView($name, $this->model, $type);
}
function addModelAttribute($key, $value) {
$this->model[$key] = $value;
}
}
// controllers/HelloController.phpclass HelloController extends Controller {  function sayHello($params) {
$this->addModelAttribute('user', $params['user']);
$this->render('hello');
}
}

Put It All Together

As the last, we have use our HelloController in the routing definition:

// index.php(new Dispatcher())
->routing('/hello/{user}', function($params) {
(new HelloController())->sayHello($params);
})
->dispatch();
curl http://localhost:8080/hello/user123
<!doctype html>
<html lang="en">
<head>
<title>Minimalist MVC Framework</title>
<meta charset="utf-8">
</head>
<body>
<main>
<h1>Hello, user123!</h1>
<p>Make yourself at home.</p>
</main>
<footer>
&copy; 2020 by PHP
</footer>
</body>
</html>

Conclusion

We’ve implemented a minimalist super-performant MVC framework using only vanilla PHP. The focus was simplicity, ease of use, separation of concerns.