My Profile Photo

MSc. Alban Afmeti


Information Technology is my passion, especially software development. I am trying to grow professionally every day with hard work, with practice and online courses. My objective is clear. Everything is possible if we devote to that. All the persons with a higher position than me are there because the life has bring to them some opportunities, not because they are smarter than me. Let's find these opportunities and let's thank the god that we have a good healthy life.


How to build a PHP framework!

Usage of frameworks like Laravel, Symfony etc. makes us ( who love PHP ) start thinking and asking ourselves: Could we implement our own framework? In this article we are going to create our simple framework and understanding how can we build a framework with MVC design pattern using PHP.

Micro Framework

Introduction

Why would you like to create your own framework?

If you look around, everybody will tell you that it’s a bad thing to reinvent the wheel and that you’d better choose an existing framework and forget about creating your own. But anyhow there are a few good reasons to start creating your own framework:

  1. To create a framework tailored to your very specific needs
  2. To experiment creating a framework for fun
  3. To prove the world that you can actually create a framework on your own
  4. To get a good experience in PHP

MVC architecture

The Model-View-Controller (MVC) is an architectural pattern that separates an application into three main logical components: the model, the view, and the controller. Each of these components are built to handle specific development aspects of an application. The Model component corresponds to all the data related logic that the user works with. The View component is used for all the UI logic of the application. Controllers act as an interface between Model and View components to process all the business logic and incoming requests, manipulate data using the Model component and interact with the Views to render the final output. For more on MVC design pattern read this article at SitePoint: The MVC design pattern and PHP. We are going to use MVC design pattern to build our framework.

Our Project

Instead of creating everything in our framework from scratch, we are going to use some external packages. To install these we are going to use Composer, a project dependency manager used by modern PHP applications. If you don’t have it yet, download and install Composer now.

To continuously we are going to discuss the main components and features that our framework will have.

  1. Bootstraping. “bootstrap PHP code” means creating a bootstrapper that handles all the dynamic requests coming to a server and apply the true MVC (Model View Controller) framework so that in future you can change the functionality for each unique controller or application without changing the entire code or application.

  2. Routing.
    To understand what a router does, you must first understand what a rewrite engine is.
    A rewrite engine is software that modifies a web URL’s appearance (URL rewriting). Rewritten URLs (sometimes known as short, fancy URLs, or search engine friendly - SEF) are used to provide shorter and more relevant-looking links to web pages. The technique adds a degree of separation between the files used to generate a web page and the URL that is presented to the World.
    If you want rewritten URLs, you need some kind of routing, as routing is the process of taking the URL, braking it into components and deciding what is the actual script to call.
    Routing is the process of taking a URI endpoint (that part of the URI which comes after the base URL) and decomposing it into parameters to determine which module, controller, and action of that controller should receive the request.

  3. Filters. Before executing an action of the controller, some filters can be applied to a route, which are functions to be executed before the action. In this filters we can make different validation and checks e.g. if the user is not logged in redirect to login page.

  4. Modules. It’s a widget based feature, which helps us separating different parts of an application in classes with a special function, that will be executed in a specific part of a layout e.g. a widget showing the weather.
  5. For Database functions we are going to use a powerful ORM currently in the web, used by Laravel framework, Eloquent
  6. Regarding to Views we are going to use a powerful templating engine used by Laravel too, Blade.

The directory structure of our project is going to be like this:

/
+-- app
    +-- controllers
    +-- exceptions
    +-- libs
    +-- models
    +-- modules
+-- bootstrap
+-- config
+-- public
+-- resources
    +-- langs
    +-- views
+-- storage
    +-- cache
    +-- logs
+-- vendor
  • Inside the controllers directory are going to stay the controllers classes that will extend a parent class called Controller located under /app/libs.
  • Inside the libs directory are going to be different classes used to make the framework functional.
  • The exceptions directory is going to contain different exceptions classes extending the Exception class.
  • Inside the models directory will be the models classes of the framework that are going to extend an Eloquent class.
  • modules directory will contain some classes serving for creation of different widgets in the UI while creating for example an website.
  • bootstrap directory will contain a file called FrontController.php which serves like an engine and is going to make all the bootstraping part of the framework
  • We don’t have to do with cache directory because it’s necessary only for Blade templating engine.
  • logs directory it’s going to contain log files for different application errors when it is in production mode
  • The public directory is accessible to all, and here is going to live the index.php file together with .htaccess, an Apache config file. In the public directory will stay all the public stuff like js, css and images etc.

  • In the langs directory are going to be some .ini files which are key = value pairs necessary for the languages of the framework.
  • In the views directory are going to stay all blade view files and layouts
  • cache is the directory where will be compiled files from Blade templating engine which we are going to talk later for. The logs folder may contain different log files.
  • The last directory vendor doesn’t have to do with us, because it’s necessary for downloaded packages and libraries via Composer.

Bootstraping

I have chosen Apache HTTP Server, like the web server of our hosting environment. Apache is the world’s most used web server software, a freely available Web server that is distributed under an “open source” license. .htaccess file is a configuration file for the apache. We have said that public directory is the accessible one from all, so .htaccess file is going to stay here. .htaccess files (or “distributed configuration files”) provide a way to make configuration changes on a per-directory basis. A file, containing one or more configuration directives, is placed in a particular document directory, and the directives apply to that directory, and all subdirectories thereof.

Its content is as follows:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-l

RewriteRule ^(.+)$ index.php?req=$1 [QSA,L]

RewriteEngine is On, so we are able to change Apache rules. We have write 3 conditions and a rule. This means: if the requested url is not a real file, and isn’t a directory, and isn’t a symlink, then redirect to index.php. After this all the path passes like an url query parameter.
For example if the request is like this:
http://www.domain.com/path1/path2/path3
It’s converted to this:
http://www.domain.com/index.php?req=path1/path2/path3

We have mentioned above that we are going to use some external packages. To install these we are going to use Composer and to know more about that you can see the documentation from the provided link. We will use composer to make some file autoloading too which we are going to explain as follows.

The packages we are going to install are these:

  1. Eloquent ORM
  2. Blade templating engine
  3. Symfony Http foundation package

Inside the file composer.json (which was created after doing composer init from the terminal), in the require section write the following packages:

