RU EN

Simple MVC applications

2 July, 2019

I would like to touch on the topic of proper PHP application architecture. The article will focus on the MVC design pattern. Write about MVC I was inspired by the fact that understanding this pattern is key to becoming a good php programmer. So if you're new and want to start writing programs right, read on.

Theory

In short, the MVC (model view controller) is a way of writing a program, when the code responsible for data output is written in one place, and the code that generates this data is written in another place. The result is that if for example you need to correct the output, you immediately know where to look. Now all popular frameworks use this architecture.
Also worth mentioning is the fact that there are two camps: one writes logic in controllers, the second in models. In those frameworks that I know (Yii, Laravel), logic is written in controllers, and models are just ORM instances (read about the Active Record pattern). In yii, by the way, the manual says that it is necessary to write logic in models, and then they write it in controllers in examples, quite funny.
Determined with business logic, we write in controllers. Also in the controller's methods there is a call to the models, which are essentially ORM instances, in order to use them to get data from the database over which changes will be made. The end result is sent to the views. Views contain HTML markup and small PHP code inserts for crawling, formatting and displaying data.
Also I need to mention that there are two types of MVC Page Controller and Front Controller. The Page Controller is rarely used, its approach is to use multiple entry points (requests to the site are made to several files), and within each entry point there is a display code. We will write Front Controller with one entry point.

Practice

We need to start by setting up the server to redirect all requests to our single entry point. If you are using apache, then in the .htaccess file write the following.

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L]
Further in the folder of our project create a folder that can be called the App for example. It will contain the following content.

Our service locator. File App.php


class App
{
    public static $router;

    public static $db;

    public static $kernel;    

    public static function init()
    {
        spl_autoload_register(['static','loadClass']);
        static::bootstrap();
        set_exception_handler(['App','handleException']);
    }

    public static function bootstrap()
    {
        static::$router = new App\Router();
        static::$kernel = new App\Kernel();
        static::$db = new App\Db();
    }

    public static function loadClass ($className)
    {
        $className = str_replace('\\', DIRECTORY_SEPARATOR, $className);
        require_once ROOTPATH.DIRECTORY_SEPARATOR.$className.'.php';
    }

    public function handleException (Throwable $e)
    {
        if($e instanceof \App\Exceptions\InvalidRouteException) {
            echo static::$kernel->launchAction('Error', 'error404', [$e]);
        }else{
            echo static::$kernel->launchAction('Error', 'error500', [$e]);  
        }
    }
}
Service locator is needed to store the components of our application in it. Since we have a simple mvc application, we do not use the registry pattern (as done in Yii, for example). And simply store the application components to static properties in order to access them easier. Also App class register class autoloader and exception handler.

Router. File Router.php


namespace App;

class Router
{
    public function resolve ()
    {
        if(($pos = strpos($_SERVER['REQUEST_URI'], '?')) !== false){
        $route = substr($_SERVER['REQUEST_URI'], 0, $pos);
        }
        $route = is_null($route) ? $_SERVER['REQUEST_URI'] : $route;
        $route = explode('/', $route);
        array_shift($route);
        $result[0] = array_shift($route);
        $result[1] = array_shift($route);
        $result[2] = $route;
        return $result;
    }
}
In simple mvc application router have only one method. Which parses adress from $_SERVER['REQUEST_URI']. I have not yet said that all our links to the pages of the site should be www.ourwebsite.com/%controller%/%action%, where %controller% is the name of the controller file, and %action% is the name of the controller method that will be called .

File Db.php


namespace App;

use App;

class Db 
{
    public $pdo;

    public function __construct()
    {
        $settings = $this->getPDOSettings();
        $this->pdo = new \PDO($settings['dsn'], $settings['user'], $settings['pass'], null);
    }

    protected function getPDOSettings()
    {
        $config = include ROOTPATH.DIRECTORY_SEPARATOR.'Config'.DIRECTORY_SEPARATOR.'Db.php';
        $result['dsn'] = "{$config['type']}:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
        $result['user'] = $config['user'];
        $result['pass'] = $config['pass'];
        return $result;       
    }

    public function execute($query, array $params=null)
    {
        if(is_null($params)){
            $stmt = $this->pdo->query($query);
            return $stmt->fetchAll();
        }
        $stmt = $this->pdo->prepare($query);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }    
}
This class uses config file which returns an array.

File Config/Db.php


