Chapter 1 - Intro

If you are planning to land a job as a web developer or if you just want to understand how webapps work so you can hack them, you might wanna know a thing or two about frameworks. Over the course of this tutorial I'll show you how the frameworks are made, why people use use them and how they function. Keep in mind though, that the framework we will build, isn't fit for a production purposes, I'm just gonna use it to help you understand them.
Onto the main course, in order to follow this tutorial you will need:

  • php 7.x
  • mysql
  • webserver (apache or nginx)
  • editor

Chapter 2 - Structure

I will assume that you have testing environment set up (nginx/apache and database) and running. So the first thing we need to is to separate logic part of the framework from the html part. To do so, we will create two folders in the root directory of the project. It will look something like this:

root/
	index.php 
	application/ #logic goes here
	public/	#design goes here

MVC stands for Model-View-Controller which tells us how the backend distributes tasks. Let's take a look at what it looks like in practice.

1. user navigates browser to somewebsite.com/forum/introductions
2. router on ourwebsite.com recognizes the route /forum/introductions as one of it's own that happens to be governed by forum controller
3. router calls forum controller and passes the parameters (in this case introductions)
4. controller creates the model Introductions (which contains list of all introductory threads) and passes it to a view for a user to see 

So from this example we can conclude that the core part of the framework consists of 4 parts (router, controller, model and view) so let's create them. Oh yeah, we will need two more files; one that contains a list of routes and another, let's call it config file, to store defaults.
Now lets add something to the public/ folder so we could actually see something when we test it. Actually let's structure it as well so we will have an easier time customizing it later. After all that our structure should look like this:

root/
	index.php
	application/
		config/
			config.php
			routes.php
		core/
			Controller.php
			Model.php
			Router.php
			View.php
	public/
		components/
		errors/
			404.php
			500.php
		layouts/
			default.php
		views/

Now we are finally ready to do some coding.

Chapter 3 - Bare bones

Okay now onto coding. First we'll take care of two config files so open up /application/config/config.php and put in some defaults (this file usually holds a whole bunch of stuff like language, timezone, db settings and more, but for now we will just add what we will need in this lesson):

<?php

define('TITLE', 'mvc from scratch');
define('ENVIRONMENT', 'dev'); //"dev" will will show errors, "prod" hides them
define('LAYOUT', 'default'); //in /public/layouts/
define('ROOT_URI_PATH', '/'); //or, if you are on xampp, name of the project folder in htdocs

//WEB STRUCTURE
define('URL_PROTOCOL', '//'); //protocol independent (as opposed of explicitly setting http/https)
define('URL_DOMAIN', $_SERVER['HTTP_HOST']);
define('URL', URL_PROTOCOL . URL_DOMAIN);

//FOLDER STRUCTURE
define('PUBLIC_PATH', ROOT_PATH . 'public/');
define('VIEW_PATH', PUBLIC_PATH . 'public/views/');	

Then do the same thing for the routes in /application/config/routes.php. Routes.php is just an array that holds list of open routes and their info (so that router knows which action of what controller to call when the route is triggered).

<?php

return []; //let's just leave it empty for now

For now that should be enough, but over the course of the tutorial we will add more things to it. So now that we've set up defaults, we can start working on the core of our system. Let's open the /application/core/View.php first.

View.php is a simple class whose only mission is to render a HTML by combining web template from /public and variables from model object(s) that were passed to it by controller. The following lines should be enough so you can put them in the file.

<?php

namespace application\core;


class View 
{

	public $path;
	public $route;
	public $layout;

	public function __construct($route, $layout=LAYOUT) 
	{
		$this->route = $route;
		$this->layout = $layout;
		
		// this part is just a convention.. it means that views will
		// always be named like /public/views/controllerName/actionName.php
		// so that it will be easier to find them, but change it if you want
		$this->path = $route['controller'].'/'.$route['action'];
	}

	// Main render method that merges layout with the view so only
	// thing that changes is the content part of the website (we don't
	// need to code navigation footer and rest of the static elements 
	// for every view)
	public function render($vars = []) 
	{
		extract($vars); //data passed by controller
		$path = VIEW_PATH . $this->path. '.php';
		if (file_exists($path)) { 
			ob_start();
			require $path;
			$content = ob_get_clean();
			require PUBLIC_PATH . '/layouts/'.$this->layout.'.php';
		}
	}

	// Method for rendering error pages as they should usually only
	// show error message/code, we do not need to fetch a main layout 
	// for them
	public static function errorCode($code) 
	{
		http_response_code($code);
		$path = PUBLIC_PATH . '/errors/'.$code.'.php';
		if (file_exists($path)) {
			require $path;
		}
		exit;
	}

	// Simple redirect method
    public function redirect($url)
    {
        header('location: '.$url);
        exit;
    }
}

Now that the View class is done, we will move on to the next core class which is router. This class will take the url that user had searched for and check if it matches with one of the routes from our config folder. If the match is found router will trigger the corresponding controller, and if it's not it will render an error. So let's get to it, open the /application/core/Router.php and write:

<?php

namespace application\core;


use View; //we will need it to render errors (if there are any)

class Router 
{

    protected $routes = [];
    protected $params = [];
    
    public function __construct() 
    {
        $arr = require 'application/config/routes.php';
        foreach ($arr as $key => $val) {
            $this->add($key, $val);
        }
    }

    public function add($route, $params) 
    {
        $route = parse_url($route, PHP_URL_PATH);	
        $route = '#^'.$route.'$#';
        $this->routes[$route] = $params;
    }

	//this is not a very secure way to do this but later on in
	//the tutorial, when we start talking about security, we will 
	//return here to patch it
    public function match() 
    {
        $url = parse_url(trim($_SERVER['REQUEST_URI'], ROOT_URI_PATH), PHP_URL_PATH);

        foreach ($this->routes as $route => $params) {
            if (preg_match($route, $url, $matches)) {
                $this->params = $params;
                return true;
            }
        }
        return false;
    }

    public function run()
    {
        if ($this->match()) {
            $path = 'application\controllers\\'.ucfirst($this->params['controller']).'Controller';
            if (class_exists($path)) {
                $action = $this->params['action'].'Action';
                if (method_exists($path, $action)) {
                    $controller = new $path($this->params);
                    $controller->$action();
                } else {
                    View::errorCode(400); //both match and controller are found, but there is no action with that name, return 400
                }
            } else {
                View::errorCode(500); //match is found, but there is no controller, return 500
            }
        } else {
            View::errorCode(404); //no match found, return 404
        }
    }

}

We are almost done with the core, so let's open the /application/core/Controller.php. We don't wanna too much code in here. In fact, rule of thumb is that all business logic should be placed in models, and controllers should just point which model should be passed to which view. So with that in mind lets create an abstract class for controllers.

<?php

namespace application\core;


use View;

// We are using abstract class here because this is just
// a template for controllers. Every section of the webapp 
// will have it's own controller that will extend this template
abstract class Controller 
{

	public $route;
	public $view;

	public function __construct($route) 
	{
		$this->route = $route;

		$this->view = new View($route);
		
		//if model is named the same as a controller, 
		//load it automatically
		$this->model = $this->loadModel($route['controller']);
	}

	public function loadModel($name) 
	{
		$path = 'application\models\\'.ucfirst($name);
		if (class_exists($path)) {
			return new $path;
		}
	}

}

When I said that we are almost done with the core functionality I meant it, as the contents for our last file /application/core/Model.php are just:

<?php

namespace application\core;


abstract class Model 
{	
}

Contents of the model are usually specific to that model, so right now we will leave it empty, but later on, when we add database support into the mix, we can extend it with some common functionalities.

And that is it. In the next section we will create a simple webpage to test if everything is working, then we will move on to extend our framework further.

Chapter 4 - First app

Now that we have the core set up, we are ready building something. First, let's edit the index.php in the root folder of our project. As a first file server will load, we need to make sure it has the instructions to initialize the router, so that our app can be triggered.