"require": {
    "philo/laravel-blade": "3.*",
    "illuminate/database": "^5.3",
    "symfony/http-foundation": "^3.2"
}

Run from terminal the command composer install and the packages will be installed.
To autoload some files from some of our directories we use Composer too.
Inside the file composer.json, if there isn’t one add an autoload section and write the following:

"autoload": {
    "psr-4": {
        "App\\Libs\\": "app/libs",
        "App\\Models\\": "app/models",
        "App\\Exceptions\\": "app/exceptions",
        "App\\Modules\\": "app/modules"
    }
}

Run in the terminal the command composer dump-autoload.

Now that we have made some configuration let’s explain the index.php file :

<?php

define('DS', DIRECTORY_SEPARATOR);
define('DOCROOT', dirname(dirname(__FILE__)) . DS);

require DOCROOT . 'config' . DS . 'definitions.php';
setReporting();

require DOCROOT . 'vendor' . DS . 'autoload.php';
require DOCROOT . BOOTSTRAP_PATH . "FrontController.php";

$app = new Bootstrap\FrontController();
$app->bootstrap();

Initially we have defined two constants, DOCROOT which is the absolute path to the root directory of our project, and DS that’s the directory separator, declared because different operating systems may have different directory separators.

After this we require the definitions.php file which have some important constants that we will use through all the project:

<?php

define('WEBROOT', '/');
define('DEVELOPMENT_MODE', true);

define('APP_PATH', 'app' . DS);
define('CONFIG_PATH', 'config' . DS);
define('BOOTSTRAP_PATH', 'bootstrap' . DS);
define('CONTROLLERS_PATH', 'app' . DS . 'controllers' . DS);
define('MODULES_PATH', 'app' . DS . 'modules' . DS);
define('MODELS_PATH', 'app' . DS . 'models' . DS);
define('LIBS_PATH', 'app' . DS . 'libs' . DS);
define('EXCEPTIONS_PATH', 'app' . DS . 'exceptions' . DS);
define('VIEWS_PATH', 'resources' . DS . 'views' . DS);
define('LANGS_PATH', 'resources' . DS . 'langs' . DS);
define('CACHE_PATH', 'storage' . DS . 'cache' . DS);
define('LOGS_PATH', 'storage' . DS . 'logs' . DS);

function setReporting() {
    if (DEVELOPMENT_MODE == true) {
        error_reporting(E_ALL);
        ini_set('display_errors','On');
    } else {
        error_reporting(E_ALL);
        ini_set('display_errors','Off');
        ini_set('log_errors', 'On');
        ini_set('error_log', DOCROOT . LOGS_PATH . 'error.log');
    }
}

We have declared here the function setReporting() which serves to separate the development mode from production mode, so in the dev. mode the errors are shown in the screen unlike the production mode where we use a log file.

In the index.php file, after the inclusion of the definitions file, we call the function setReporting();.
Include the Composer autoloading file located under vendor directory, and FrontController.php file located under bootstrap directory.

Create an object of FrontController class and call the method bootstrap().

FrontController

<?php

namespace Bootstrap;

use App\Exceptions\HttpNotFoundException;
use App\Libs\Session;
use App\Libs\Request;
use App\Libs\Settings;
use App\Libs\Router;
use App\Libs\Loader;
use App\Libs\View;
use App\Exceptions\ControllerNotFoundException;
use Exception;

class FrontController
{
    private $controller;
    private $action;
    private $parameters = [];
    private $lang = "en";

    private $route;
    private $request;

    function __construct()
    {
        Session::start();
    }

    public function bootstrap()
    {
        $this->request = Request::createFromGlobals();

        try {
            include DOCROOT . APP_PATH . "filters.php";
            include DOCROOT . APP_PATH . "routes.php";
            $this->route();
            include DOCROOT . CONFIG_PATH . "globals.php";
            include DOCROOT . APP_PATH . "helpers.php";
            $this->dispatch();

        } catch (Exception $ex) {
            View::create("errors/error")->set("message", $ex->getMessage())->render();
            exit;
        }
    }

    public function route()
    {
        $arrPath = self::URItoArray(ltrim($this->request->getPathInfo(), "/"));

        //Check the language. We don't consider it part of the route
        if (is_numeric(array_search($arrPath[0], Settings::$availableLangs))) {
            $this->lang = $arrPath[0];

            array_shift($arrPath);
            if (empty($arrPath)) {
                array_push($arrPath, "");
            }
        }

        $this->ProcessRequest($arrPath);
        return;
    }

    public function dispatch()
    {

        Loader::configureORM();

        //Load the Controller
        if ($this->isController($this->controller)) {

            //Here we run the filters that are registered to the route
            Router::runFilters($this->route);

            $filename = DOCROOT . CONTROLLERS_PATH . $this->controller . '.php';
            require_once $filename;
            $cName = $this->controller;
            $controller = new $cName();
            $controller->setLang($this->lang);

            //Execute Pre Action
            $controller->before_action($this->action, $this->parameters);

            //Execute Modules
            foreach ($this->route->modules as $module) {
                $module->execute(Request::createFromGlobals());
            }

            //Execute Action
            Loader::runController($controller, $this->action, $this->parameters);

        } else {
            throw new ControllerNotFoundException("Controller $this->controller not found!");
        }
        return;
    }

    private function ProcessRequest($arrPath)
    {
        $this->route = Router::findRoute($arrPath, $this->request->getMethod());
        if (!is_null($this->route)) {
            $this->controller = $this->route->controller;
            $this->action = $this->route->action;
            $this->parameters = $this->route->parameters;

            Request::setRoute($this->route);
            Request::setController($this->controller);
            Request::setAction($this->action);
            Request::setParameters($this->parameters);
            Request::setLang($this->lang);
        } else {
            throw new HttpNotFoundException("404 - Http request not found", 404);
        }
    }

    private function isController($name)
    {
        if (file_exists(DOCROOT . CONTROLLERS_PATH . $name . '.php')) {
            return true;
        } else {
            return false;
        }
    }

    private static function URItoArray($req = "")
    {
        $req = rtrim($req, '/');
        $req = urldecode($req);
        $req = explode('/', $req);
        return $req;
    }
}


