There are already many open source MVCs and frameworks for us to use but what if you want to create your own version. This is a basic example that shows us how it can be done. Our example uses composer.json and PSR-4 namespace structure. It will help us prevent using many 'require' and 'include' statements in our files. I'm not going to create all the class files for now but you can easily work out what goes where if you want to add something new. I assume that you have the knowledge! Remember this is not a "finished work". It just gives you an idea.


File and Folder structure


Files and folder commented as "System" must be kept but can be modified or extended. Pay attention to src directory because files and folders are case-sensitive.


mvc                                     # Name of the application (can be anything)
app # System directory
config.php # System file
asset # Where you keep assets (you can restructure it)
css # Example directory
site.css # Example file
image # Example directory
image.jpg # Example file
js # Example directory
site.js # Example file
src # System directory
Controller # System directory
AbstractController.php # System file
Home.php # Example file
League.php # Example file
Core # System directory
Request.php # System file
Template.php # System file
Helper # Where you put helper classes
Model # Where you put model classes
Repository # Where you put repository classes
View # System directory
Home # Example directory
index.html # Example file
League # Example directory
index.html # Example file
base.html # System file
autoload.php # System file
vendor # System directory generated by composer.json
.htaccess # System file
composer.json # System file
index.php # System file

Install


# Install composer on system
$ curl -sSk https://getcomposer.org/installer | php -- --disable-tls
All settings correct for using Composer
Downloading 1.2.1...

Composer successfully installed to: /vagrant/composer.phar
Use it: php composer.phar
You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.
This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks

# Globally enable composer
$ mv composer.phar /usr/local/bin/composer
$ composer self-update
You are already using composer version 1.2.1 (stable channel).

# Generate composer autoloader for the application
mvc $ composer dump-autoload -o
Generating optimized autoload files

How it works


The first segment of URL is "controller" name and the second one is "method" name in that controller.


# Logic
1. User goes to: http://www.yourdomain/
2. Request gets picked up by index.php
3. Relevant controller and its method gets triggered
4. Response is send back

# Example 1
1. Request: http://www.yourdomain/league
2. Process: index.php hits "indexMethod" method of League.php controller
3. Content: Template.php reads "Home\index.html" under "View"
4. Response send back

# Example 2
1. Request: http://www.yourdomain/user/full-detail
2. Process: index.php hits "fullDetailMethod" method of User.php controller
3. Content: Template.php reads "User\fullDetail.html" under "View"
4. Response send back

# Example (Error)
1. Request: http://www.yourdomain/league/non-existing
2. Error gets triggered as:
404
Method cannot be found: [Controller\league:nonExistingMethod]
/var/www/html/mvc/src/Core/Request.php:88

Structure


app/config.php


define('APP_NAME', 'Simple MVC');
define('APP_DOMAIN', 'http://192.168.50.20');
define('APP_INNER_DIRECTORY', '/mix/mvc');
define('APP_ROOT', __DIR__.'/..');

define('APP_CONTROLLER_NAMESPACE', 'Controller\\');
define('APP_DEFAULT_CONTROLLER', 'Home');
define('APP_DEFAULT_CONTROLLER_METHOD', 'index');
define('APP_CONTROLLER_METHOD_SUFFIX', 'Method');

define('DB_HOST', 'localhost');
define('DB_NAME', 'simplemvc');
define('DB_USER', 'root');
define('DB_PASS', 'root');

src/Controller/AbstractController


namespace Controller;

use Core\Template;

class AbstractController
{
private $template;

public function __construct()
{
$this->template = new Template();
}

protected function getView($controller, array $variables = [])
{
return $this->template->getView($controller, $variables);
}
}

src/Controller/Home


namespace Controller;

use Core\Template;

class Home extends AbstractController
{
public function __construct()
{
parent::__construct(new Template());
}

public function indexMethod()
{
return parent::getView(
__METHOD__,
[
'title' => APP_NAME.' - Home',
'header' => 'Welcome to '.APP_NAME,
'application_name' => APP_NAME,
]
);

}
}

src/Controller/League


namespace Controller;

use Core\Template;

class League extends AbstractController
{
public function __construct()
{
parent::__construct(new Template());
}

public function indexMethod()
{
return parent::getView(
__METHOD__,
[
'title' => APP_NAME.' - Home',
'header' => 'League list',
'league_list' => 'A B C',
]
);

}
}

src/Core/Request


namespace Core;

use Exception;