<?php

use application\core\Router;

define('ROOT_PATH', __DIR__. '/');
require_once ROOT_PATH . 'application/config/config.php';

//autoload_register will magically load every class that we need
spl_autoload_register(function($class) {
    $path = str_replace('\\', '/', $class.'.php');
    if (file_exists($path)) {
        require $path;
    }
});

if(ENVIRONMENT === 'dev') {
	//debug flags will go here later on
}elseif(ENVIRONMENT === 'prod'){
    error_reporting(0);
    ini_set('display_errors', 0);
}else{
    die('Fatal error: environment not defined or valid!');
}

$router = new Router;
$router->run();

Now that that's done lets quickly add some basic html template. Open the /public/templates/default.php and add

<!DOCTYPE html>
<html>
<head>
</head>
<body>
	<!-- $content is rendered in view -->
	<?php echo $content; ?>
</body>
</html>

we also need to create the view, so navigate to /public/views/ and create folder "main" (which will also be the name of our controller) and in it create the file "index.php" ("index" being the name of the triggered action in that controller) and paste this

<p>
	<!-- $data will be defined in the controller -->
	<?php echo $data->getMessage(); ?>
</p>

Okay. Now we are done with the frontend so let's get back to our /application/config/routes.php and add a new route:

<?php

return [
	
	// '' (as in empty string) is equivalent to domain name
	// for example ourwebsite.com, if we, for example wrote  
	// 'topic/new' user would need to navigate to 
	// ourwebsite.com/topic/new to trigger the route

	'' => ['controller' => 'main',  'action' => 'index'],

];

Now lets create a model for our page. Create folder "models" inside /application/ directory and inside it create Main.php, then open it and write something like this:

<?php

namespace application\models;


use application\core\Model;

class Main extends Model //we are extending the model defined in the core/
{
	public function getMessage()
	{

        return "My message";
    }
}

Great, now we have something to return to view. Now we only need to make a controller that will put it all together. Create folder "controllers" in /application/ and inside it create the file "MainController.php"

<?php

namespace application\controllers;


use application\core\Controller;

class MainController extends Controller {

	public function indexAction() {
		//if model is named like the controller (like it is now) 
		//we don't have to explicitly define it

		$vars = [
			'data' => $this->model->get
		];

		$this->view->render($vars);
	}

}

Now when you navigate your browser to "ourwebapp.com/home/page" we are greeted wtih the fallowing screen:

Screenshot

And there you have it. Our first app creted with this framework. In the next sections we will talk about security and error handling, add a database support then make another example that is more in line with what would you think when you hear someone says web app.

Chapter 5 - Basic security

I have told you before that this framework will not be of a production grade quality, but that does not mean we should leave the security holes wide open. In this chapter we will make three new classes that will hopefully make this framework more secure. And first class we'll be covering is request helper. So create a new folder "helpers" in our /application/ directory and put in it a new file "Request.php".

<?php

namespace application\helpers;


class Request 
{

}

This class will consist of three parts. First is gonna be so called checkers part that we can use to get informations about the request so let's add those methods in our Request.php file.

//returns protocol used for by the request
public static function protocol() {
    $secure = (self::server('HTTP_HOST') && self::server('HTTPS') && strtolower(self::server('HTTPS')) !== 'off');

    return $secure ? 'https' : 'http';
}