The properties of this class are shown below:

<?php

private $controller;
private $action;
private $parameters = [];
private $lang = "en";

private $route;
private $request;

The $controller property will contain the name of the current controller got from the request, $action the name of the current action, $parameters an array of the current parameters got from the request if there are. $lang contains the current language of the site got from the URL. The last two properties are of type object, where $route is an object of type Route, and $request is an object of type Request we are going to explain later.

Inside the cunstructor, start the session calling a method of Session class created for different session operations like setting a value, clearing a value etc.

Inside of the bootstrap method, initialize the $request property with the value got from Request::createFromGlobals(). This line initializes the request, getting all the values of PHP global variables like $_GET, $_POST, $_SERVER etc.

Inside a try,catch block declare:

<?php

include DOCROOT . APP_PATH . "filters.php";
include DOCROOT . APP_PATH . "routes.php";
$this->route();
include DOCROOT . CONFIG_PATH . "globals.php";
include DOCROOT . APP_PATH . "helpers.php";
$this->dispatch();

We include files filters.php and routes.php located to app directory which we are going to talk later for. In filters.php will be declared all the filters (functions) that are going to be applied to routes, and in routes.php are going to be declared all the routes of the application.
Then we have called the route() function which is going to determine the values of the controller, action, parameters, and the lang properties.

Next, we include two other files globals.php and helpers.php. globals.php file contains different functions which are going to be used globally. The same thing is for helpers.php, but this file is empty and can be used by anyone for customized functions. I mean globals.php is a core application file, and helpers.php can be used by anyone to create your own functions.
Then we call the dispatch() function which is going to create the controller object, call the action and execute the modules.
If there is thrown an exception we are going to render a view with showing the message of the exception. This is made by the line View::create("errors/error")->set("message", $ex->getMessage())->render(), where View::create("errors/error") creates a View class object with the source of blade file to /app/resources/views/errors directory. The ->set("message", $ex->getMessage()) function binds a variable (with name $message) to the view where can be used. ->render() renders the view to the UI.

->route() function
<?php

$arrPath = self::URItoArray(ltrim($this->request->getPathInfo(), "/"));

//Check the language. We don't consider it part of the route
if (is_numeric(array_search($arrPath[0], Settings::$availableLangs))) {
    $this->lang = $arrPath[0];

    array_shift($arrPath);
    if (empty($arrPath)) {
        array_push($arrPath, "");
    }
}

$this->ProcessRequest($arrPath);
return;

Firstly we convert the path to an array. We get the path with the method $this->request->getPathInfo() and pass it to a static method URItoArray() which makes the conversion from path1/path2/path3 to an array ['path1', 'path2', 'path3']. Then we check if the first element of the array is a string representing the language of the app.
For example if the path was /en/path1/path2, the array will be ['en', 'path1', 'path2'] and so first element of the array will be a string that represents the language of the app.
We see a class called Settings which we can save different static values in, and so we have done with the available languages of the app. There is a member Settings::$availableLangs which represents an array with the available languages.
We use the method array_search to check if the language from the URL was found inside that array, if yes we assume it as a language otherwise no. After getting the language we make an array_shift to remove the language from the route because we don’t assume the language part of the route.

The we call $this->ProcessRequest($arrPath) which is a private function where we process the URL path.

<?php

$this->route = Router::findRoute($arrPath, $this->request->getMethod());
if (!is_null($this->route)) {
    $this->controller = $this->route->controller;
    $this->action = $this->route->action;
    $this->parameters = $this->route->parameters;

    Request::setRoute($this->route);
    Request::setController($this->controller);
    Request::setAction($this->action);
    Request::setParameters($this->parameters);
    Request::setLang($this->lang);
} else {
    throw new HttpNotFoundException("404 - Http request not found", 404);
}

In the above method we check from available routes if there is a route that corresponds to the current requested. We do that calling the static method:
Router::findRoute($arrPath, $this->request->getMethod()) where we pass the $arrPath and current Http method like parameters.
If it finds a route from available ones it will return it, otherwise it will throw an HttpNotFoundException. We are going to talk later for findRoute() method.

If the route object was found, it will be returned and different FrontController and Request properties will be initialized.
For example:

<?php

$this->controller = $this->route->controller;
$this->action = $this->route->action;
$this->parameters = $this->route->parameters;

It gets the controller value from the route object and passes it to controller property of FrontController and so on with action and parameters.

->dispatch() function
<?php

Loader::configureORM();

//Load the Controller
if ($this->isController($this->controller)) {

    //Here we run the filters that are registered to the route
    Router::runFilters($this->route);

    $filename = DOCROOT . CONTROLLERS_PATH . $this->controller . '.php';
    require_once $filename;
    $cName = $this->controller;
    $controller = new $cName();
    $controller->setLang($this->lang);

    //Execute Pre Action
    $controller->before_action($this->action, $this->parameters);

    //Execute Modules
    foreach ($this->route->modules as $module) {
        $module->execute(Request::createFromGlobals());
    }

    //Execute Action
    Loader::runController($controller, $this->action, $this->parameters);

} else {
    throw new ControllerNotFoundException("Controller $this->controller not found!");
}

We have mentioned above that for the database functions we are going to use Eloquent. Before starting to use it we need to configure it so we have called a method: Loader::configureORM() which makes the configuration. Below is shown its content:

<?php

$capsule = new Capsule;
$capsule->addConnection(config("db")['mysql']);
$capsule->setAsGlobal();
$capsule->bootEloquent();

We have created an object of Capsule class. Then, we have called addConnection() method which adds the connection settings to the Capsule object. We have passed an argument to it, a global function config("db")['mysql'] which returns an array from file /app/config/database.php and gets the configuration for mysql connection.

Then, if the controller’s file exists, before running the action we run all the filter functions related with the current route Router::runFilters($this->route).
Next, an object of the controller is created and the method ->setLang() ic called to bind the language to the controller.
Then, we see this method $controller->before_action($this->action, $this->parameters) called.
This method doesn’t execute the action directly but executes a function in the controller called preload_ + the name of the action.

