I've seen at least two recent PHP questions that would do well from a Model-View-Controller ('MVC' from here-on-out in this question) setup. Now, me, being the horrible person I am, I wanted to build one because it's been a while since I've done any actual PHP work, and as much trash as I have been known to talk about it it's still a very popular and, to be fair, very easy language to use.
So, I've built an MVC structure in it. It's very basic, but it does exactly what an MVC application should. Separation of view from code.
It all starts with the .htaccess
file, this file basically allows the use of /Controller/Action/Id?QueryString
structured URL's, so I feel it's a good starting place:
RewriteEngine on
# Rewrite /Controller as /Controller/Index
RewriteRule ^([a-zA-Z0-9]+)/?$ index.php?Controller=$1&Action=Index [L,QSA]
# Rerwite /Controller/Action(/Id?Querystring)
RewriteRule ^([a-zA-Z0-9]+)/([a-zA-Z0-9]+)/?(/([a-zA-Z0-9]+)/?)?$ index.php?Controller=$1&Action=$2&Id=$4 [L,QSA]
This is pretty self-explanatory, and it should make sense.
The next step is directing the user to index.php
. This is where magic starts happening, and this is where we start making actual MVC decisions:
<?php
function LoadDirectory($dir) {
foreach (scandir($dir) as $file) {
if ($file !== "." && $file !== "..") {
require_once("{$dir}/{$file}");
}
}
}
LoadDirectory("Include");
LoadDirectory("Controllers");
LoadDirectory("Models");
$controllerName = $_GET["Controller"];
$action = $_GET["Action"];
$id = "";
if (isset($_GET["Id"])) {
$id = $_GET["Id"];
}
// Forced 'Controller' name suffix, configured in Constants
$controllerClassName = $controllerName . "Controller";
if (Constants::REQUIRE_CONTROLLER_SUFFIX === FALSE
&& class_exists($controllerClassName) === FALSE) {
$controllerClassName = $controllerName;
}
// If this one fails then we could not find a valid Controller for our
// request, this means we have an error and must quit.
// TODO: Pretty this up a bit.
if (class_exists($controllerClassName) === FALSE) {
die("The controller '{$controllerName}' does not exist.");
}
$controller = new $controllerClassName();
// TODO: Pretty this up a bit.
if (method_exists($controller, $action) === FALSE) {
die("The controller '{$controllerClassName}' does not have an action '{$action}'.");
}
$requestType = $_SERVER['REQUEST_METHOD'];
if ($requestType === "POST") {
$methodModelMethod = $action . "Model";
$methodModelName = $controller->$methodModelMethod();
$methodModel = new $methodModelName();
foreach (get_object_vars($methodModel) as $var => $value) {
$methodModel->$var = $_POST[$var];
}
echo $controller->$action($methodModel);
} else if ($requestType === "GET") {
echo $controller->$action($id);
} else {
die("HTTP request type '{$requestType}' is unsupported.");
}
?>
Nothing here needs to be modified to use the MVC system, this is the landing so that index.php
can take all the work and send it away.
This next controller is an example, because I want to demonstrate how easy this MVC system is to use:
<?php class HomeController extends BaseController { private $items = array("Cat", "Dog", "Rat", "Fox", "Horse"); public function Name() { return "Home"; } public function Index() { $model = new HomeIndexViewModel(); $model->Title = "Test"; $model->Message = "Some awesome message"; $model->Items = $this->items; return $this->View("Index", $model); } public function Detail($id) { $item = $this->items[$id]; $model = new HomeIndexViewModel(); $model->Title = "View Item"; $model->Message = "Viewing an item"; $model->Items = array($item); return $this->View("Index", $model); } public function UpdateModel() { return "UpdateModel"; } public function Update($postModel) { $item = $this->items[$postModel->Id]; $model = new HomeIndexViewModel(); $model->Title = "Updated Item" . $postModel->Id; $model->Message = "Updated an item"; $model->Items = array($item); return $this->View("Index", $model); } } ?>
Of course, this begs the question, what is BaseController
? Well, that's where the View
magic happens:
<?php
abstract class BaseController {
abstract public function Name();
public function View($action, $model) {
$controllerName = $this->Name();
$view = file_get_contents("Views/{$controllerName}/{$action}." . Constants::VIEW_EXTENSION);
$view = $this->PopulateView($view, $model);
$layoutModel = new LayoutModel();
$layoutModel->Title = $model->Title;
$layoutModel->Controller = $controllerName;
$layoutModel->Action = $action;
$layoutModel->View = $view;
$layout = file_get_contents("Views/" . Constants::LAYOUT_VIEW . "." . Constants::VIEW_EXTENSION);
$finalHtml = $this->PopulateView($layout, $layoutModel);
return $finalHtml;
}
public function PopulateView($view, $model) {
$start = strpos($view, "<?php");
while ($start !== FALSE) {
$end = strpos($view, "?>", $start + 6);
$phpCode = substr($view, $start + 6, $end - $start - 6);
$result = CleanEval::RunCode($phpCode, $model);
$view = substr_replace($view, $result, $start, $end - $start + 2);
$start = strpos($view, "<?php", $start + 6);
}
return $view;
}
}
?>
Well this introduces our CleanEval
class, and Constants
really stands out here. Let's look at both of those:
The CleanEval
class does some dirty things, and I'm not proud that it does them, but I think it does them well enough that, while we lack other options now, this will suffice. I built the $actCode
the way I have to isolate the $code
from the environment mostly. It can still access all the classes and such loaded, but at the very least it has no unwanted local variables, and won't leave any unwanted local variables in scope:
<?php
class CleanEval {
public static function RunCode($code, $model) {
$actCode = "
\$html = new Html();
\$cleanFunction = function(\$Model, &\$Html) {
" . $code . "
};
\$result = \$cleanFunction(\$model, \$html);
return array(\$html->GetBuffer());";
return eval($actCode);
}
}
?>
And our Constants
:
<?php
class Constants {
// Set this to the base directory
public const BASE_URL = "/Php7Mvc";
// If set to TRUE then URL's will be: /Controller/Action/Id?QueryString,
// If set to FALSE then URL's will be:
// /index.php?Controller=controller&Action=action&Id=id&QueryString
public const USE_CLEAN_URLS = TRUE;
// If set to TRUE then controllers will require the Controller suffix,
// If set to FALSE then we'll try to find the class with the suffix
// first, but then will allow any class with the controller name.
public const REQUIRE_CONTROLLER_SUFFIX = TRUE;
// Set this to the base layout view (defaults to 'Layout'), do not
// append the VIEW_EXTENSION.
public const LAYOUT_VIEW = "Layout";
// Set this to the extension to use for 'View' files (defaults to php)
public const VIEW_EXTENSION = "php";
}
?>
Then we have a LayoutModel
:
<?php
class LayoutModel extends BaseModel {
public $Controller;
public $View;
}
?>
Now we need to know what BaseModel
is:
<?php
class BaseModel {
public $Title;
}
?>
Not much to those.
We also see HomeIndexViewModel
(another usage example):
<?php class HomeIndexViewModel extends BaseModel { public $Message; public $Items; } ?>
And UpdateModel
(yet another usage model):
<?php class UpdateModel { public $Id; } ?>
The final core class of the framework is the Html
class, which allows easy rendering:
<?php
class Html {
private $buffer;
public function Render($string) {
$this->buffer .= $string;
}
public function AbsoluteUri($relativePath) {
return Constants::BASE_URL . "/" . $relativePath;
}
public function StyleSheet($path) {
return "<link rel=\"stylesheet\" href=\"" . $this->AbsoluteUri($path) . "\" />";
}
public function BuildLink($name, $controller, $action, $id = "", $queryString = "", $classes = "") {
$url = "";
if (Constants::USE_CLEAN_URLS === TRUE) {
$url = Constants::BASE_URL . "/{$controller}/{$action}";
if (strlen($id) > 0) {
$url .= "/{$id}";
}
if (strlen($queryString) > 0) {
$url .= "?{$queryString}";
}
} else {
$url = Constants::BASE_URL . "/index.php?Controller={$controller}&Action={$action}";
if (strlen($id) > 0) {
$url .= "&Id={$id}";
}
if (strlen($queryString) > 0) {
$url .= "&{$queryString}";
}
}
return "<a href=\"{$url}\" class=\"{$classes}\">{$name}</a>";
}
public function GetBuffer() {
return $this->buffer;
}
public function ClearBuffer() {
$this->buffer = "";
}
}
?>
All that's left are the views, the Layout
view is part of the framework:
<html>
<head>
<title><?php $Html->Render($Model->Action . " - " . $Model->Controller); ?></title>
<?php $Html->Render($Html->StyleSheet("Content/Site.css")); ?>
</head>
<body>
<?php $Html->Render($Model->View); ?>
</body>
</html>
The Index
view is not:
<div class="test"> <h1>Page Title</h1> <?php $Html->Render($Model->Title); ?> </div> <div class="test"> <h1>Message</h1> <?php $Html->Render($Model->Message); ?> </div> <div class="test"> <h1>Items</h1> <?php foreach ($Model->Items as $item) { $Html->Render('<div class="test">' . $item . '</div>'); } ?> <?php $Html->Render($Html->BuildLink("Test", "Home", "Detail", 1)); ?> <form method="POST" action="/Php7Mvc/Home/Update"> <input type="text" value="0" name="Id" /> <input type="submit" value="Update" /> </form> </div>
Currently working on improving the form building, but it's still pretty easy.
So, realistically, it's not a lot of work to make this happen. If you don't use the .htaccess
version (which requires mod_rewrite
), you can set USE_CLEAN_URLS
in Constants
to FALSE
, and then links and URL's will generate from the Html
class without the /Controller/Action/Id?QueryString
bit, and will be a regular URL (/index.php?Controller=controller&Action=action&Id=id&QueryString
).
You can comment on everything except the quoted blocks, those are just examples of usage.