Dec 16, 2015
Building a simplistic MVC framework with AuraPHP and Twig
7 min read
Check out my previous post
In view of that, i thought it'd be nice to describe how a minimal MVC structure could be implemented without running a composer require large/framework
when all you just want
is a clean way to separate your logic from presentation and do not need all the features that'd be available within large/framework
.
AuraPHP
This is a router and we only are installing it since we get pretty urls which is a must nowadays.
Provides a web router implementation: given a URL path and a copy of
$_SERVER
, it will extract path-info and$_SERVER
values for a specific route.
Twig
Twig is a templating engine and helps us separate our template files for reusability and maintenance. I already wrote a post on the benefits of using a template engine.
Installation
Create a config.json
file in the root of any directory and update it as below :
{
"require": {
"twig/twig": "^1.22",
"aura/router": "^2.3"
},
"psr-4": {
"application\\": "src"
}
}
after which you'd run composer install
in an open terminal (with it's current directory where the config.json
file have been placed).
Usage
For AuraPHP (or any other router) to route and prettify our routes, we need to configure the web server to forward all requests to a single file, usually index.php
.
Create a web sever config file (i'm using apache, so i would have .htaccess
) and a index.php
file.
touch index.php
touch .htaccess
Put the following in the web server config file
RewriteEngine on
Options -MultiViews
Options -Indexes
RewriteBase /
RewriteRule !\.(js|gif|jpg|png|css)$ index.php [L]
This configuration is meant for Apache web servers, if you use Nginx or another, a quick google search should help you forward all requests to a single file.
Fire up your favorite editor with the index.php
file opened
<?php
require_once "path/to/vendor/autoload.php";
use Aura\Router\RouterFactory;
$router_factory = new RouterFactory();
$router = $router_factory->newInstance();
//"action" is the Controller class to be called while "method" is the method to be invoked.
$router->addGet(null, '/')
->addValues([
"action" => "IndexController",
"method" => "showIndex"
]);
$router->addPost(null, '/submit')
->addValues([
"action" => "ContactController",
"method" => "validateIndexPageForm"
]);
// get the incoming request URL path
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// get the route based on the path and server
$route = $router->match($path, $_SERVER);
if (!$route) {
$unroutable = new \application\name\ErrorController();
$unroutable->invalidRoute();
} else {
$action_class = $route->params['action'];
$fullclassNameWithNs = "application\\name\\$action_class";
$instance = new $fullclassNameWithNs();
$methodToCall = $route->params['method'];
$instance->$methodToCall($route->params);
}
As depicted above, the methods are quite descriptive.
This is a simple example as it represents a :
GET
request to the/
route. The action and method param are what we actually need in order to present a little attractive something to our users. In order to achieve this, we would employ Twig since we are all about separating the pieces of our application .POST
request to the/submit
route.
If no routes match what have been registered, call the ErrorController
, which is simply a wrapper for 404
HTTP responses. All it does is present the user an error page.
Only HTTP Method(s) assigned to a route would match else a 404 HTTP error would be thrown.
addHead()
,addDelete()
,addPut
,addPatch
,addOptions
are also available for use. But do have it in mind that not all servers are configured to accept some certainHTTP
request methods,GET
andPOST
are available on all servers though.
There's a C (controller) in MVC, so we'd want something to mediate between web requests and our web app. Let's write one .
<?php
//filename : BaseController.php
namespace application\name;
/**
* Every other controller extends this class
*/
abstract class BaseController
{
/**
* @var string
* Directory where templates are saved,can be overridden in child classes.
*/
const TEMPLATE_DIRECTORY = "src/view";
/**
* @param string $templateFileToLoad the name of the file (without extension) to fire up.
* @param array $params the parameters passed to the views.
* @param string $fileNameEndsWith the suffix of the file name.
*/
protected function makeView($templateFileToLoad, array &$params, $fileNameEndsWith = ".html.twig")
{
$path = static::TEMPLATE_DIRECTORY;
$fileFullName = $templateFileToLoad . $fileNameEndsWith;
$twig = $this->getTwig($path, [
"debug" => true
]);
$twig->addExtension(new \Twig_Extension_Debug());
echo $twig->render($fileFullName, $params);
}
/**
* @param string $path the path where templates are stored
* @param array $options
* @return \Twig_Environment
*/
protected function getTwig($path, array $options = null)
{
$loader = new \Twig_Loader_Filesystem($path);
return new \Twig_Environment($loader, $options);
}
}
<?php
//filename : IndexController.php
namespace application\name;
/**
* Controller for the '/' route.
*/
class IndexController extends BaseController
{
public function showIndex(array $params = null)
{
$data = [
"name" => "Adelowo Lanre" ,
"superpowers" => "Programming, Spending hours on the internet"
];
//In the real world, this would be gotten from a file or database engine.
parent::makeView("index", $data);
}
}
<?php
//FileName : ContactController.php
namespace application\name;
class ContactController extends BaseController
{
const FORM_HAS_ERRORS = "The submitted form is invalid and could not be processed as a result of unprovided data";
public function validateIndexPageForm(array $params = null)
{
//Whoosh, Validate the POSTed data here
}
}
In the above code block for IndexController.php
, we have instructed Twig to load a template file named index.html.twig
located within the src/view
directory.
The file does not exist yet so it should be created at this instance.
{% highlight liquid %}
{% raw %}
{# filename : index.html.twig #}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="author" content="Adelowo Lanre"/>
<title> Index Page </title>
</head>
<body>
{{ name }} {# "Adelowo Lanre" #}
{{ superpowers }} {# "Programming, Spending hours on the internet" #}
</body>
</html>
{% endraw %} {% endhighlight %}
Ok, that works right but we can't always know what parameters a route would present as web developers are always working with dynamic data every now and then. We need dynamic routes, i.e one with parameters!!
If you aren't working with dynamic routes, it would be an overkill to use a routing engine (or even the MVC pattern) since all pages are predetermined. Plain old multiple points of entry (i.e
index.php
,contact.php
,about.php
) OR a simpleswitch
statement in theindex.php
file would do the trick.
<?php
//match routes like /10/adelowo
$router->addGet(null, '/{id}{/name}')
->addValues([
"action" => "UserController",
"method" => "showAUser"
])
->addTokens([
"id" => "\d+", //Regular expression to run on the 'id' parameter in the uri. Make sure it is a digit, we do not want an alpha-numeric value as ID.
//you can also validate the 'name' parameter as shown below
"name" => "\w+"
]);
//match routes like /pdf/10
$router->addGet(null, '/pdf/{id}')
->addValues([
"action" => 'UserController',
"method" => "showPdf"
])
->addTokens([
"id" => "\d+"
]);
<?php
//fileName :UserController.php
namespace application\name;
class UserController extends BaseController
{
public function showAUser(array $params = null)
{
$data = [];
//This method is for routes like /10/adelowo, so the url parameters are available as $params['id'] and $params['name'].
// The array keys are the same as what was specified in the route registration.
$data['user'] = Helper::LoadDataFromFile($params['id']); //load the user data from some file.
parent::makeView("school", $data);
}
/**
* allows a user data to be downloaded in PDF format
* @param array $params
*/
public function showPdf(array $params = null)
{
//matches routes like /pdf/10
$pdfMaker = $this->getPdfLib();
$pdfMaker->writeBody(45, 55, 56, 55, String::getRandom());
$pdfMaker->Output('user'.$param['id'] . '.Pdf', 'I'); //user10.pdf gets downloaded.
}
}
Official Documentation
There are other methods AuraPHP router offers but this are the most implemented of them. Do check out it's github's page to find out those that were not mentioned .
Twig's online documentation can be found here.
There you go!!. A minimal MVC framework without the use of framework Y. With that said, frameworks on the market do offer far more features than what we've just built so far - heck, all our 'framework' has is just a router and a template engine but hey you sometimes do not want that monolithic Framework.