For example, if the current action is store(), with the before_action we execute a function in the controller called preload_store(), but only if it exist. If it doesn’t exist it ignores that line. Why do we need that? We need that because we will see that in ->dispatch() method the order of execution is like this:

  1. Firstly the filters are executed
  2. Then the before_action
  3. Then are executed the modules related to the current route
  4. Lastly is executed the action method.

Sometimes before executing the modules we need some information from the action to use in modules, so we execute the before_action to pass that information to the modules. Then the action will be executed. This is the main reason that before executing the modules directly we executed a before_action method.

<?php

foreach ($this->route->modules as $module) {
	$module->execute(Request::createFromGlobals());
}

In this part of code all the modules related to the current route are going to be executed calling their special method ->execute(), which we pass a Request object in. We are going to explain how modules work later.

Then the requested action will be executed calling Loader::runController($controller, $this->action, $this->parameters) which is a method that uses Reflection classes to call the specific action, passing the route parameters.

Request

We have used often in the FrontController.php the class Request:

<?php

namespace App\Libs;

use Symfony\Component\HttpFoundation\Request as SymfonyRequest;

class Request extends SymfonyRequest
{
    private static $controller;
    private static $action;
    private static $parameters;
    private static $lang;
    private static $route;

    public static function getPath()
    {
        return self::createFromGlobals()->getPathInfo();
    }

    public static function getController()
    {
        return self::$controller;
    }

    public static function getAction()
    {
        return self::$action;
    }

    public static function getParameters()
    {
        return self::$parameters;
    }

    public static function getLang()
    {
        return self::$lang;
    }

    public static function setController($controller)
    {
        self::$controller = $controller;
    }

    public static function setAction($action)
    {
        self::$action = $action;
    }

    public static function setParameters($parameters)
    {
        self::$parameters = $parameters;
    }

    public static function setLang($lang)
    {
        self::$lang = $lang;
    }

    public static function setRoute(Route $route)
    {
        self::$route = $route;
    }

    public static function getRoute()
    {
        return self::$route;
    }

    public static function clear($key)
    {
        if (isset($_REQUEST[$key])) {
            unset($_REQUEST[$key]);
        }
    }

    public static function input($key)
    {
        return self::createFromGlobals()->get($key);
    }

    public function has($key)
    {
        if (!is_null(Request::createFromGlobals()->get($key, null))) {
            return true;
        }
        return false;
    }

    public static function is($str)
    {
        $str = ltrim($str, "/");
        if (substr($str, -1) == "%" && substr(rtrim(self::getPath(), "/"), 0, strlen($str)) === $str) {
            return true;
        } elseif ($str == ltrim(self::getPath(), "/")) {
            return true;
        }
        return false;
    }
}

This class extends Symfony\Component\HttpFoundation\Request which is a class of package symfony/http-foundation we have install before with composer.
Inside of the Request class i have added some other functions like getters and setters for properties controller, action, parameters, lang, route. We see a method ->getPath() which returns the current path of the URL. We see some other methods like ::clear($key) which unsets a value from $_REQUEST global array, ::input($key) which returns a value if the $key was found in the global arrays like $_GET or $_POST etc., ->has($key) checks if the given key exists in the global arrays, and ->is($pattern) checks if the pattern is equal with the current route.

Routing

Routing is the process of taking a URI endpoint (that part of the URI which comes after the base URL) and decomposing it into parameters to determine which controller, module and action of that controller should receive the request.

We have build a class called Route that represents a route. In this class exists some properties which store all the information about a route. $alias is the part of the URL after the base URL. $method is the HTTP method used for that request. $controller is the name of the controller class which will handle the request. $action is the name of the function which will handle the request. $filters is the list of the filters that will be executed before the action. More about filters we are going to explain below. $parameters is an array of parameters in the URL. $modules is an array of different modules objects related to the current route.

<?php

class Route
{
    public $alias;
    public $method;
    public $filters = [];
    public $controller;
    public $action;
    public $parameters = [];
    public $modules = [];
}

The Router class is made from some important methods as follows:

<?php

namespace App\Libs;

use Exception;

class Router
{
    private static $routes = [];
    private $route;


    public function get($alias)
    {
        $this->route = new Route();
        $this->route->alias = ltrim($alias, "/");
        $this->route->method = "GET";
        return $this;
    }

    public function post($alias)
    {
        $this->route = new Route();
        $this->route->alias = ltrim($alias, "/");
        $this->route->method = "POST";
        return $this;
    }

    public function put($alias)
    {

        $this->route = new Route();
        $this->route->alias = ltrim($alias, "/");
        $this->route->method = "PUT";
        return $this;
    }

    public function delete($alias)
    {
        $this->route = new Route();
        $this->route->alias = ltrim($alias, "/");
        $this->route->method = "DELETE";
        return $this;
    }

    public function controller($controller)
    {
        if (is_object($this->route)) {
            $this->route->controller = $controller;
        } else {
            throw new Exception("You have to set the route first!");
        }
        return $this;
    }

    public function action($action)
    {
        if (is_object($this->route)) {
            $this->route->action = $action;
        } else {
            throw new Exception("You have to set the route first!");
        }
        return $this;
    }

    public function target($pattern)
    {
        if (is_object($this->route)) {
            $pattern = explode("@", $pattern);
            $this->route->controller = $pattern[0];
            $this->route->action = $pattern[1];
        } else {
            throw new Exception("You have to set the route first!");
        }
        return $this;
    }

    public function modules($modules)
    {
        if (is_object($this->route)) {
            $this->route->modules = $modules;
        } else {
            throw new Exception("You have to set the route first!");
        }
        return $this;
    }

    public function add($filters = [])
    {

        if (is_string($filters)) {
            $this->route->filters = [Filter::get($filters)];
        } else if (is_array($filters)) {
            $this->route->filters = Filter::getArray($filters);
        }

        array_push(self::$routes, $this->route);
    }

    public static function getRoutes()
    {
        return self::$routes;
    }