return [
'type' => 'mysql',
'host' => 'localhost',
'dbname' => 'gotlib',
'charset' => 'utf8',
'user' => 'root',
'pass' => ''
];

Our kernel. File Kernel.php


namespace App;

use App;

class Kernel 

{

    public $defaultControllerName = 'Home';
    
    public $defaultActionName = "index";

    public function launch()
    {        
        list($controllerName, $actionName, $params) = App::$router->resolve();
        echo $this->launchAction($controllerName, $actionName, $params);
    }

    public function launchAction($controllerName, $actionName, $params)

    {
        $controllerName = empty($controllerName) ? $this->defaultControllerName : ucfirst($controllerName);

        if(!file_exists(ROOTPATH.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR.$controllerName.'.php')){
            throw new \App\Exceptions\InvalidRouteException();
        }

        require_once ROOTPATH.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR.$controllerName.'.php';
        if(!class_exists("\\Controllers\\".ucfirst($controllerName))){
            throw new \App\Exceptions\InvalidRouteException();
        }

        $controllerName = "\\Controllers\\".ucfirst($controllerName);
        $controller = new $controllerName;
        $actionName = empty($actionName) ? $this->defaultActionName : $actionName;

        if (!method_exists($controller, $actionName)){
            throw new \App\Exceptions\InvalidRouteException();
        }
        return $controller->$actionName($params);
    }
}
Kernel calls router and after that launches controller's action. Also kernel may throw an exception if there is no corresponding controller or action.

File Controller.php

We also need to create a base controller to inherit other controllers from it. Inheriting controllers needed so that we can render view. Rendering methods support use of layout which contains common (for all other views) components like footer and header.

namespace App;

use App;

class Controller 
{

    public $layoutFile = 'Views/Layout.php';

    public function renderLayout ($body) 
    {
        ob_start();
        require ROOTPATH.DIRECTORY_SEPARATOR.'Views'.DIRECTORY_SEPARATOR.'Layout'.DIRECTORY_SEPARATOR."Layout.php";
        return ob_get_clean();
    }

    public function render ($viewName, array $params = [])
    {
        $viewFile = ROOTPATH.DIRECTORY_SEPARATOR.'Views'.DIRECTORY_SEPARATOR.$viewName.'.php';
        extract($params);

        ob_start();

        require $viewFile;
        $body = ob_get_clean();
        ob_end_clean();

        if (defined(NO_LAYOUT)){
            return $body;
        }

        return $this->renderLayout($body);
    }
}

File index.php

Don't forget to create an index.php file in root folder.

define('ROOTPATH', __DIR__);

require __DIR__.'/App/App.php';

App::init();

App::$kernel->launch();

Creating controllers and views

Working with our application (you can even say a mini framework) is now reduced to creating views and controllers. The following controller example (in the Controllers folder)

namespace Controllers;

class Home extends \App\Controller
{
    public function index ()
    {
        return $this->render('Home');
    }
}
View example(in Views folder)

<img src="Img/my_photo.jpeg" alt="my_photo" id="my_photo">
<h1>Привет</h1>
<p>Меня зовут Глеб и я - веб-разработчик.</p>
Мои контакты:<br>
8-912-641-3462<br>
goootlib@gmail.com
In Layout folder create Layout.php

<!DOCTYPE html>
<html lang="ru">
    <head>
        <meta charset="utf-8">
        <title>Обо мне</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link href="/Css/style_layout.css" rel="stylesheet" type="text/css">
        <link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
    </head>
    <body>

        <header>
            <nav>
                <a id="about_button" href="/home">Обо мне</a>
                <a id="portfolio_button" href="/portfolio">Портфолио</a>
                <a id="blog_button" href="/blog">Блог</a>
            </nav>
        </header>

        <div class="main">
                <?= $body ?>
        </div>

        <footer>
            <div class="copyrights">
            ©2017 Жуков Глеб(gotlib)<br>
            При копировании материалов на сторонние ресурсы, ссылка на http://www.gotlib.info обязательна!
            </div>
            <div class="contacts">
                8-912-641-3462<br>
                goootlib@gmail.com
            </div>
        </footer>

    </body>
</html>

Сonclusion

If you decide to use the code of the application that I described, do not forget to create an Error controller with the error404 and error500 methods. The class for working with the database described by me is suitable for writing queries with hands, instead of it, you can include ORM and you will have real models.