class Request
{
private $server;
private $post;
private $get;
private $files;

public function __construct(
array $server = [],
array $post = [],
array $get = [],
array $files = []
) {
$this->server = $server;
$this->post = $post;
$this->get = $get;
$this->files = $files;
}

public function getServer($index = null)
{
return !is_null($index) && isset($this->server[$index]) ? $this->server[$index] : $this->server;
}

public function getPost()
{
return $this->post;
}

public function getGet()
{
return $this->get;
}

public function getFiles()
{
return $this->files;
}

public function getController()
{
$urlParts = $this->getUrlParts();

// If controller name is not set in URL return default one
if (!isset($urlParts[0])) {
return APP_CONTROLLER_NAMESPACE.APP_DEFAULT_CONTROLLER;
}

// If controller exists in system then return it
if (class_exists(APP_CONTROLLER_NAMESPACE.$urlParts[0])) {
return APP_CONTROLLER_NAMESPACE.$urlParts[0];
}

// Otherwise
http_response_code(404);
throw new Exception(sprintf('Controller cannot be found: [%s]', APP_CONTROLLER_NAMESPACE.$urlParts[0]), 404);
}

public function getMethod($controller)
{
$urlParts = $this->getUrlParts();

// If controller method is not set in URL return default one
if (!isset($urlParts[1])) {
return APP_DEFAULT_CONTROLLER_METHOD.APP_CONTROLLER_METHOD_SUFFIX;
}

// If controller method name pattern is invalid
if (!preg_match('/^[a-z\-]+$/', $urlParts[1])) {
http_response_code(400);
throw new Exception(sprintf('Invalid method: [%s]', $urlParts[1]), 400);
}

// If controller method exists in system then return it
$method = $this->constructMethod($urlParts[1]);
if (method_exists($controller, $method)) {
return $method;
}

// Otherwise
http_response_code(404);
throw new Exception(sprintf('Method cannot be found: [%s:%s]', $controller, $method), 404);
}

private function getUrlParts()
{
$url = str_replace(APP_INNER_DIRECTORY, null, $this->getServer('REQUEST_URI'));
$urlParts = explode('/', $url);
$urlParts = array_filter($urlParts);
$urlParts = array_values($urlParts);

return $urlParts;
}

private function constructMethod($part)
{
$method = null;

$parts = explode('-', $part);
foreach ($parts as $part) {
if (!$method) {
$method = $part;
} else {
$method .= ucfirst($part);
}
}

return $method.APP_CONTROLLER_METHOD_SUFFIX;
}
}

src/Core/Template


namespace Core;

use Exception;

class Template
{
private $viewPath = '%s/src/View';
private $baseView = 'base.html';
private $reservedVariables = ['application_name', 'body'];

public function __construct()
{
$this->viewPath = sprintf($this->viewPath, APP_ROOT);
}

public function getView($controller, array $variables = [])
{
$variables = $this->validateVariables($variables);

$parts = explode('::', $controller);
$directory = $this->getDirectory($parts[0]);
$file = $this->getFile($parts[1]);

$viewPath = $this->viewPath.'/'.$directory.'/'.$file.'.html';
if (file_exists($viewPath)) {
$baseView = file_get_contents($this->viewPath.'/'.$this->baseView);
$body = file_get_contents($viewPath);
$view = str_replace('{{ body }}', $body, $baseView);

foreach ($variables as $key => $value) {
$view = str_replace('{{ '.$key.' }}', $value, $view);
}

return $view;
}

http_response_code(404);
throw new Exception(sprintf('View cannot be found: [%s]', $viewPath), 404);
}

private function validateVariables(array $variables = [])
{
foreach ($variables as $name => $value) {
if (in_array($name, $this->reservedVariables)) {
http_response_code(404);
throw new Exception('Unacceptable view variable given: [body]', 409);
}
}

$variables['application_name'] = APP_NAME;

return $variables;
}

private function getDirectory($controller)
{
$parts = explode('\\', $controller);

return end($parts);
}

private function getFile($controller)
{
return str_replace(APP_CONTROLLER_METHOD_SUFFIX, null, $controller);
}
}

src/View/Home/index.html


Welcome to our {{ application_name }} application! It shows us how to create a simple MVC.

src/View/League/index.html


See the list of Football Leagues: {{ league_list }}

src/View/base.html


<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<div>
<ul>
<li><a href="home">HOME</a></li>
<li><a href="league">LEAGUE</a></li>
</ul>
</div>
<hr />
<div>
<h3>{{ header }}</h3>
</div>
<hr />

{{ body }}

<hr />
<div id="footer">
<h3>© All rights reserved. {{ application_name }}</h3>
</div>
</body>
</html>

src/autoload.php


require_once __DIR__.'/../vendor/autoload.php';

.htaccess


# This will prevent generating "Not Found" error in browser
# Then we will manually do the rest to produce page for user
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>

composer.json


{
"autoload": {
"psr-4": {
"": "src/"
}
}
}

index.php


require_once __DIR__.'/app/config.php';
require_once __DIR__.'/src/autoload.php';

use Core\Request;

$request = new Request($_SERVER, $_POST, $_GET, $_FILES);

try {
$controller = $request->getController();
$method = $request->getMethod($controller);

$controller = new $controller;
echo $controller->$method();
} catch (Exception $e) {
echo sprintf(
'<h3>%s</h3><h4>%s</h4><h5>%s:%s</h5>',
$e->getCode(),
$e->getMessage(),
$e->getFile(),
$e->getLine()
);
}