    public static function findRoute($alias, $method) //:alias -> array of separated paths
    {

        foreach (self::$routes as $route) {

            $routeParts = explode("/", $route->alias);

            $founded = false;
            $removedPaths = [];

            foreach ($routeParts as $i => $part) {

                if (!isset($alias[$i])) {
                    $founded = false;
                    continue;
                };

                if ($part == $alias[$i]) {
                    $founded = true;
                } else {
                    $founded = false;
                }

                array_push($removedPaths, $alias[$i]);

                if ($i == count($routeParts) - 1) {
                    $route->parameters = array_diff($alias, $removedPaths);
                }
            }
            if ($founded) {

                if ($route->method == $method) {
                    return $route;
                } else {
                    continue;
                }

            }
        }

        return null;
    }

    public static function runFilters(Route $route)
    {

        if (!empty($route->filters)) {

            foreach ($route->filters as $filter) {
                $callback = $filter->callback;
                $callback(Request::createFromGlobals());
            }
        }
        return;
    }
}

We see different methods with the names of different HTTP methods like ->get(), ->post(), ->delete(), ->put().

With the ->get() function we handle the GET requests, and with other functions we handle requests with HTTP method like their name respectively. Inside of this methods we create the route object, we set the alias and return $this.

Why do we return $this ?

This is necessary to use Method Chaining. We can call methods from one another like a chain for example: $object->method1()->method2()->method3() and the reason is because we are returning $this, and returning $this object give us the possibility to call another method after calling the first.

Like the ->get() method, in the Request class exist other methods like ->post() ->put() ->delete(), that do the same thing, only use different HTTP methods.

Other methods of Request class are ->controller($controller) which sets the controller and ->action($action) which sets the action of the route object. To set the controller and the action we have an alternate method called ->target($target) which we can pass a pattern: ControllerName@ActionName.

Another method of this class is ->add($filters) which adds a list of filters to a route:

<?php

public function add($filters = [])
{

    if (is_string($filters)) {
        $this->route->filters = [Filter::get($filters)];
    } else if (is_array($filters)) {
        $this->route->filters = Filter::getArray($filters);
    }

    array_push(self::$routes, $this->route);
}

This method registers the filter objects to the property $filters of the route. After this it register the route to the list of registered routes, that’s a property of the Router class. The filter objects have to be from the list of registered filters in the filters.php file. We use Filter::getArray($filters) or Filter::get($filters) to get the filter objects where the argument is a string with the name of one filter, or an array with filters names.

Another method inside Router class is the ->modules() method which takes as an argument an array of objects (modules) . The array contains different object types of classes that extend Module class and implement ModuleInterface.

Another two important methods of class Request are very useful for FrontController.
We mention it before ::findRoute($alias, $method) which is used by FrontController to search if there exist a route in the registered routes, that corresponds to the actual route which came from the HTTP request. If founded return the $route object, else return null.

The other method ::runFilters(Route $route) is used by FrontController too, where in the dispatch() function before executing the action of the controller, the filters are executed.

What is a filter ?

A filter is an object of type FilterObj which contains a name and a callback function:

<?php

class FilterObj
{
    public $name;
    public $callback;
}

These filters are registered in the filters.php file located under /app directory.

Implemetation of runFilters() is shown below:

<?php

public static function runFilters(Route $route)
{

    if (!empty($route->filters)) {

        foreach ($route->filters as $filter) {
            $callback = $filter->callback;
            $callback(Request::createFromGlobals());
        }
    }
    return;
}

If exist filters for the current route, we call the callback function of every filter (we pass a Request object like a parameter) $callback(Request::createFromGlobals());
We don’t do nothing special here, we only call the function declared to the filters.php file, that we have related with the current route through ->add($filters) function of the Router.

These were all the methods of Router class very important to understand.

routes.php

routes.php file is located under the /app directory and serves for registering different routes.

<?php

use App\Libs\Router;
use App\Modules\OurClients;
use App\Modules\LatestPosts;

$router = new Router();

$router->get("/")->target("PagesController@index")->modules([
    "pre-footer" => new OurClients(),
    "middle-right" => new LatestPosts()
])->add();

$router->get("about")->target("PagesController@about")
    ->modules([
        "pre-footer" => new OurClients()
    ])->add();

$router->get("/contact")->controller("PagesController")->action("contact")
    ->add();

$router->post("/contact")->controller("PagesController")->action("postContact")
    ->add();

Initially we have created a Router object, and then we have started registering routes. $router->get("/") sets the alias for the home page, and with ->target("PagesController@index") sets the handler controller PagesController and the handler action index. Using ->modules() method it adds a list of modules to the route, an array of objects of different classes, in this case OurClients and LatestPosts. The keys of this array are very important because the describe the position in the layout where the modules will be printed.

<?php

->modules([
    "pre-footer" => new OurClients(),
    "middle-right" => new LatestPosts()
])
What does it mean?

Let’s explain shortly how are rendered the modules in the blade layout. Every module in the layout will have a position name, e.g. “pre-content” or “footer-right”. In the layout we will call a global method to specify this positions called module($mod_name) e.g. module("footer-right").

For example let’s construct a layout:


           <!----------- header -------------->

               {!! module("pre-content") !!} 

           <!----------- content -------------->

<!-- some other content -->  {!! module("footer-right") !!}

So we have used 2 different positions with different names. With the array passed to the ->modules() method above, we specify which object will be used to which position. So we pass an object of a class (child of Module class) to the position we want.

The latest method is called ->add() and is necessary to be called because it registers the route. We can pass an array of filter names to it but in this case we haven’t used filters because we are going to explain below. The same idea is followed with the other routes.

Filters

Filters provides a convenient mechanism for filtering HTTP requests entering your application. For example, a filter can be to verify the user of your application is authenticated or not, and to redirect it to another path.

The filters are registered to the file filters.php located in the /app directory. A filter is an object of class FilterObj which contains a name and a callback function.

<?php

use App\Libs\Request;
use App\Libs\Filter;

Filter::add("auth", function (Request $request) {
    $authenticated = true;

    if (!$authenticated) {
        redirect("login");
    }
});

Filters are registered by the static method ::add of the Filter class located in /app/libs directory. The filter above is registered with the name “auth”, and the callback function which is the second parameter passed to the :add() function.

Implementation of Filter class:

<?php

namespace App\Libs;

class Filter
{

    private static $filters = [];

    public static function add($filtName, $callback)
    {
        $fil = new FilterObj();
        $fil->name = $filtName;
        $fil->callback = $callback;
        self::$filters[$filtName] = $fil;
    }