//check if the request is sent via ajax function
public static function isAjax() {
    return (self::server('HTTP_X_REQUESTED_WITH') && strtolower(self::server('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest');
}

//determains which method is used to send a request
public static function method($upper = true) {
    $method = self::server('REQUEST_METHOD');

    return $upper ? strtoupper($method) : strtolower($method);
}

//and finally we can check the referer header if we need to
public static function referrer($default = null) {
    $referrer = self::server('HTTP_REFERER', $default);

    if ($referrer === null && $default !== null) {
        $referrer = $default;
    }

    return $referrer;
}

Now those methods are just used to check something, they don't actually parse the request. To do so we first need to sanatize it, so let's also add the function that will do just that.

public static function xssClean($str = '') {
    // No data? We're done here
    if (is_string($str) && trim($str) === '') {
        return $str;
    }

    // Recursive sanitize if this is an array
    if (is_array($str)) {
        foreach ($str as $key => $value) {
            $str[$key] = self::xssClean($value);
        }

        return $str;
    }

    $str = str_replace(array(
        '&amp;',
        '&lt;',
        '&gt;'
    ), array(
        '&amp;amp;',
        '&amp;lt;',
        '&amp;gt;'
    ), $str);

    // Fix &entitiy\n;
    $str = preg_replace('#(&\#*\w+)[\x00-\x20]+;#u', '$1;', $str);
    $str = preg_replace('#(&\#x*)([0-9A-F]+);*#iu', '$1$2;', $str);
    $str = html_entity_decode($str, ENT_COMPAT, 'UTF-8');

    // remove any attribute starting with "on" or xmlns
    $str = preg_replace('#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>#iUu', '$1>', $str);

    // remove javascript
    $str = preg_replace('#([a-z]*)[\x00-\x20\/]*=[\x00-\x20\/]*([\`\'\"]*)[\x00-\x20\/]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2nojavascript...', $str);
    $str = preg_replace('#([a-z]*)[\x00-\x20\/]*=[\x00-\x20\/]*([\`\'\"]*)[\x00-\x20\/]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2novbscript...', $str);
    $str = preg_replace('#([a-z]*)[\x00-\x20\/]*=[\x00-\x20\/]*([\`\'\"]*)[\x00-\x20\/]*-moz-binding[\x00-\x20]*:#Uu', '$1=$2nomozbinding...', $str);
    $str = preg_replace('#([a-z]*)[\x00-\x20\/]*=[\x00-\x20\/]*([\`\'\"]*)[\x00-\x20\/]*data[\x00-\x20]*:#Uu', '$1=$2nodata...', $str);

    // Remove any style attributes, IE allows too much stupid things in them
    $str = preg_replace('#(<[^>]+[\x00-\x20\"\'\/])style[^>]*>#iUu', '$1>', $str);

    // Remove namespaced elements
    $str = preg_replace('#</*\w+:\w[^>]*>#i', '', $str);

    // Remove really unwanted tags
    do {
        $oldstring = $str;
        $str = preg_replace('#</*(applet|meta|xml|blink|link|style|script|embed|object|iframe|frame|frameset|ilayer|layer|bgsound|title|base)[^>]*>#i', '', $str);
    }
    while ($oldstring !== $str);

    return $str;
}

Now every time we need to get data from the request we'll need to run it through xssClean function, but doing so is easy to forget so lets add some wrapper methods to help us with that.

//check if something exists in request payload
private static function _findFromArray($array = array(), $item = '', $default = null, $xss_clean = true) {
    if (empty($array)) {
        return $default;
    }

    if ( ! $item) {
        $arr = array();
        foreach (array_keys($array) as $key) {
            $arr[$key] = self::_fetchFromArray($array, $key, $default, $xss_clean);
        }
        return $arr;
    }

    return self::_fetchFromArray($array, $item, $default, $xss_clean);
}

//if it exists, fetch it
private static function _fetchFromArray($array, $item = '', $default = null, $xss_clean = true) {
    if ( ! isset($array[$item])) {
        return $default;
    }

    if ($xss_clean) {
        return self::xssClean($array[$item]);
    }

    return $array[$item];
}

Now we have wrappers that will make sure we always sanatize request payload before we use them. But it looks ugly and it's not really intuitive, so let's also add the layer of abstraction over it so when we use it we know, on glance, what it's used for.

public static function server($index = '', $default = null) {
    return self::_findFromArray($_SERVER, $index, $default, false);
}

public static function get($item = null, $default = null, $xss_clean = true) {
    return self::_findFromArray($_GET, $item, $default, $xss_clean);
}

public static function post($item = null, $default = null, $xss_clean = true) {
    return self::_findFromArray($_POST, $item, $default, $xss_clean);
}

public static function request($item = null, $default = null, $xss_clean = true) {
    $request = array_merge($_GET, $_POST);

    return self::_findFromArray($request, $item, $default, $xss_clean);
}

public static function file($item = null, $default = null) {
    // If a file field was submitted without a file selected, this may still return a value.
    // It is best to use this method along with Input::hasFile()
    return self::_findFromArray($_FILES, $item, $default, false);
}

We can do an even higher level of abstraction if we want. In fact, let's do it, just to make things easier for us later on.

public static function inGet($item = null) {
    return self::get($item, null, false) !== null;
}

public static function inPost($item = null) {
    return self::post($item, null, false) !== null;
}

public static function inRequest($item = null) {
    return self::request($item, null, false) !== null;
}

public static function inFile($item = null) {
    return self::file($item) !== null;
}

public static function hasFile($item = null) {
    $file = self::file($item);

    return ($file !== null && $file['tmp_name'] !== '');
}

And that is it for the request helper. Now we can patch some earlier vulnerabilities. Edit the file /application/core/Router.php like this:

<?php

namespace application\core;

use View;
use application\helpers\Request;

class Router 
{

    protected $routes = [];
    protected $params = [];
    
    public function __construct() 
    {
        $arr = require 'application/config/routes.php';
        foreach ($arr as $key => $val) {
            $this->add($key, $val);
        }
    }

    public function add($route, $params) 
    {
        $route = parse_url($route, PHP_URL_PATH);
        $route = '#^'.$route.'$#';
        $this->routes[$route] = $params;
    }

    public function match() 
    {
        $url = parse_url(trim(Request::server('REQUEST_URI'), ROOT_URI_PATH), PHP_URL_PATH);

        foreach ($this->routes as $route => $params) {
            if (preg_match($route, $url, $matches)) {
                $this->params = $params;
                return true;
            }
        }
        return false;
    }

    public function run()
    {
        if ($this->match()) {
            $path = 'application\controllers\\'.ucfirst($this->params['controller']).'Controller';
            if (class_exists($path)) {
                $action = $this->params['action'].'Action';
                if (method_exists($path, $action)) {
                    $controller = new $path($this->params);
                    $controller->$action();
                } else {
                    View::errorCode(500);
                }
            } else {
                View::errorCode(400);
            }
        } else {
            View::errorCode(404);
        }
    }

}

We can also put the use application\helpers\Request; in the /application/core/Model.php so it look like this:

<?php

namespace application\core;


use application\helpers\Request;

abstract class Model 
{	

}

The next file we are going to make will be focused on protection against csrf attacks so create a file /application/helpers/CSRF.php

<?php

namespace application\helpers;


use Request;

class CSRF
{
    public static $name = '_CSRF';

    public function __construct()
    {
        session_start(); //later we will need to make sessions a bit more secure also
    }

    public function insert($form = 'default')
    {
        echo '<input type="hidden" name="csrf_token" value="' . $this->generate($form) . '">';
    }


    public function generate($form = NULL)
    {
        $token = self::token() . self::fingerprint();
        $_SESSION[self::$name . '_' . $form] = $token;
        return $token;
    }

    public function check($token, $form = NULL)
    {
         if( isset($_SESSION[self::$name . '_' . $form]) && $_SESSION[self::$name . '_' . $form] == $token){ //token OK
            return (substr($token, -32) === self::fingerprint()); // fingerprint OK?
        }
        return FALSE;
    }

    protected static function token()
    {
        mt_srand((double) microtime() * 10000);
        $charid = strtoupper(md5(uniqid(rand(), TRUE)));
        return substr($charid, 0, 8) .
               substr($charid, 8, 4) .
               substr($charid, 12, 4) .
               substr($charid, 16, 4) .
               substr($charid, 20, 12);
    }

    protected static function fingerprint()
    {
        return strtoupper(md5(implode('|', array(Request::server('REMOTE_ADDR'), Request::server('HTTP_USER_AGENT')))));
    }
}

That was a relatively simple class, what it does is basicly generating and checking the csrf token when forms are sent. Let's add that to our view core class so that it always has csrf token ready if we need to create form in the future. Edit the /application/core/View.php

<?php

namespace application\core;


use application\helpers\CSRF;

class View 
{

	public $path;
	public $route;
	public $layout;

	public function __construct($route, $layout=LAYOUT) 
	{
		$this->route = $route;
		$this->layout = $layout;
		$this->path = $route['controller'].'/'.$route['action'];
	}

	public function render($vars = []) 
	{
		$csrf = new CSRF(); // create a new csrf object so we can use it to generate tokens on the fly
		extract($vars);
		$path = VIEW_PATH . $this->path. '.php';
		if (file_exists($path)) {
			ob_start();
			require $path;
			$content = ob_get_clean();
			require PUBLIC_PATH . '/layouts/'.$this->layout.'.php';
		}
	}

	public static function errorCode($code) 
	{
		http_response_code($code);
		$path = PUBLIC_PATH . '/errors/'.$code.'.php';
		if (file_exists($path)) {
			require $path;
		}
		exit;
	}

    public function redirect($url)
    {
        header('location: '.$url);
        exit;
    }
}	

And with that we are done with csrf helper class. The last class we are going to make in this chapter is a validation helper class. So create yet another in /application/helpers/ and name it Validation.php. This will be a relativly long file, but all it holds is a list of definitions for the rules we are going to use when we get to validate user input. It is worth noting that is not a be all end all list of validators, if you think your app will need some additional definitions then by all means add them. But for the course of this tutorial theese should be enough.

<?php

namespace application\helpers;


class Validation {

	public static function checkLength($value, $maxLength, $minLength = 0) 
	{
		if (!(strlen($value) > $maxLength) && !(strlen($value) < $minLength)) {
			return true;
		} else {
			return false;
		}
	}

	public static function compare($value1, $value2, $caseSensitive = false) 
	{
		if ($caseSensitive) {
			return ($value1 == $value2 ? true : false);
		} else {
			if (strtoupper($value1) == strtoupper($value2)) {
				return true;
			} else {
				return false;
			}
		}
	}

	public static function contains($string, $find, $caseSensitive = true) 
	{
		if (strlen($find) == 0) {
			return true;
		} else {
			if ($caseSensitive) {
				return (strpos($string, $find) !== false);
			} else {
				return (strpos(strtoupper($string), strtoupper($find)) !== false);
			}
		}
	}

	public static function convertToDate($date, $timezone = TIMEZONE, $forceFixDate = true) 
	{
		if ($date instanceof DateTime) {
			return $date;
		} else {
			date_default_timezone_set($timezone);

			$timestamp = strtotime($date);

			if ($timestamp) {
				$date = DateTime::createFromFormat('U', $timestamp);
			} else {
				$date = false;
			}

			return $date;
		}
	}

	public static function getAge($dob, $timezone = TIMEZONE) 
	{
		$date     = self::convertToDate($dob, $timezone);
		$now      = new DateTime();
		$interval = $now->diff($date);
		return $interval->y;
	}

	public static function getDefaultOnEmpty($value, $default) 
	{
		if (self::hasValue($value)) {
			return $value;
		} else {
			return $default;
		}
	}

	public static function hasArrayKeys($array, $required_keys, $keys_case = false) 
	{
		$valid = true;
		if (!is_array($array)) {
			$valid = false;
		} else {
			foreach ($required_keys as $key) {
				if ($keys_case == CASE_UPPER) {
					if (!array_key_exists(strtoupper($key), $array)) {
						$valid = false;
					}
				} elseif ($keys_case == CASE_LOWER) {
					if (!array_key_exists(strtolower($key), $array)) {
						$valid = false;
					}
				} else {
					if (!array_key_exists($key, $array)) {
						$valid = false;
					}
				}
			}
		}
		return $valid;
	}

	public static function hasValue($value) 
	{
		return !(self::isEmpty($value));
	}

	public static function isOfAge($age, $legal = 18) 
	{
		return self::getAge($age) < $legal ? false : true;
	}

	public static function isAlpha($value, $allow = '') 
	{
		if (preg_match('/^[a-zA-Z' . $allow . ']+$/', $value)) {
			return true;
		} else {
			return false;
		}
	}

	public static function isAlphaNumeric($value) 
	{
		if (preg_match('/^[A-Za-z0-9 ]+$/', $value)) {
			return true;
		} else {
			return false;
		}
	}

	public static function isEmail($email) 
	{
		$pattern = '/^([a-zA-Z0-9])+([\.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-]+)+/';

		if (preg_match($pattern, $email)) {
			return true;
		} else {
			return false;
		}
	}

	public static function isEmpty($value) 
	{
		if (!isset($value)) {
			return true;
		} elseif (is_null($value)) {
			return true;
		} elseif (is_string($value) && strlen($value) == 0) {
			return true;
		} elseif (is_array($value) && count($value) == 0) {
			return true;
		} else {
			return false;
		}
	}

	public static function isFloat($number) 
	{
		if (is_float($number)) {
			return true;
		} else {
			$pattern = '/^[-+]?(((\\\\d+)\\\\.?(\\\\d+)?)|\\\\.\\\\d+)([eE]?[+-]?\\\\d+)?$/';
    		return (!is_bool($number) &&
				(is_float($number) || preg_match($pattern, trim($number))));
		}
	}

	public static function isInternetURL($value) 
	{
		if (preg_match('/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?$/i', $value)) {
			return true;
		} else {
			return false;
		}
	}

	public static function isNumber($number) 
	{
		if (preg_match('/^\-?\+?[0-9e1-9]+$/', $number)) {
			return true;
		} else {
			return false;
		}
	}

	public static function isTooLong($value, $maximumLength) 
	{
		if (strlen($value) > $maximumLength) {
			return true;
		} else {
			return false;
		}
	}

	public static function isTooShort($value, $minimumLength) 
	{
		if (strlen($value) < $minimumLength) {
			return true;
		} else {
			return false;
		}
	}

	public static function isValidCreditCardNumber($cardnumber) 
	{
		$number    = preg_replace('/[^0-9]/i', '', $cardnumber);
		$length    = strlen($number);
		$revNumber = strrev($number);

		// calculate checksum
		$sum       = '';
		for ($i = 0; $i < $length; $i++) {
			$sum .= $i & 1 ? $revNumber[$i] * 2 : $revNumber[$i];
		}

		return array_sum(str_split($sum)) % 10 === 0;
	}

	public static function isValidJSON($string) 
	{
		@json_decode($string);
		return (json_last_error() == JSON_ERROR_NONE);
	}

	public static function sanitize($input) 
	{
		$search = array(
			'@<script[^>]*?>.*?</script>@si',   // Strip out javascript
			'@<[\/\!]*?[^<>]*?>@si',            // Strip out HTML tags
			'@<style[^>]*?>.*?</style>@siU',    // Strip style tags properly
			'@<![\s\S]*?--[ \t\n\r]*>@'         // Strip multi-line comments
		);
		return preg_replace($search, '', $input);
	}

	public static function stripExcessWhitespace($string) 
	{
		return preg_replace('/  +/', ' ', $string);
	}

	public static function stripNonAlpha($string) 
	{
		return preg_replace('/[^a-z]/i', '', $string);
	}

	public static function stripNonAlphaHyphenSpaces($string) 
	{
		return preg_replace('/[^a-z\- ]/i', '', $string);
	}

	public static function stripNonAlphaNumeric($string) 
	{
		return preg_replace('/[^a-z0-9]/i', '', $string);
	}

	public static function stripNonAlphaNumericHyphenSpaces($string) 
	{
		return preg_replace('/[^a-z0-9\- ]/i', '', $string);
	}

	public static function stripNonAlphaNumericSpaces($string) 
	{
		return preg_replace('/[^a-z0-9 ]/i', '', $string);
	}

	public static function stripNonNumeric($string) 
	{
		return preg_replace('/[^0-9]/', '', $string);
	}

	public static function trim($value, $mask = ' ') 
	{
		if (is_string($value)) {
			return trim($value, $mask);
		} elseif (is_null($value)) {
			return '';
		} else {
			return $value;
		}
	}

	public static function truncate($string, $length, $dots = '') 
	{
		if (strlen($string) > $length) {
			return substr($string, 0, $length - strlen($dots)) . $dots;
		} else {
			return $string;
		}
	}

	public static function truncateDecimal($float, $precision = 0) 
	{
		$pow     = pow(10, $precision);
		$precise = (int) ($float * $pow);
		return (float) ($precise / $pow);
	}
}

We could also use some abstraction if the data we are going to validate needs to pass multiple checks. For example if we are going to validate password we could also add this:

public static function validatePassword($password, $confirm, $email = '', $username = '', $forceUpperLower = false) 
{

	$problem = '';

	if ($password != $confirm) {
		$problem .= 'Password and confirm password fields did not match.' . "<br>\n";
	}
	if (strlen($password) < 8) {
		$problem .= 'Password must be at least 8 characters long.' . "<br>\n";
	}
	if ($email) {
		if (strpos(strtoupper($password), strtoupper($email)) !== false
			|| strpos(strtoupper($password), strtoupper(strrev($email))) !== false) {
			$problem .= 'Password cannot contain the email address.' . "<br>\n";
		}
	}
	if ($username) {
		if (strpos(strtoupper($password), strtoupper($username)) !== false
			|| strpos(strtoupper($password), strtoupper(strrev($username))) !== false) {
			$problem .= 'Password cannot contain the username (or reversed username).' . "<br>\n";
		}
	}
	if (!preg_match('#[0-9]+#', $password)) {
		$problem .= 'Password must contain at least one number.' . "<br>\n";
	}
	if ($forceUpperLower) {
		if (!preg_match('#[a-z]+#', $password)) {
			$problem .= 'Password must contain at least one lowercase letter.' . "<br>\n";
		}
		if (!preg_match('#[A-Z]+#', $password)) {
			$problem .= 'Password must contain at least one uppercase letter.' . "<br>\n";
		}
	} else {
		if (!preg_match('#[a-zA-Z]+#', $password)) {
			$problem .= 'Password must contain at least one letter.' . "<br>\n";
		}
	}

	if (strlen($problem) == 0) {
		$problem = false;
	} else ($returnArray) {
		$problem = explode("<br>\n", trim($problem, "<br>\n"));
	}

	return $problem;
}

And that is all for this chapter. Soon we will integrate database support and so we can start create some more interesting examples, but before that we will need to secure our sessions a bit. So that is what we'll do in the next chapter.

Chapter 6 - Cookies and sessions

When we created CSRF helper in the last chapter we briefly mentioned that we'll need to tighten the security of our sessions. So now we will do just that. First we need to define three new variables in our config.php file

define('SESSION_PATH', ROOT_PATH . 'application/storage/sessions/');
define('SESSION_KEY', 'randomstringofcharacters');
define('SESSION_TTL', 60);

Now create a new file named "SecureSession.php" inside the /application/helpers/ folder.

<?php

namespace application\helpers;


use SessionHandler;

class SecureSession extends SessionHandler
{

    protected $key, $name, $cookie;

    public function __construct($key = SESSION_KEY, $name = 'SESSION')
    {
        $this->key = $key;
        $this->name = $name;
        if(!isset($_SESSION)) {
            $this->setup();
        }
    }
    
    protected function setup()
    {
		ini_set('session.use_cookies', 1);
		ini_set('session.use_only_cookies', 1);

        session_name($this->name);
    }
}

It's also a good idea to have a session encrypted so lets also add that

private function _encrypt($data, $key) {
    $encryption_key = base64_decode($key);
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
    if($iv===false) die('Fatal error: encryption was not successful - could not save data!!'); // weak encryption

    $encrypted = openssl_encrypt($data, 'aes-256-cbc', $encryption_key, 0, $iv);
    return base64_encode($encrypted . '::' . $iv);
}

private function _decrypt($data, $key) {
    $encryption_key = base64_decode($key);
    list($encrypted_data, $iv) = explode('::', base64_decode($data), 2);
    return openssl_decrypt($encrypted_data, 'aes-256-cbc', $encryption_key, 0, $iv);
}

//and abstract it
public function _read($id)
{
    return $this->_decrypt(parent::read($id), $this->key);
}

public function _write($id, $data)
{
    return parent::write($id, $this->_encrypt($data, $this->key));
}

Now we can add the ussual session functionality

public function start()
{
    if (session_id() === '') {
        session_start();
        return (mt_rand(0, 4) === 0) ? $this->refresh() : true; // 1/5
    }

    return false;
}

public function forget()
{
    if (session_id() === '') {
        return false;
    }

    $_SESSION = [];

    return session_destroy();
}

public function refresh()
{
    return session_regenerate_id(true);
}

We should also have some method for validating sessions

public function isExpired($ttl = SESSION_TTL)
{
    $activity = isset($_SESSION['_last_activity'])
        ? $_SESSION['_last_activity']
        : false;

    if ($activity !== false && time() - $activity > $ttl * 60) {
        return true;
    }

    $_SESSION['_last_activity'] = time();

    return false;
}

public function isFingerprint()
{
    $hash = md5(
        $_SERVER['HTTP_USER_AGENT'] .
        (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))
    );

    if (isset($_SESSION['_fingerprint'])) {
        return $_SESSION['_fingerprint'] === $hash;
    }

    $_SESSION['_fingerprint'] = $hash;

    return true;
}

//and abstarct it
public function isValid($ttl = SESSION_TTL)
{
    return ! $this->isExpired($ttl) && $this->isFingerprint();
}

And final two methods are for getting and setting values from the session

public function get($name)
{
    $parsed = explode('.', $name);

    $result = $_SESSION;

    while ($parsed) {
        $next = array_shift($parsed);

        if (isset($result[$next])) {
            $result = $result[$next];
        } else {
            return null;
        }
    }

    return $result;
}

public function put($name, $value)
{
    $parsed = explode('.', $name);

    $session =& $_SESSION;

    while (count($parsed) > 1) {
        $next = array_shift($parsed);

        if ( ! isset($session[$next]) || ! is_array($session[$next])) {
            $session[$next] = [];
        }

        $session =& $session[$next];
    }

    $session[array_shift($parsed)] = $value;
}

That's it we now have are secured session system. Now edit our CSRF helper to look like this:

<?php

namespace application\helpers;


use Request;

class CSRF
{
    public static $name = '_CSRF';

    public function __construct()
    {
        $this->session = new SecureSession();
        $this->session->start();
    }

    public function insert($form = 'default')
    {
        echo '<input type="hidden" name="csrf_token" value="' . $this->generate($form) . '">';
    }


    public function generate($form = NULL)
    {
        $token = self::token() . self::fingerprint();
        $this->session->put(self::$name . '_' . $form,  $token);
        return $token;
    }

    public function check($token, $form = NULL)
    {
        if ($this->session->get(self::$name . '_' . $form) && $this->session->get(self::$name . '_' . $form) === $token) { // token OK
            return (substr($token, -32) === self::fingerprint()); // fingerprint OK?
        }
        return FALSE;
    }

    protected static function token()
    {
        mt_srand((double) microtime() * 10000);
        $charid = strtoupper(md5(uniqid(rand(), TRUE)));
        return substr($charid, 0, 8) .
               substr($charid, 8, 4) .
               substr($charid, 12, 4) .
               substr($charid, 16, 4) .
               substr($charid, 20, 12);
    }

    protected static function fingerprint()
    {
        return strtoupper(md5(implode('|', array(Request::server('REMOTE_ADDR'), Request::server('HTTP_USER_AGENT')))));
    }
}

Now that we have done that we need to ensure that session will be started when website is opened. To do that we will need to create folder named "middleware" in /application/ directory and inside create the file "Persister.php"

<?php

namespace application\middleware;


use application\helpers\SecureSession;

class Persister
{
    public static $session;

    public static function run()
    {
        $session = new SecureSession(SESSION_KEY, 'PHPSESSID');

        ini_set('session.save_handler', 'files');
        session_set_save_handler($session, true);
        session_save_path(SESSION_PATH);

        $session->start();
    }
}

Now we need to register middleware in the core, so let's create Middleware.php inside /application/core/ and write this:

<?php

namespace application\core;


use application\middleware\Persister;

class Middleware
{
    public static function persist()
    {
        Persister::run();
    }
}

And finally we need to call th middleware in our root index.php file so change it's contents to the following

<?php

use application\core\Router;
use application\core\Middleware;

define('ROOT_PATH', __DIR__. '/');
require_once ROOT_PATH . 'application/config/config.php';

spl_autoload_register(function($class) {
    $path = str_replace('\\', '/', $class.'.php');
    if (file_exists($path)) {
        require $path;
    }
});

if(ENVIRONMENT === 'dev') {
	//debugger handler
}elseif(ENVIRONMENT === 'prod'){
    error_reporting(0);
    ini_set('display_errors', 0);
}else{
    die('Fatal error: environment not defined or valid!');
}

Middleware::persist(); 

$router = new Router;
$router->run();

That's it sessions are up and running. Next up are cookies so create /application/helper/Cookies.php

<?php

namespace application\helpers;


use Request;

class Cookies
{

    public $name;
    public $value = '';
    public $expires;
    public $path = '/';
    public $domain = '';
    public $secure = false;
    public $http_only = false;


    public function __construct($name, $value = null)
    {
        $this->name = $name;
        $this->domain = '.' .Request::server('SERVER_NAME');

        if (Request::server('HTTPS') && Request::server('HTTPS') !== 'off') {
            $this->secure = true;
        }

        if ($value !== null) {
            $this->value = $value;
        }
    }
}

As cookies have expiry date we can create some functions that will make it easier to configure

public function expires_in($time, $unit = "months")
{
    if (!empty($time)) {
        switch ($unit) {
            case 'months' :
                $time = $time * 60 * 60 * 24 * 31;
                break;
            case 'days'   :
                $time = $time * 60 * 60 * 24;
                break;
            case 'hours'  :
                $time = $time * 60 * 60;
                break;
            case 'minutes':
                $time *= 60;
                break;
        }
    }
    $this->expires_at($time);
}

public function expires_at($time)
{
    if (empty($time)) {
        $time = null;
    }
    $this->expires = $time;
}

And while we are abstarcting, we might as well add methods for reading and writing to a cookie

public function set()
{
    return setcookie(
        $this->name,
        $this->value,
        $this->expires,
        $this->path,
        $this->domain,
        $this->secure,
        $this->http_only
    );
}

public function get()
{
    return ($this->value === '' && isset($_COOKIE[$this->name])) ? $_COOKIE[$this->name] : $this->value;
}

public function delete()
{
    $this->value = '';
    $this->expires = time() - 3600;
    return $this->set();
}

And that's the whole cookie class. We can now add it to our middleware if we need. Actually let's do that. Open up the /application/middleware/Persister.php and edit it like this:

<?php

namespace application\middleware;


use application\helpers\SecureSession;
use application\helpers\Cookies;

class Persister
{
    public static $session;
    public static $cookies;

    public static function run()
    {
        $session = new SecureSession(SESSION_KEY, 'PHPSESSID');

        $cookies = new Cookies('SESSION_COOKIE');
        $cookies->expires = 0;
        $cookies->value = 'TRUE';
        $cookies->set();

        ini_set('session.save_handler', 'files');
        session_set_save_handler($session, true);
        session_save_path(SESSION_PATH);

        $session->start();
    }
}

So now we have session cookie stored on on browser of the user who visits our webpage.

Chapter 7 - Databases

Now we have finally come to the part where we get to play with the databases. For the course of the tutorial I will use PDO as an abstraction layer because we might not always want to use mysql as our database, and I want to do this as modular as possible. So first we want to add next part of code to our config.php

//DATABASES
define ('CONNECTIONS', [
    'DEFAULT' => [
        'DB_TYPE' => 'mysql',
        'DB_HOST' => '127.0.0.1',
        'DB_PORT' => '3306',
        'DB_NAME' => 'mvc',
        'DB_USER' => 'mvcuser',
        'DB_PASS' => 'mvcpassword',
        'DB_CHARSET' => 'utf8'
    ],
]

As you can see, we will be defining our database in a multidim array so if you ever find yourself in need of handling mltiple databases simultaneously just add them to the list.
Now that we have set up the defaults it's time for us to create our database handler, so create file "DB.php" inside of /application/helpers/

<?php

namespace application\helpers;


use PDO;

class DB extends PDO
{

    protected static $instances = array();

    public static function connect($connection = 'DEFAULT')
    {
    		//if database is not explicitly set use default (from config.php)
        ($connection === 'DEFAULT') ? $db = CONNECTIONS['DEFAULT'] : $db = CONNECTIONS[$connection];
        if($db === NULL) die("Connection '$connection' is not defined");

        $type = $db['DB_TYPE'];
        $host = $db['DB_HOST'];
        $name = $db['DB_NAME'];
        $user = $db['DB_USER'];
        $pass = $db['DB_PASS'];

        $id = "$type.$host.$name.$user.$pass";

		//don't create new instance if one is already running
		//if you need more, create new object 
        if (isset(self::$instances[$id])) {
            return self::$instances[$id];
        }

		//create a connection
        $instance = new DB("$type:host=$host;dbname=$name;charset=utf8", $user, $pass);
        $instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        self::$instances[$id] = $instance;

        return $instance;
    }
}

Next we are gonna make method for selecting data from the database

public function select($sql, $array = array(), $fetchMode = PDO::FETCH_OBJ, $class = '', $single = null)
{
    if (stripos($sql, 'select ') !== 0) {
        $sql = 'SELECT ' . $sql;
    }
    $stmt = $this->prepare($sql);
    foreach ($array as $key => $value) {
        if (is_int($value)) {
            $stmt->bindValue("$key", $value, PDO::PARAM_INT);
        } else {
            $stmt->bindValue("$key", $value);
        }
    }
    $stmt->execute();
    if ($single === null) {
        return $fetchMode === PDO::FETCH_CLASS ? $stmt->fetchAll($fetchMode, $class) : $stmt->fetchAll($fetchMode);
    }

    return $fetchMode === PDO::FETCH_CLASS ? $stmt->fetch($fetchMode, $class) : $stmt->fetch($fetchMode);
}

We can also abstract it by adding methods for specific selects like a

//select only single row
public function find($sql, $array = array(), $fetchMode = PDO::FETCH_OBJ, $class = '')
{
    return $this->select($sql, $array, $fetchMode, $class, true);
}

//select only row count
public function count($table, $column= 'id')
{
    $stmt = $this->prepare("SELECT $column FROM $table");
    $stmt->execute();
    return $stmt->rowCount();
}

Following this logic, insert method would look like this:

//table is table name
//data is an assoc array where keys are field names, and values are data we want to insert
public function insert($table, $data)
{
    ksort($data);
    $fieldNames = implode(',', array_keys($data));
    $fieldValues = ':'.implode(', :', array_keys($data));
    $stmt = $this->prepare("INSERT INTO $table ($fieldNames) VALUES ($fieldValues)");
    foreach ($data as $key => $value) {
        $stmt->bindValue(":$key", $value);
    }
    $stmt->execute();
    return $this->lastInsertId();
}

And with that we practically insured that we will always use prepared statements, and amount of code we would be writing is as minimized as it can be because of the abstraction. Let's do that for the update function as well.

public function update($table, $data, $where)
{
    ksort($data);
    $fieldDetails = null;
    foreach ($data as $key => $value) {
        $fieldDetails .= "$key = :$key,";
    }
    $fieldDetails = rtrim($fieldDetails, ',');
    $whereDetails = null;
    $i = 0;
    foreach ($where as $key => $value) {
        if ($i == 0) {
            $whereDetails .= "$key = :$key";
        } else {
            $whereDetails .= " AND $key = :$key";
        }
        $i++;
    }
    $whereDetails = ltrim($whereDetails, ' AND ');
    $stmt = $this->prepare("UPDATE $table SET $fieldDetails WHERE $whereDetails");
    foreach ($data as $key => $value) {
        $stmt->bindValue(":$key", $value);
    }
    foreach ($where as $key => $value) {
        $stmt->bindValue(":$key", $value);
    }
    $stmt->execute();
    return $stmt->rowCount();
}

Call parameters here are the same as with insert with the addition of where parameter, because we need to specify which row we wnat to update. For example, use of update function would look like this:

$data = array(
    'firstName' => 'Joe',
    'lastnName' => 'Smith',
    'email' => 'someone@domain.com'
);
$where = array('memberID' => 2);
$db->update('users', $data, $where);

Now delete() method is somewhat similar to the update() but instead of data parametere we have limit

public function delete($table, $where, $limit = 1)
{
    ksort($where);
    $whereDetails = null;
    $i = 0;
    foreach ($where as $key => $value) {
        if ($i == 0) {
            $whereDetails .= "$key = :$key";
        } else {
            $whereDetails .= " AND $key = :$key";
        }
        $i++;
    }
    $whereDetails = ltrim($whereDetails, ' AND ');
    if (is_numeric($limit)) {
        $uselimit = "LIMIT $limit";
    }
    $stmt = $this->prepare("DELETE FROM $table WHERE $whereDetails $uselimit");
    foreach ($where as $key => $value) {
        $stmt->bindValue(":$key", $value);
    }
    $stmt->execute();
    return $stmt->rowCount();
}

This should cover most use cases, but somtimes we will need a little more flexibility with generating statements. For those cases we can also add this method:

//only use this if you know what you are doing
public function raw($sql)
{
    return $this->query($sql);
}

Now we can integrate our DB helper with the rest of the framework by calling it in our core model class, so let's open /application/core/Model.php

<?php

namespace application\core;


use application\helpers\DB;
use application\helpers\Request;

abstract class Model 
{	
	protected $request;
	protected $db;

    protected static $_table = '';
    protected static $_primaryKey = '';

    protected $columns;

	public function __construct() 
	{
		$this->request = new Request;
        $this->db = DB::connect();

        $this->columns = [];
	}
}

Now it looks a little more like a legit model class. Now when you create new models by extending this class, you will automagically be connected to the database, you just need to specify base table and primary. Let's add basic crud functions to our core model as well.

protected function populate($object)
{
    foreach ($object as $key => $value) {
        $this->set($key, $value);
    }
}

public function set($column,$value)
{
    $this->columns[$column] = $value;
}

public function get($column)
{
    return $this->columns[$column];
}

public function create()
{
    return $this->db->insert(static::$_table, $this->columns);
}

public function update($where = NULL)
{

    return $this->db->update(static::$_table, $this->columns, $where);
}

public function save($where = NULL)
{
    if($where || $this->get(static::$_primaryKey) !== null) $this->db->update(static::$_table, $this->columns, ($where)?$where:[static::$_primaryKey=>$this->get(static::$_primaryKey)]);
    else $this->db->insert(static::$_table, $this->columns);
    return $this;
}

public static function delete($value){
    return DB::connect()->delete(static::$_table, [static::$_primaryKey => $value]);
}

public static function purge()
{
    return DB::connect()->truncate(static::$_table);
}

public static function getAll($condition=array(),$order=NULL,$startIndex=NULL,$count=NULL,$group=NULL){
    $query = "SELECT * FROM " . static::$_table;
    if(!empty($condition)){
        $query .= " WHERE ";
        foreach ($condition as $key => $value) {
            $query .= $key . "=:".$key." AND ";
        }
    }
    $query = rtrim($query,' AND ');
    if($group){
        $query .= " GROUP BY " . $group;
    }
    if($order){
        $query .= " ORDER BY " . $order;
    }
    if($startIndex !== NULL){
        $query .= " LIMIT " . $startIndex;
        if($count){
            $query .= "," . $count;
        }
    }
    foreach ($condition as $key => $value) {
        $condition[':'.$key] = $value;
        unset($condition[$key]);
    }

    return DB::connect()->select($query,$condition);
}

public static function findOne($value){

    $sql = "SELECT * FROM " . static::$_table . " WHERE " . static::$_primaryKey . " = :" . static::$_primaryKey;
    $params = [ ":" . static::$_primaryKey => $value];

    $result = DB::connect()->find($sql, $params);
    return $result;
}

public static function getCount(){
    return DB::connect()->count(static::$_table);
}

And now we have very flexible base model with database support.

Chapter 8 - Recap

We will use this chapter as a breather - to look back on what we have done and to talk about things we will do next.
If you have followed a tutorial until now, you folder structure probably looks somewhat like this:

root/
	index.php
	application/
		config/
			config.php
			routes.php
		controllers/
			MainController.php
		core/
			Controller.php
			Middleware.php
			Model.php
			Router.php
			View.php
		helpers/
			Cookies.php
			CSRF.php
			DB.php
			Request.php
			SecureSession.php
			Validation.php
		middleware/
			Persister.php
		models/
			Main.php
		storage/
			sessions/
	public/
		components/
		errors/
			400.php
			404.php
			500.php
		layouts/
			default.php
		views/
			main/
				index.php

We started somewhat slow, with just the bare bones of the framework, but over the course of the tutorial we were addeing to that and little by little, our framework became more and more.. well, usable. We added db support, tighten our seurity, abstract the system for session and cookies and we even have a validation system.

So the next part of the tutorial will focus on file handling, and localization, and when when we are done with that we are going to build our own templating engine for faster frontend developing.

Chapter 9 - File handling

If you had ever code forms for file uploads you probably know how unintuitive native file handling in php is. But, whether you want it or not, it's one of the essential features of every web app, so we will try to make the process of manipulating files as easy as possible.
First, let's create new helper named "File.php".

<?php

namespace application\helpers;


class File 
{
	//initialize varibles we'll need 
    public $the_file;
    public $the_temp_file;
    public $validate_mime = true; 
    public $upload_dir;
    public $replace;
    public $do_filename_check;
    public $max_length_filename = 100;
    public $extensions;
    public $valid_mime_types = [
        '.bmp'  => 'image/bmp', 
        '.gif'  => 'image/gif', 
        '.jpg'  => 'image/jpeg', 
        '.jpeg' => 'image/jpeg', 
        '.pdf'  => 'application/pdf', 
        '.png'  => 'image/png', 
        '.zip'  => 'application/zip'
    ]; 
    public $ext_string;
    public $http_error;
    public $rename_file;
    public $file_copy; 
    public $message = [];
    public $create_directory = true;

	//set default permissions
    protected $fileperm = 0644;
    protected $dirperm = 0755;

	protected function error_text($err_num) 
	{
	    // externalize messages
	    $error[0] = 'File: <b>'.$this->the_file.'</b> successfully uploaded!';
	    $error[1] = 'The uploaded file exceeds the max. upload filesize directive in the server configuration.';
	    $error[2] = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the html form.';
	    $error[3] = 'The uploaded file was only partially uploaded';
	    $error[4] = 'No file was uploaded';
	    $error[6] = 'Missing a temporary folder. ';
	    $error[7] = 'Failed to write file to disk. ';
	    $error[8] = 'A PHP extension stopped the file upload. ';
	    // end  http errors
	    $error[10] = 'Please select a file for upload.';
	    $error[11] = 'Only files with the following extensions are allowed: <b>'.$this->ext_string.'</b>';
	    $error[12] = 'Sorry, the filename contains invalid characters. Use only alphanumerical chars and separate parts of the name (if needed) with an underscore. <br>A valid filename ends with one dot followed by the extension.';
	    $error[13] = 'The filename exceeds the maximum length of '.$this->max_length_filename.' characters.';
	    $error[14] = 'Sorry, the upload directory does not exist!';
	    $error[15] = 'Uploading <b>'.$this->the_file.'...Error!</b> Sorry, a file with this name already exitst.';
	    $error[16] = 'The uploaded file is renamed to <b>'.$this->file_copy.'</b>.';
	    $error[17] = 'The file %s does not exist.';
	    $error[18] = 'The file type (MIME type) is not valid.'; 
	    $error[19] = 'The MIME type check is enabled, but is not supported.'; 
	    
	    return $error[$err_num];
	}

	public function file_upload() 
    {
        $this->rename_file = false;
        $this->ext_string = '';
    } 
    
    public function show_error_string($br = '<br />') 
    {
        $msg_string = '';
        foreach ($this->message as $value) {
            $msg_string .= $value.$br;
        }
        return $msg_string;
    }
}

First, let's add some basic functions to make our life easier later

public function get_extension($from_file) 
{
    $ext = strtolower(strrchr($from_file,'.'));
    return $ext;
}

public function show_extensions() 
{
    $this->ext_string = implode(' ', $this->extensions);
}

public function set_file_name($new_name = '') 
{ 
    if ($this->rename_file) {
        if ($this->the_file == '') return;
        $name = ($new_name == '') ? strtotime('now') : $new_name;
        sleep(3);
        $name = $name.$this->get_extension($this->the_file);
    } else {
        $name = str_replace(' ', '_', $this->the_file);
    }
    return $name;
}

We will also need something that will check the existence of the file

public function check_dir($directory) 
{
    if (!is_dir($directory)) {
        if ($this->create_directory) {
            umask(0);
            mkdir($directory, $this->dirperm);
            return true;
        } else {
            return false;
        }
    } else {
        return true;
    }
}

public function existing_file($file_name) 
{
    if ($this->replace == 'y') {
        return true;
    } else {
        if (file_exists($this->upload_dir.$file_name)) {
            return false;
        } else {
            return true;
        }
    }
}

public function move_upload($tmp_file, $new_file) 
{
    if ($this->existing_file($new_file)) {
        $newfile = $this->upload_dir.$new_file;
        if ($this->check_dir($this->upload_dir)) {
            if (move_uploaded_file($tmp_file, $newfile)) {
                umask(0);
                chmod($newfile , $this->fileperm);
                return true;
            } else {
                $this->message[] = $this->error_text(7); 
                return false;
            }
        } else {
            $this->message[] = $this->error_text(14);
            return false;
        }
    } else {
        $this->message[] = $this->error_text(15);
        return false;
    }
}

When we establish that, we might also need to inspect the file to see what are we working with.

public function get_mime_type($file) 
{
    $mtype = false;
    if (function_exists('finfo_open')) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mtype = finfo_file($finfo, $file);
        finfo_close($finfo);
    } elseif (function_exists('mime_content_type')) {
        $mtype = mime_content_type($file);
    } 
    return $mtype;
}
    
public function get_uploaded_file_info($name) 
{
    $str = 'File name: '.basename($name).PHP_EOL;
    $str .= 'File size: '.filesize($name).' bytes'.PHP_EOL;
    if ($mimetype = get_mime_type($name)) {
        $str .= 'Mime type: '.$mimetype.PHP_EOL;
    }
    if ($img_dim = getimagesize($name)) {
        $str .= 'Image dimensions: x = '.$img_dim[0].'px, y = '.$img_dim[1].'px'.PHP_EOL;
    }
    return $str;
}

Now we can code our validators

public function validateMimeType($mime_type) 
{
    $ext = $this->get_extension($this->the_file);
    if ($mime_type == $this->valid_mime_types[$ext]) {
        return true;
    } else {
        $this->message[] = $this->error_text(18);
        return false;
    }
}

public function validateExtension() 
{
    $extension = $this->get_extension($this->the_file);
    $ext_array = $this->extensions;
    if (in_array($extension, $ext_array)) {
        if ($this->validate_mime) {
            if ($mime_type = $this->get_mime_type($this->the_temp_file)) {
                if ($this->validateMimeType($mime_type)) {
                    return true;
                } else {
                    return false;
                }
            } else {
                $this->message[] = $this->error_text(18);
                return false;
            }
        } else {
            return true;
        }
    } else {
        return false;
    }
}

public function check_file_name($the_name) 
{
    if ($the_name != '') {
        if (strlen($the_name) > $this->max_length_filename) {
            $this->message[] = $this->error_text(13);
            return false;
        } else {
            if ($this->do_filename_check == 'y') {
                if (preg_match('/^([a-z0-9_\-]*\.?)\.[a-z0-9]{1,5}$/i', $the_name)) { 
                    return true;
                } else {
                    $this->message[] = $this->error_text(12);
                    return false;
                }
            } else {
                return true;
            }
        }
    } else {
        $this->message[] = $this->error_text(10);
        return false;
    }
}

Now that we've checked everything we can, provided no errors show up, we can commence uploading

public function upload($to_name = '') 
{
    if ($this->http_error > 0) {
        $this->message[] = $this->error_text($this->http_error);
        return false;
    } else {
        $new_name = $this->set_file_name($to_name);
        if ($this->check_file_name($new_name)) {
            if ($this->validateExtension($this->the_temp_file)) {
                if (is_uploaded_file($this->the_temp_file)) {
                    $this->file_copy = $new_name;
                    if ($this->move_upload($this->the_temp_file, $this->file_copy)) {
                        $this->message[] = $this->error_text(0);
                        if ($this->rename_file) $this->message[] = $this->error_text(16);
                        return true;
                    }
                } else {
                    $this->message[] = $this->error_text(7); 
                    return false;
                }
            } else {
                $this->show_extensions();
                $this->message[] = $this->error_text(11);
                return false;
            }
        } else {
            return false;
        }
    }
}

We will also need a method for deleting temp files so let's make that

public function del_temp_file($file)
{
    $delete = @unlink($file); 
    clearstatcache();
    if (@file_exists($file)) { 
        $filesys = eregi_replace('/','\\',$file); 
        $delete = @system('del $filesys');
        clearstatcache();
        if (@file_exists($file)) { 
            $delete = @chmod ($file, 0644); 
            $delete = @unlink($file); 
            $delete = @system('del $filesys');
        }
    }
}

With that our file handling helper is finished. But it only covers validation and upload part of the handling. Which, strictly speaking should be enough in most cases, but if we are going to upload images, it would be a good idea to cover some functionality specific to them. So create another helper "Image.php".

<?php

namespace application\helpers;


use application\helpers\File;

class Image extends File 
{
    public $x_size;
    public $y_size;
    public $x_max_size = 300;
    public $y_max_size = 200;
    public $x_max_thumb_size = 110;
    public $y_max_thumb_size = 88;
    public $thumb_folder;
    public $foto_folder;
    public $larger_dim;
    public $larger_curr_value;
    public $larger_dim_value;
    public $larger_dim_thumb_value;
    
    private $use_image_magick = false; 

}

Seeing that we are talking about images, we should really add some methods for checking their dimensions.

public function get_img_size($file) 
{
    $img_size = getimagesize($file);
    $this->x_size = $img_size[0];
    $this->y_size = $img_size[1];
}

public function check_dimensions($filename) 
{
    $this->get_img_size($filename);
    $x_check = $this->x_size - $this->x_max_size;
    $y_check = $this->y_size - $this->y_max_size;

    if ($x_check < $y_check) {
        $this->larger_dim = "y";
        $this->larger_curr_value = $this->y_size;
        $this->larger_dim_value = $this->y_max_size;
        $this->larger_dim_thumb_value = $this->y_max_thumb_size;
    } else {
        $this->larger_dim = "x";
        $this->larger_curr_value = $this->x_size;
        $this->larger_dim_value = $this->x_max_size;
        $this->larger_dim_thumb_value = $this->x_max_thumb_size;
    }
}

With the diminesion checker coded, we can now write the method for image processing

public function process_image($landscape_only = false, $create_thumb = false, $delete_tmp_file = false, $compression = 85) 
{
    $filename = $this->upload_dir.$this->file_copy;
    
    $this->check_dir($this->thumb_folder); 
    $this->check_dir($this->foto_folder); 
    
    $thumb = $this->thumb_folder.$this->file_copy;
    $foto = $this->foto_folder.$this->file_copy;
    
    if ($landscape_only) {
        $this->get_img_size($filename);
        if ($this->y_size > $this->x_size) {
            $this->img_rotate($filename, $compression);
        }
    }

    $this->check_dimensions($filename); 
    
    if ($this->larger_curr_value > $this->larger_dim_value) {
        $this->thumbs($filename, $foto, $this->larger_dim_value, $compression);
    } else {
        copy($filename, $foto);
    }
    
    if ($create_thumb) {
        if ($this->larger_curr_value > $this->larger_dim_thumb_value) {
            $this->thumbs($filename, $thumb, $this->larger_dim_thumb_value, $compression); 
        } else {
            copy($filename, $thumb);
        }
    }
    
    if ($delete_tmp_file) $this->del_temp_file($filename); 
}

Another useful method would be a function for making a thumbnail out of images. Just in case we need to render a huge amount of images on a single page.

public function thumbs($file_name_src, $file_name_dest, $target_size, $quality = 80) 
{
    $size = getimagesize($file_name_src);

    if ($this->larger_dim == "x") {
        $w = number_format($target_size, 0, ',', '');
        $h = number_format(($size[1]/$size[0])*$target_size,0,',','');
    } else {
        $h = number_format($target_size, 0, ',', '');
        $w = number_format(($size[0]/$size[1])*$target_size,0,',','');
    }
    
    if ($this->use_image_magick) {
        exec(sprintf("convert %s -resize %dx%d -quality %d %s", $file_name_src, $w, $h, $quality, $file_name_dest));
    } else {
        $dest = imagecreatetruecolor($w, $h);
        imageantialias($dest, TRUE);
        $src = imagecreatefromjpeg($file_name_src);
        imagecopyresampled($dest, $src, 0, 0, 0, 0, $w, $h, $size[0], $size[1]);
        imagejpeg($dest, $file_name_dest, $quality);
    }
}

But we can also add some less useful methods like the next one, which will rotate it.

public function img_rotate($wr_file, $comp) 
{
    $new_x = $this->y_size;
    $new_y = $this->x_size;

    if ($this->use_image_magick) {
        exec(sprintf("mogrify -rotate 90 -quality %d %s", $comp, $wr_file));
    } else {
        $src_img = imagecreatefromjpeg($wr_file);
        $rot_img = imagerotate($src_img, 90, 0);
        $new_img = imagecreatetruecolor($new_x, $new_y);
        imageantialias($new_img, TRUE);
        imagecopyresampled($new_img, $rot_img, 0, 0, 0, 0, $new_x, $new_y, $new_x, $new_y);
        imagejpeg($new_img, $this->upload_dir.$this->file_copy, $comp);
    }
}

So, now we have a complete file handling system ready to go.

Chapter 10 - Localization

TBD

Chapter 11 - Templating

TBD

Second App

TBD

Emails

TBD