I recently wrote a series of articles about creation of a pizza shop on Laravel 8 that was well received by blog readers. Therefore, I decided that the detailed description of creating various projects is interesting to people, and today I will tell you how to create MVC Framework in PHP.

Whether you need to create your own MVC Framework or use one of the already created – it’s your choise. In most cases, it is better to use ready-made solutions, but understanding the operation of such systems is necessary for a good PHP programmer. And when you create something yourself, you always start to understand it better. Therefore, I advise you to create your own MVC Framework at least once in your life in order to better understand how ready-made solutions work.

In this article I will describe the process of creating a simple MVC Framework (model-view-controller is a software design pattern) in PHP and I will try to do it so that everyone understands and can repeat it.

Database connection

To work with the database in the framework, I decided to use the ready-made Eloquent ORM, so as not to “reinvent the wheel”. So the first thing I did was create a composer.json file to connect the dependency. It will look like this.

{
  "name": "anerdy/mynewmvc",
  "type": "project",
  "require": {
    "illuminate/database": "^8.14"
  }
}

After running the composer install command in the console, the composer.lock file and the vendor folder containing the dependencies should appear.

Now create a config.php file that will contain the database connection settings.

<?php
defined("DBDRIVER") or define('DBDRIVER','mysql');
defined("DBHOST") or define('DBHOST','localhost');
defined("DBNAME") or define('DBNAME','test_project_task');
defined("DBUSER") or define('DBUSER','root');
defined("DBPASS") or define('DBPASS','');

Important: to prevent your connection settings from getting into git (this is unsafe), it is advisable to exclude config.php in the .gitignore file, and at the same time the vendor folder from indexing. For convenience, you can create a config.example.php file, which is the same as config, but does not contain personal connection data, and from it on the server create a real config.php. The preparation is done, I will write a description of how we will connect to the database and receive and write data further.

Structure of MVC Framework in PHP

When I open my MVC Framework in the browser first time it will run the index.php file located in the root of the project, it contains the following code.

<?php
ini_set('display_errors', 0);
require_once 'config.php';
require_once 'vendor/autoload.php';
require_once 'app/bootstrap.php';

As you can see, it is quite small, here I turn off the display of errors, include the config file, and then the autoload.php file that the composer generated for me so that I can conveniently add all the dependency classes to the project. The last line includes the bootstrap.php file from the app folder, which contains instructions for running the classes I created and the entire application (in fact, the structure of a simple MVC Framework). It contains the following lines.

<?php
require_once 'core/database.php';
new App\Core\Database();
require_once 'core/model.php';
require_once 'core/view.php';
require_once 'core/controller.php';
require_once 'core/route.php';
App\Core\Route::start();

In the file, several other files are indicated, which are sequentially connected. They are all located in the /app/core folder relative to the root. Let’s talk about them in more detail.

Database.php – contains the initialization of the capsule instance, which will later be used to retrieve data from the database.

<?php
namespace App\Core;

use Illuminate\Database\Capsule\Manager as Capsule;

class Database {

    function __construct() {
        $capsule = new Capsule;
        $capsule->addConnection([
            'driver' => DBDRIVER,
            'host' => DBHOST,
            'database' => DBNAME,
            'username' => DBUSER,
            'password' => DBPASS,
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
        ]);
        // Setup the Eloquent ORM…
        $capsule->bootEloquent();
    }
}

Model.php – here you can specify some general features of the models that will expand it in the future. In the simplest implementation, it is not used and contains an empty model.

View.php – this file contains the View class, in which the only generate method is engaged in displaying templates (the visible part of the site).

<?php
namespace App\Core;

class View
{
    function generate($content_view, $template_view, $data = null)
    {
        include 'app/views/'.$template_view;
    }
}

Controller.php is the parent class for all controllers that we will create a little later.

<?php
namespace App\Core;

class Controller {
    public $model;
    public $view;

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

    function action_index()
    {
    }
}

And at the very end of bootstrap.php, the route.php file is connected, which is responsible for routing the entire application.

Routing in the new MVC Framework

The /app/core/route.php file contains a static start method (it is launched in the bootstrap.php file), which specifies the routing and connection of the necessary models and controllers.

<?php
namespace App\Core;