    public static function filters()
    {
        return self::$filters;
    }

    public static function get($filtName)
    {
        if (isset(self::$filters[$filtName])) {
            return self::$filters[$filtName];
        }
    }

    public static function getArray($arrFilt)
    {
        $filts = [];
        foreach ($arrFilt as $filtName) {
            if (isset(self::$filters[$filtName])) {
                array_push($filts, self::$filters[$filtName]);
            }
        }
        return $filts;
    }
}

class FilterObj
{
    public $name;
    public $callback;
}

The method :add() registers an object of type FilterObj to the $filters property of Filter class. So this property contains all the filters of the application.

How can we use the filters? In the routes.php file we can pass an array of filters (or only one filter) to a route like this:

<?php

$router->get("/admin/secret")->target("AdminController@secret")->add("auth");

or

$router->get("/admin/secret")->target("AdminController@secret")->add(["auth", "api"]);

So we add filters giving the names in the ->add() function. In this case the route is related to the specific filter and so in the dispatch() function of the FrontController the filters will be executed before the action for the specific route.

Inside the ->add() method of the Router class we see that are called the methods Filter::get($filters) and Filter::getArray($filters).

<?php

public function add($filters = [])
{

    if (is_string($filters)) {
        $this->route->filters = [Filter::get($filters)];
    } else if (is_array($filters)) {
        $this->route->filters = Filter::getArray($filters);
    }

    array_push(self::$routes, $this->route);
}

It only search in the registered filters if exist filter for the specific route, and if yes returns and adds them to the specific route object.

Controllers

Every controller we create have to be located in the /app/controllers path, and we must name them with the postfix “Controller”. Every controller created by us needs to extend Controller class located under /app/libs. This class makes some configurations for every child. Let’s see the implementation of Controller class:

<?php

namespace App\Libs;

class Controller
{
    protected $view;
    private $lang;

    protected $preloadResult;

    function __construct()
    {
        $this->view = new View();
        $this->view->setViewPath(DOCROOT . VIEWS_PATH);
    }

    public function setLang($lang)
    {
        $this->lang = $lang;
        $this->view->setLangPath(DOCROOT . LANGS_PATH . $this->getLang() . DS);
        $this->view->setLang($lang);
        $this->view->setLangFile(strtolower(substr(get_class($this), 0, strpos(get_class($this), "Controller"))));
    }

    public function setLangFile($langFile)
    {
        $this->view->setLangFile(rtrim($langFile, ".ini"));
    }

    public function getLang()
    {
        return $this->lang;
    }

    public function before_action($action, $actionParams)
    {
        if (method_exists($this, "preload_" . $action)) {
            $this->preloadResult = Loader::runController($this, "preload_" . $action, $actionParams);
        }
    }


We see that it have two properties $view which will contain the View object used for a specific action, and $lang which will contain the current language of the application as a string.

In the constructor it initializes the $view and sets the path where the view file is found using the method ->setViewPath($path) of the View class. The path is constructed using the definitions we have declared to definitions.php file: DOCROOT concatenated with VIEWS_PATH.

<?php

public function setLang($lang)
{
    $this->lang = $lang;
    $this->view->setLangPath(DOCROOT . LANGS_PATH . $this->getLang() . DS);
    $this->view->setLang($lang);
    $this->view->setLangFile(strtolower(substr(get_class($this), 0, strpos(get_class($this), "Controller"))));
}

We have mentioned above that the method ->setLang() was called by FrontController in the dispatch() method when we create the controller object $controller->setLang($this->lang). ->setLang() method sets the language of the specific controller, sets the path where to find the language file, which is a .ini file format. In this case the set path is DOCROOT . LANGS_PATH . $this->getLang() . DS so for example if the language is en the path will be /var/www/.../resources/langs/en/. This is the directory where will live the language files. This can be changed later if you want. After setting the directory we need to set the name of the file which will be used so we call $this->view->setLangFile(strtolower(substr(get_class($this), 0, strpos(get_class($this), "Controller")))). This is a little mess but I can explain you. What it does is that sets the name of the language file same as controller name without prefix Controller and in lowercase. For example if the controller is called PagesController the language file will be pages.ini. Here is set the default file and can be changed any time with a customized name.

Inside the langs directory are different .ini files , that contains key = value pairs, which will translate the terms in the view with the correspondent value. For example an .ini file content:

[en]
Hello   =   Hello
GoodBye = Good bye

Inside the view files we can use terms like this [$GoodBye] and in the render time it will be replaced with Good bye.

Another method we see inside the Controller class is ->before_action() method.

<?php

public function before_action($action, $actionParams)
{
    if (method_exists($this, "preload_" . $action)) {
        $this->preloadResult = Loader::runController($this, "preload_" . $action, $actionParams);
    }
}

We have mentioned this method above when we have called it in the dispatch() function of the FrontController. We call before_action if it is necessary because this is called before executing the modules of the routes so, if we want to pass any data from action to the modules, we create a method inside the controller called preload_ + action name. So before_action() method checks if exist such a method and calls it with Loader::runController($this, "preload_" . $action, $actionParams) and the results are saved to the property $preloadResult of class Controller so we can access them to the main action method if we want.

View class content:

<?php

namespace App\Libs;

use Philo\Blade\Blade;

class View
{
    private $view;
    private $viewPath;

    private $lang;
    private $langFile;
    private $langPath;

    private $vars = [];
    private $terms = [];

    private $blade;


    function __construct($view = null)
    {
        $this->setViewPath(DOCROOT . VIEWS_PATH);
        $this->setLangPath(DOCROOT . LANGS_PATH . $this->lang . DS);
        $this->blade = new Blade($this->viewPath, DOCROOT . CACHE_PATH);

        $this->view = $view;
    }

    public static function create($name = null)
    {
        $view = new View($name);
        return $view;
    }

    public function setViewPath($viewpath)
    {
        if (is_dir($viewpath)) {
            $this->viewPath = $viewpath;
            return true;
        } else {
            return false;
        }
    }

    public function setLang($lang)
    {
        $this->lang = $lang;
        return $this;
    }

    public function setLangPath($langPath)
    {
        if (is_dir($langPath)) {
            $this->langPath = $langPath;
            return true;
        } else {
            return false;
        }
    }

