How to build a simple PHP MVC framework 5 (2)

MVC design pattern

By on Tue Mar 30 in PHP, Programming


Introduction

Today I am going to show how to create a simple PHP application following the MVC pattern (Model-View-Controller). I was inspired by a PHP course I taught some years ago, and in which I built a simple e-commerce with the students. This e-commerce was based on a simple MVC framework based on PHP. Then, people who have continued with code and programming already had a smattering of what means MVC before get their hands on a real framework.

MVC frameworks are widely used in the industry because they offer a lot of advantages for rapid and structured development. There are MVC frameworks for most of the programming languages you may know, from DotNet to PHP. Unfortunately, those frameworks might have a steep learning curve. This is due to the fact that people need to learn how to write code in the framework ecosystem.

Personal note: in 2010 I was already developing software for more than 5 years, and I was looking for a good solution in order to build a web application for my boss. Briefing with a former colleague of mine (thanks Davide C.!), I started using Symfony 1.4. I used the “RTFM approach” (Read The Friendly Manual…) before writing any code. In two months I realized a medium-complex application (registration, ACL, dashboard, frontend, etc).

After that, I worked on Zend Framework, Symfony 2.0 and 5, and Laravel (currently working on 5.8), and also on microframeworks like Silex (not maintained anymore) and Lumen. Without any doubt, my favorite framework is Laravel. Despite some “magical thing” that can scare people, Laravel offers a lot of out-of-the-box features that you can simply activate with the right configuration setting.

What does mean MVC?

MVC is a design pattern used to decouple data (Models), the user-interfaces (Views), and application logic (Controllers). To be able to follow this “How to”, you need to have a good knowledge of PHP and OOP (Object Oriented Programming).

Build a simple PHP MVC framework

Independently you are using Docker, XAMPP, or whatever for your development environment, let’s create a simple structure for the simple PHP MVC framework. I use to have a folder called “Solutions” for all my projects, then enter your folder, create a new folder called “simple-php-mvc” and then enter that folder.
Let’s create the basis folders for your MVC:

  1. app
  2. config
  3. public
  4. views
  5. routes

Starting small, let’s create the two most important files of our simple PHP MVC: index.php and htaccess.

The htaccess configuration file

Enter the public folder, and let’s create a file called index.php

Now, at the root level of your project, let’s create a new file called .htaccess
Then open it, and put this code inside the htaccess:

<IfModule mod_rewrite.c>
RewriteEngine On

# Stop processing if already in the /public directory
RewriteRule ^public/ - [L]

# Static resources if they exist
RewriteCond %{DOCUMENT_ROOT}/public/$1 -f
RewriteRule (.+) public/$1 [L]

# Route all other requests
RewriteRule (.*) public/index.php?route=$1 [L,QSA]
</IfModule>

Htaccess is a configuration file for the Apache web server, and the mod_rewrite directive tells to Apache that every request will end up to the index.php located in the folder named public. What does it mean? It means that if you browse https://simple-php-mvc/page1, https://simple-php-mvc/page2 or https://simple-php-mvc/page3, all of them will end up in the index.php under public, that is the entry point of your PHP MVC framework. This is a big advantage because you can now handle your request in one place, understand what resource is requested and provide the right response.
Another thing: using htaccess and drive the traffic under the public folder, the rest of your project’s structure will be hidden to anyone.

This is how your project looks like right now:

PHP MVC application folder structure
Folder structure
app
config
public
    index.php
views
routes
.htaccess

Bootstrap your PHP MVC framework

Now you need a way to bootstrap your app and load the code you need. We already said that index.php under the public folder is the entry point, for that reason we include the necessary files from there.

First of all, we load the config file, here the content of index.php:

// Load Config
require_once '../config/config.php';

Now we can create a config.php file under the config folder.

Inside the config file, we can store the settings of the framework, for example, we can store the name of our app, the path of the root, and of course, the database connection parameters:

<?php
//site name
define('SITE_NAME', 'your-site-name');

//App Root
define('APP_ROOT', dirname(dirname(__FILE__)));
define('URL_ROOT', '/');
define('URL_SUBFOLDER', '');

//DB Params
define('DB_HOST', 'your-host');
define('DB_USER', 'your-username');
define('DB_PASS', 'your-password');
define('DB_NAME', 'your-db-name');

Autolader

We want to be able to load the future classes without any pain (see: dozen of include or require), then we’ll use the PSR-4 autoloading with Composer.
Composer is a dependency manager for PHP, it allows you to declare the libraries your project depends on and it will manage them for you. Really helpful!

First, at the root level, you must create a file called composer.json and add the following content:

{
    "name": "gmaccario/simple-mvc-php-framework",
    "description": "Simple MVC PHP framework: a demonstration of how to create a simple MVC framework in PHP",
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

Then, assuming that you already installed composer on your computer or container, execute the following command (at the root level of your project):

composer dump-autoload

If you check your root folder now, you can see a new folder called vendor that contains the autoload.php file and the composer folder.
Open the index.php and simply add the following code at the beginning of the file:

require_once '../vendor/autoload.php';

From now on, you can use App as a starting point of your namespaces, like this:

use App\Controllers\MyController;

Now, let’s learn what the MVC acronym stands for.

Model

A model is an object that represents your data. The model will be modeled on your database table structure and it will interact with the database operations (create, read, update and delete).
For instance, if you have a Products table like that:

CREATE TABLE IF NOT EXISTS products (
  id int(10) NOT NULL auto_increment,
  title varchar(255) collate utf8_unicode_ci NOT NULL,
  description text collate utf8_unicode_ci,
  price decimal(12,5) NOT NULL,
  sku varchar(255) collate utf8_unicode_ci NOT NULL,
  image varchar(255) collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;

First of all, let’s create a new folder called Models under app folder. Then let’s create a new file called Product under Models.
Your model Product will be:

<?php 
namespace App\Models;

class Product
{
    protected $id;
    protected $title;
    protected $description;
    protected $price;
    protected $sku;
    protected $image;

    // GET METHODS
    public function getId()
    {
        return $this->id;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function getPrice()
    {
        return $this->price;
    }

    public function getSku()
    {
        return $this->sku;
    }

    public function getImage()
    {
        return $this->image;
    }

    // SET METHODS
    public function setTitle(string $title)
    {
        $this->title = $title;
    }

    public function setDescription(string $description)
    {
        $this->description = $description;
    }

    public function setPrice(string $price)
    {
        $this->price = $price;
    }

    public function setSku(string $sku)
    {
        $this->sku = $sku;
    }

    public function setImage(string $image)
    {
        $this->image = $image;
    }

    // CRUD OPERATIONS
    public function create(array $data)
    {

    }

    public function read(int $id)
    {

    }

    public function update(int $id, array $data)
    {

    }

    public function delete(int $id)
    {

    }
}

And that’s it. With the methods, you’ll create the objects you need to be filled with the real values based on the model.

View

The view is responsible to take in the data from the controller and display those values. That’s it. There are a lot of template engines for PHP, from Twig to Blade. For this MVC tutorial for PHP, we’ll use only plain HTML to make things simple.

In order to create a new view, we must create a new file called product.php under the views folder.
Based on the product attributes, we can write a simple HTML like this:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="shortcut icon" href="favicon.png">
    <title>Simple PHP MVC</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>

<body>

    <section>
        <h1>My Product:</h1>
        <ul>
            <li><?php echo $product->getTitle(); ?></li>
            <li><?php echo $product->getDescription(); ?></li>
            <li><?php echo $product->getPrice(); ?></li>
            <li><?php echo $product->getSku(); ?></li>
            <li><?php echo $product->getImage(); ?></li>
        </ul>
        <a href="<?php echo $routes->get('homepage')->getPath(); ?>">Back to homepage</a>
    <section>

    <script src="https://code.jquery.com/jquery-3.6.0.min.js" 
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" 
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>

</html>

The view is now ready to get the product object ($product) and display its values.

Controller

The controller is the heart of the application logic. Is responsible for accepting the input and convert it to commands for the model or view.
Let’s create a new folder called Controllers under the app folder, and create a new controller file called ProductController.php. Here the content:

<?php 

namespace App\Controllers;

use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;

class ProductController
{
    // Show the product attributes based on the id.
	public function showAction(int $id, RouteCollection $routes)
	{
        $product = new Product();
        $product->read($id);

        require_once APP_ROOT . '/views/product.php';
	}
}

Very simple, isn’t it? Obviously, things might be more complex, we can create a parent Controller class, a view method, and other helper functions. But it’s enough for now.

The routing system

Now we need a mechanism to deal with the URLs. We want to use a friendly URL; in other words, we want to deal with web addresses that are easy to read and that include words that describe the content of the webpage. We need a routing system then.
We can create our own route system, or since we used composer for the autoload, we can dig into the extensive Symfony ecosystem packages and work smart!
So, let’s see how we can take advantage of the Symfony Routing Component. Here the documentation: https://symfony.com/doc/current/create_framework/routing.html

First of all, install the component:

composer require symfony/routing
Install symfony/routing via composer.
Install symfony/routing via composer.

If you check inside the vendor folder now, you can see that a new folder called Symfony has been created.

Let’s start to implement the routing system for our MVC framework then. The goal is to display the values of the product with ID=1 when browsing the URL /product/1
Let’s create a new file called web.php under the routes folder. This file will contain all the routes of your application.

<?php 

use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

// Routes system
$routes = new RouteCollection();
$routes->add('product', new Route(constant('URL_SUBFOLDER') . '/product/{id}', array('controller' => 'ProductController', 'method'=>'showAction'), array('id' => '[0-9]+')));

We use Route and RouteCollection classes from the Symfony Routing component in order to create and list all the routes we need. We start with a single product page.

Then, let’s create the routing engine. Add a new file called Router.php inside your app folder and put this code inside it:

<?php 

namespace App;

use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\NoConfigurationException;

class Router
{
    public function __invoke(RouteCollection $routes)
    {
        $context = new RequestContext();

        // Routing can match routes with incoming requests
        $matcher = new UrlMatcher($routes, $context);
        try {
            $matcher = $matcher->match($_SERVER['REQUEST_URI']);
    
            // Cast params to int if numeric
            array_walk($matcher, function(&$param)
            {
                if(is_numeric($param)) 
                {
                    $param = (int) $param;
                }
            });
    
            // https://github.com/gmaccario/simple-mvc-php-framework/issues/2
            // Issue #2: Fix Non-static method ... should not be called statically
            $className = '\\App\\Controllers\\' . $matcher['controller'];
            $classInstance = new $className();
    
            // Add routes as paramaters to the next class
            $params = array_merge(array_slice($matcher, 2, -1), array('routes' => $routes));

            call_user_func_array(array($classInstance, $matcher['method']), $params);
            
        } catch (MethodNotAllowedException $e) {
            echo 'Route method is not allowed.';
        } catch (ResourceNotFoundException $e) {
            echo 'Route does not exists.';
        } catch (NoConfigurationException $e) {
            echo 'Configuration does not exists.';
        }
    }
}

// Invoke
$router = new Router();
$router($routes);

The code is straightforward and speaks for itself, but let’s explain it a bit: the URL matcher takes in the request URI and will check if there is a match with the routes defined in routes/web.php. If there is a match, the function call_user_func_array will do the magic, calling the right method of the right controller.
Moreover, we used the function array_walk to cast the numeric values into integer values, because in our class methods we used the explicit type declaration.

Now we can include the routes system in the index.php file:

<?php

// Autoloader
require_once '../vendor/autoload.php';

// Load Config
require_once '../config/config.php';

// Routes
require_once '../routes/web.php';
require_once '../app/Router.php';

Now that we prepared the routing system, we can browse /product/1 page and see the result. Obviously the values are now empty. Let’s add some fake values to the product:

public function read(int $id)
{
    $this->title = 'My first Product';
    $this->description = 'Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum ';
    $this->price = 2.56;
    $this->sku = 'MVC-SP-PHP-01';
    $this->image = 'https://via.placeholder.com/150';

    return $this;
}

And browse again the page /product/1

You can now add your database connection, and return the values from the database, using a raw query or an ORM like Doctrine or Eloquent.

The homepage

Let’s prepare now the homepage route. Open the routes/web.php and add the new route:

$routes->add('homepage', new Route(constant('URL_SUBFOLDER') . '/', array('controller' => 'PageController', 'method'=>'indexAction'), array()));

Obviously, we need to create the new controller PageController:

<?php 

namespace App\Controllers;

use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;

class PageController
{
    // Homepage action
	public function indexAction(RouteCollection $routes)
	{
		$routeToProduct = str_replace('{id}', 1, $routes->get('product')->getPath());

        require_once APP_ROOT . '/views/home.php';
	}
}

And the new view:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="shortcut icon" href="favicon.png">
    <title>Simple PHP MVC</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>

<body>

    <section>
        <h1>Homepage</h1>
        <p>
            <a href="<?php echo $routeToProduct ?>">Check the first product</a>
        </p>
    <section>

    <script src="https://code.jquery.com/jquery-3.6.0.min.js" 
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" 
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>

</html>

Since we use the same code for the header and footer, we can create a layout folder and separate the code for those pieces of HTML.

Improve the PHP MVC framework

Database connection and ORM, session, cookies, better page controller that accept different page parameters, or any other feature can be added very easily, but this article wants to show only the way to build a really simple PHP MVC.

Download the code

You can download the zip file, or clone the code of this article via Github.
https://github.com/gmaccario/simple-mvc-php-framework

Conclusion

Once you’re getting confident with the MVC paradigm, I suggest reading the documentation of Laravel (my favorite PHP MVC framework) or Symfony and start to get your feet wet! You’ll notice that the development becomes faster than use a pure PHP solution.

Thanks for your time, I hope you got some new information about the MVC paradigm. Let me know what do you think about this article through the comments or send me a message!

Leave a Reply

Your email address will not be published. Required fields are marked *

No comments yet. Be the first one!