class Route
{
    static function start()
    {
        $controller_name = 'Main';
        $action_name = 'index';
        $_SERVER['REQUEST_URI'] = strtok($_SERVER['REQUEST_URI'], '?');
        $routes = explode('/', $_SERVER['REQUEST_URI']);

        if ( !empty($routes[1]) )
        {
            $controller_name = $routes[1];
        }
        if ( !empty($routes[2]) )
        {
            $action_name = $routes[2];
        }

        $controller_name = 'Controller_'.$controller_name;
        $action_name = 'action_'.$action_name;

        $files1 = scandir("app/models/");
        foreach ($files1 as $model_file) {
            if (!in_array($model_file ,[".", ".."]))
                if(file_exists("app/models/".$model_file))
                    include "app/models/".$model_file;
        }

        $services = scandir("app/services/");
        foreach ($services as $service_file) {
            if (!in_array($service_file ,[".", ".."]))
                if(file_exists("app/services/".$service_file))
                    include "app/services/".$service_file;
        }

        $controller_file = strtolower($controller_name).'.php';
        $controller_path = "app/controllers/".$controller_file;
        if(file_exists($controller_path))
        {
            include "app/controllers/".$controller_file;
        } else {
            Route::ErrorPage404();
        }

        $controller_name = "App\\Controllers\\" . $controller_name;
        $controller = new $controller_name;
        $action = $action_name;

        if(method_exists($controller, $action))
        {
            $controller->$action();
        } else {
            Route::ErrorPage404();
        }
    }

    static function ErrorPage404()
    {
        $host = 'http://'.$_SERVER['HTTP_HOST'].'/';
        header('HTTP/1.1 404 Not Found');
        header("Status: 404 Not Found");
        header('Location:'.$host.'404');
    }
}

At the beginning are the default controller and action ($controller_name and $action_name), which will be used if the user simply enters your_site/ in the browser. Otherwise, the correct routing would be your_site/controller/action. Therefore, further we read the entered address from the browser line and determine if there is a controller name and an action name, and if so, we override the standard ones.

Then the line $controller_name = 'Controller_'. $controller_name; defines the name of the controller class in our system (for everything to work correctly, you will need to name it in accordance with this template), and the next line is the name of the action inside this controller.

Next, using the scandir function, we search and then connect to the project all the models located in the /app/models/ folder. Then we do the same with the application service layer files, which are located in the /app/services/ folder.

The line $controller_file = strtolower($controller_name). '.php'; gets the correct file name of the desired controller, and then look for it in the /app/controllers/ folder. If no such file exists, then the ErrorPage404 method will be called, displaying a 404 error. The controller is then instantiated and checked to see if the specified action exists inside it. If it exists, it will be called, otherwise it will also have a 404 error.

Displaying Pages in the MVC Framework

Once again I will repeat the whole chain and logic of page display. For example, if a user enters your_site/task/add, then the application will call the action_add method of the Controller_Task. This controller might look like this.

<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Models\Model_Task;
use App\Core\View;
use App\Services\Task_Service;
use App\Services\Admin_Service;
use \Exception;

class Controller_Task extends Controller
{
    public $taskService;
    public $adminService;

    function __construct()
    {
        $this->model = new Model_Task();
        $this->view = new View();
        $this->taskService = new Task_Service();
        $this->adminService = new Admin_Service();
    }

    public function action_add()
    {
        $errors = [];
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            try {
                $errors = $this->taskService->validateTask();
                if ( empty($errors) ) {
                    $this->taskService->createTask();
                    header("Location: /");
                    die();
                }
            } catch (Exception $exception) {
                $errors['exception'] = $exception->getMessage();
            }
        }
        $tasks = $this->model::get();
        $this->view->generate('add_task_view.php', 'template_view.php', ['tasks' => $tasks, 'errors' => $errors] );
    }
}

As you can see, all necessary models and services are connected in __construct. Accessing models to retrieve data from the database occurs as usual in the Eloquent ORM, for example, like this:

$tasks = $this->model::get();

The Model_Task itself contains a minimum of code, just connecting to the desired table in the database.

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Model_Task extends Model
{
    protected $table = 'tasks';
}

The services contain different logic of work, which does not want to clutter up the controller.

The page template to be displayed is specified in the line:

$this->view->generate('add_task_view.php', 'template_view.php', ['tasks' => $tasks, 'errors' => $errors] );

Here template_view.php is a layout template (it can contain some general information for all pages, for example, a header and footer), and add_task_view.php is a template for a specific page. They are both located in the /app/views/ directory. The third parameter in the generate method accepts variables that will need to be passed to the template.

template_view.php looks like this.

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <title>Tasks Application</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<?php include 'app/views/'.$content_view; ?>
</body>
</html>

As you can see, we have already got a full-fledged, albeit quite simple, MVC Framework. You can see the full code of the project that I created on it in my github account.

Hopefully, based on this article, you can create MVC Framework in PHP. And in order not to miss the release of new articles, subscribe to my Twitter, there I do not spam, but I try to write only useful information.

See you later!

Share post
Twitter Facebook