    public function setLangFile($langFile)
    {
        $this->langFile = $langFile;
        $this->loadTerms();
    }

    private function loadTerms()
    {
        if (file_exists($this->langPath . $this->langFile . '.ini')) {
            $this->terms = parse_ini_file($this->langPath . $this->langFile . '.ini');
        }
    }

    public function getTerm($term)
    {
        return (isset($this->terms[$term])) ? $this->terms[$term] : null;
    }

    protected function replaceTerms($string)
    {
        if (count($this->terms)) {
            //Replace the Placeholders : [$Marcer]
            while (preg_match('~\[\$(\w+)\]~', $string, $m)) {
                if (!is_null($this->getTerm($m[1]))) {
                    $string = str_replace($m[0], $this->getTerm($m[1]), $string);
                } else {
                    $string = str_replace($m[0], '__' . $m[1] . '__', $string);
                }
            }
        }
        return $string;
    }

    /**
     *
     * @param string $view
     * @param boolean $vars
     * @return string
     */
    public function render($view = null, $vars = [], $returnOutput = false)
    {
        if (is_null($this->view) && (is_null($view) || empty($view))) {
            return "";
        } elseif (!is_null($this->view) && (is_null($view) || empty($view))) {
            $view = $this->view;
        }

        $mergedVars = array_merge($this->vars, $vars);
        $content = $this->blade->view()->make($view, $mergedVars)->render();

        if ($returnOutput) {
            return $this->replaceTerms($content);
        }

        echo $this->replaceTerms($content);
    }

    public function set($varname, $value)
    {
        $this->vars[$varname] = $value;
        return $this;
    }
}

Properties of the class:

<?php

private $view;
private $viewPath;

private $lang;
private $langFile;
private $langPath;

private $vars = [];
private $terms = [];

private $blade;

The $view property contains the name of the view, $viewPath contains the path where the view is located, $lang contains the current language, $langFile contains the name of the language .ini file, $langPath contains the path where the language file is located, $vars contains all the php variables that will be extracted to the view where we can use, $terms contains all the terms imported from .ini language file, and $blade is an object of Blade class.

In the constructor for safety is set the lang path and view path, is created the Blade object and is set the view name, which is optional because we can set the view name when we call the render function.

What is Blade ?

Blade is the simple, yet powerful templating engine provided with Laravel. Unlike other popular PHP templating engines, Blade does not restrict you from using plain PHP code in your views. You can see more information about it here in this link: Blade templating engine.

In the View class are different methods used to set the view path, set the language etc like setLang(), setLangPath(), setLangFile(), setViewPath() that are understandable. Another methods serves to load all the terms from .ini file.

<?php

private function loadTerms()
{
    if (file_exists($this->langPath . $this->langFile . '.ini')) {
        $this->terms = parse_ini_file($this->langPath . $this->langFile . '.ini');
    }
}

It checks if the .ini file exists and parses it, returning an array of all the terms with the given value. This terms are registered to the $terms property of View class.
Another important method is ->set() which gets a key and a value like parameters and saves it to the $vars property of the View class.

<?php

public function set($varname, $value)
{
    $this->vars[$varname] = $value;
    return $this;
}

The most important method is ->render() which takes three parameters, the name of the view, an array with $vars (variables to be extracted in the view), and a boolean variable telling if have to to return the content or to print it.

<?php

public function render($view = null, $vars = [], $returnOutput = false)
{
    if (is_null($this->view) && (is_null($view) || empty($view))) {
        throw new \Exception("View $view not found");
    } elseif (!is_null($this->view) && (is_null($view) || empty($view))) {
        $view = $this->view;
    }

    $mergedVars = array_merge($this->vars, $vars);
    $content = $this->blade->view()->make($view, $mergedVars)->render();

    if ($returnOutput) {
        return $this->replaceTerms($content);
    }

    echo $this->replaceTerms($content);
}

If the $view parameter is empty or null, it checks if there is a value to the $view property, and if not it throws an exception for missing the view file.
Then an array of all variables to be extracted to the view is created. $mergedVars = array_merge($this->vars, $vars) here is merged the $vars property array, with the $vars array parameter passed to the render() function. Then is called the ->render() function of the blade object passing all the $mergedVars too.
After this we need to replace the language terms if there is any and we do this with $this->replaceTerms($content) which gets a content, replaces the terms and returns the same content with replaced terms. Then in base of $returnOutput boolean parameter we return the content or print it.

The ->replaceTerms() function:

<?php

protected function replaceTerms($string)
{
    if (count($this->terms)) {
        //Replace the Placeholders : [$Marcer]
        while (preg_match('~\[\$(\w+)\]~', $string, $m)) {
            if (!is_null($this->getTerm($m[1]))) {
                $string = str_replace($m[0], $this->getTerm($m[1]), $string);
            } else {
                $string = str_replace($m[0], '__' . $m[1] . '__', $string);
            }
        }
    }
    return $string;
}

This function gets the string, and if there are terms gotten from .ini files it does the replacement of the [$Marcer] with the specified value. For example if there is a string in the view content [$GoodBye]it will be replaced with Good bye only if it exists to the .ini file.

Models

Regarding to models, they will be placed to /app/models directory and have to extend Illuminate\Database\Eloquent\Model class. For example we have a model called User:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model as Eloquent;

class User extends Eloquent
{

}

To see what we can do with the models I recommend reading the documentation of the Eloquent in this link: Eloquent Documentation
Every feature of Eloquent we can use here too, because we are extending it.

An example of a controller:

<?php

use App\Libs\Controller;
use App\Libs\Request;
use App\Libs\Validation;
use App\Libs\Flash;

class PagesController extends Controller
{

    public function index()
    {
        $this->view->render("index");
    }

    public function about()
    {
        $this->view->render("about");
    }


    public function contact()
    {
        $this->view->render("contact");
    }

    public function postContact(Request $request)
    {
        Validation::create()->check($request, [
            "name" => "required",
            "email" => "required | email",
            "message" => "required",
        ])->action("contact");

        //Send Mail
        Flash::addMessage("Message sent successfully!");

        redirect("contact");
    }
}

The first three actions only render the view with the specified name using ->render() function. The last action we see that has to do with a post request handler (because is called postContact). In every action there is an optional first parameter of type Request. If we set it we can use it, but if we don’t want we doesn’t set it. After that we can declare other parameters of the function, these will be replaced with the values from URL parameters.

For example if the route is like this:
http://domain.com/alias1/alias2/param1/param2, we can access these parameters building an action like this:
->action(Request $request, $param1, $param2) {} or ->action($param1, $param2).
Using the methods of Request class we can access all the values of global array variables like $_GET, $_POST, $_SERVER, $_FILES etc.

For example:

<?php

// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();

// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');

// retrieve SERVER variables
$request->server->get('HTTP_HOST');

// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');

// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');

// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content_type');

$request->getMethod();    // GET, POST, PUT, DELETE, HEAD

//gets the 'name' if it is set or returns the default value
$input = $request->get('name', 'World');

//gets the client ip
$request->getClientIp()

Inside the /app/libs directory you will find some other classes we have not talked much about, but you can check them.
* Validation class used above is a class located under the /app/libs directory which makes validation of the $request object in base of some rules we give to it.
* Flash is another class located under /app/libs that is used for showing flash messages to the user.
* Session is another class located under /app/libs that is used to work and manipulate the $_SESSION with some simple methods.
* Data is another class located under /app/libs that is used to save data inserted by a user when the validation failes.
* Response is another important class located under /app/libs that extends Symfony\Component\HttpFoundation\Response and we can use every feature of it, for example:

<?php

$response = new Response();

$response->setContent('<html><body><h1>Hello world!</h1></body></html>');
$response->setStatusCode(Response::HTTP_OK);

// set a HTTP response header
$response->headers->set('Content-Type', 'text/html');

// print the HTTP headers followed by the content
$response->send();

Modules

A module represents a class which contains a function called execute(). Depending on the route we can run this function in specific places in the layout. So when we want for a specific route to print some data in the layout and for other routes no, we use modules.
It seems like a widget that for some routes have to be printed and for some others no.

Let’s take an example. We have a layout and in a position of this layout we want to print the time e.g. Thursday 01, December 16. The layout is the same for 10 routes, but we want to print the time for 5 routes. What to do? In this case we need to make the widget of clock dynamic, so that for some routes to be printed for others no.
We create class DateWidget with a function called execute() in the /app/modules directory. We extend Module class and implement ModuleInterface.
Inside the function ->execute() we call the method $this->setContent(date('l d, F y')) so the content of the widget will be only the current date. For different widgets we can make data processing and we can render a view if we want. This is a simple example.

For every route that we want to print the widget of date, in the routes.php file, we add to the function ->modules() the array that contains "time-position" => new DateWidget(). The name of position can be different but in this case I used “time-position”.

<?php

$router->get("/")->target("PagesController@index")->modules([
    "time-position" => new DateWidget()
])->add();

Every module needs to implement the interface ModuleInterface located under /app/libs, its content is shown below:

<?php

namespace App\Libs;

interface ModuleInterface
{
    public function execute(Request $request);
}

This interface obliges every class (that implements it) to implement the ->execute() function. This function will be executed from the FrontController before the action being executed. We don’t print content in the ->execute() function. We only call ->setContent($content) method, which $content can be any content like a string, a number, a view etc.

With the ->setContent() function we are only preparing the view that will be shown to the user like a widget. When we will print it? Good question.
We said above that we call in the layout global function module("position-name") to print the module. Let’s see how it is implemented :

<?php

function module($position)
{
    \App\Libs\Module::output($position);
}

It only calls the static method ::output($position) from the Module class located in /app/libs. Let’s see the implementation of the class:

<?php

namespace App\Libs;

class Module
{
    public $view;
    protected $content;
    protected $parameters = [];

    function __construct()
    {
        $this->view = new View();
    }

    public function setContent($content)
    {
        $this->content = $content;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function setParameters($parameters)
    {
        $this->parameters = $parameters;
    }

    public static function output($position)
    {
        $modules = Request::getRoute()->modules;

        if (isset($modules[$position])) {
            echo $modules[$position]->getContent();
        }
    }
}

From the method output() the call Request::getRoute() is a getter for the current route object. From this object we get the modules array $modules. Remind that this array was created in the routes.php file, when we pass it to the ->modules() function.
Then we check if is set a key with that position name in that array or not. If is set we print the the widget calling ->getContent() function that have every module (inherited by Module class), else don’t do nothing.

For example :

The current route:

<?php

$router->get("/")->target("PagesController@index")->modules([
    "pre-right" => new LatestPosts()
])->add(); 

LatestPosts class content:

<?php

namespace App\Modules;

use App\Libs\ModuleInterface;
use App\Libs\Module;
use App\Libs\Request;

class LatestPosts extends Module implements ModuleInterface
{

    public function execute(Request $request)
    {
        $content = $this->view->render("modules/latest-posts", [], true);
        $this->setContent($content);
    }
}

In the layout:


<!-- header -->
<!-- content -->

{!! module("pre-footer") !!}  //this is a widget position

<!-- footer -->

Seeing the example above, the module("pre-footer") will be replaced with the content of the view located under /resources/views/modules with the name latest-posts.blade.php.

Conclusion

In this article we learnt that building a simple framework with PHP using MVC design pattern isn’t very difficult. Of course, if we want to do some magic and to built a big framework we have to work in collaboration with different designers and developers and have to follow all the software engineering phases to make a good application with high quality.

Our framework is not complicated, can be understood by all that have some knowledge of PHP object oriented and what MVC is, and is very useful for applications that are not complicated. It can be used for different websites that we build for any client, because it’s very simple and spends less space than some others. It can be easy customized to any need.

Anyhow the most important thing is to understand how a framework ca be built, and so you can build your own with the knowledge that you have acquired here and other knowledge that you had before or you are going to have.

This project is open source so you can pull all the source code from my repository in github :
https://github.com/albanafmeti/micro-framework.git

All you have to do is to clone the Github repository:

git clone https://github.com/albanafmeti/micro-framework.git

Then run composer install to install dependencies and to make all the basic configuration operations.

You can see a simple website example built over this framework with this URL:
http://micro.techalin.com

comments powered by Disqus