How to build a simple PHP MVC framework 4.5 (143)

MVC design pattern

By on Tue Mar 30 in MVC Architecture, PHP, Programming


Introduction

Today, I will show you how to create a simple PHP application following the MVC pattern (Model-View-Controller). For this article I was inspired by a PHP course I taught some years ago, in which I built a simple e-commerce application with the students. This e-commerce project was based on a simple PHP MVC framework. Because of this experience, people who continued coding and programming already understood what MVC means before getting their hands on a real framework.

My background

By 2010, I had already been developing software for 5 years, and I was looking for a good solution to build a web application for my boss. After discussing it with a former colleague of mine (thanks, Davide!), I started using Symfony 1.4. Before writing any code, I used the “RTFM approach” (Read The Friendly Manual…). I developed a moderately complex application in two months, including registration, ACL, dashboard, frontend, etc.

After that, during the years, I worked with Zend Framework, Symfony 2.0 and 5, and Laravel (from version 5.8), as well as microframeworks like Silex (no longer maintained) and Lumen. 

What does MVC mean?

MVC is a design pattern used to decouple data (Models), user interfaces (Views), and application logic (Controllers). 

MVC frameworks are widely used in the industry because they offer many advantages for rapid and structured development. There are MVC frameworks for most programming languages, from .NET to PHP. Unfortunately, these frameworks can have a steep learning curve. This is why I believe people must learn how to write code within a framework’s ecosystem.

To follow this guide you need a good knowledge of PHP and OOP (Object-Oriented Programming).

Build a simple PHP MVC framework

Whether you are using Docker, XAMPP, or any other development environment, let’s create a simple structure for a basic PHP MVC framework. 

I usually have a folder called Solutionsfor all my projects on my computer. Navigate to your folder, and create a new folder called simple-php-mvc. Enter that folder.

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

The .htaccess file

Open the .htaccess and put this code inside the file:

<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>

.htaccessis a configuration file for the Apache web server, and the mod_rewrite directive tells Apache that every request will end up in the index.phplocated in the folder named public

But what does this mean? It means that if you browse the web pageshttps://simple-php-mvc/, https://simple-php-mvc/page1, or https://simple-php-mvc/page2, all of them will end up executing the fileindex.phpunder the public folder, which is the entry point of your PHP MVC framework. This is a big advantage because you can now centralize your request handling: you understand which resource is requested, and how, and provide the correct response.

Another benefit: by using .htaccess to direct traffic to the public folder, the rest of your project’s structure will be hidden from anyone.

The PHP MVC framework structure

Let’s create the basic folders for your MVC:
1. app
2. config
3. public
4. views
5. routes

This is what your project looks like right now:

PHP MVC application folder structure
Folder structure

Bootstrap your PHP MVC framework

Now, we need a way to bootstrap the application and load the code you need. As we already mentioned, index.php in the public folder is the entry point. For that reason, we include the necessary code there.

First of all, let’s create a config.php in the config folder. Then, since we want to load the config file for every request, let’s add the following code to index.php:

require_once '../config/config.php';

Inside the config.php we store the settings of the framework. For example, we can store the name of our app, the basic paths, and, of course, the database connection parameters:

<?php

// Site Name
define('SITE_NAME', 'your-site-name');

// App Paths
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');

Autoloader

We want to load future classes without any hassle, i.e. without dozens of include or require statements, so we’ll use 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.

Note: It has been reported that some hosting environments require the classmap directive. Classmap autoloading will recursively go through .php and .inc files in specified directories and files and sniff for classes in them.

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/"
},
"classmap": [
"app/"
]
}
}

Assuming that you already installed composer on your computer — or container, execute the following command at the root level of your project, depending on where you are working and where your composer was installed:

composer install

If you check your root folder now, you’ll discover a new folder called vendor. The vendor folder contains the autoload.php and the composer folder.

Open the index.phpin the public folder, and add the following code at the beginning of the file:

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

From now on, because of the autoload directivepsr-4 in the composer.json, you can useAppas a starting point for your namespaces, like this:

use App\Controllers\MyController;

In the next section, we will explore the components of the MVC pattern and understand what the MVC acronym stands for.

NOTE: The goal of this article is to demonstrate how to implement a simple MVC framework using PHP, not to explain SOLID principles. Therefore, in this context, I will combine the model layer with database access. This approach is similar to the Active Record pattern, though alternatives like the Data Mapper pattern are also viable.

To summarize the Active Record pattern:

  • Each instance of the class represents a single row in the database.
  • Methods such as save, findBy, update, and delete interact directly with the database

Model

A model is an object that represents your data. It mirrors your database table structure and can handle CRUD operations such as creating, reading, updating, and deleting records (Laravel docet).

For example, consider a Products table structured as follows:

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,
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 Modelsunder appfolder. Then let’s create a new file called Product.phpunder Models. 

<?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)
{

}
}

View

A view is responsible for receiving data from the controller and displaying it. That’s its primary role. Many template engines are available for PHP, such as Twig and Blade. For this MVC tutorial, we’ll use plain HTML to keep things simple.

To create a new view, create a file named product.php in the views folder. Based on the product attributes, you can write a simple HTML file 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>
</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>
</body>

</html>

The view is ready to get the product object $productand display its values.

Controller

The controller is the core of the application logic. It is responsible for handling the input and converting it into commands for the model or view.

To start, create a new folderControllers inside the app directory. Then create a new fileProductController.php within that folder. Here is 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? 

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

Next, we need a mechanism to handle URLs. We aim to use user-friendly URLs, meaning web addresses that are easy to read and include descriptive words about the content of the webpage. This requires a routing system.

We can either create our routing system or since we’re using Composer for autoloading, take advantage of the extensive Symfony ecosystem packages to work more efficiently!

So, let’s see how we can take advantage of the Symfony Routing Component. Here is 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.
Including the Symfony Routing in our project

After checking the vendor folder, you’ll notice a new folder named Symfony has been created.

For the project, this package isn’t enough. We must install the following package too:

composer require symfony/http-foundation

Here is some explanation: https://symfony.com/doc/current/components/http_foundation.html

The HttpFoundation component defines an object-oriented layer for the HTTP specification.

From Symfony: In PHP, the request is represented by some global variables ($_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, …), and the response is generated by some functions (echo, header(), setcookie(), …).

The Symfony HttpFoundation component replaces these default PHP global variables and functions with an object-oriented layer.

Now, let’s implement the routing system for our simple MVC framework. 

Our goal is to display the product details of a product withID=1 when navigating to the URL /product/1.

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

To accomplish this, create a new file named web.php in the routes folder. This file will contain all the routes for your application. Here’s the content:

<?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]+')));

In short: This PHP code defines a route named productthat maps URLs like /{URL_SUBFOLDER}/product/{id} (where id is a numeric value) to the showAction method of ProductController.

The routing engine

OK then, let’s create the routing engine. 

Add a new file called Router.php inside your appfolder and put this code inside it:

<?php 

namespace App;

use Symfony\Component\HttpFoundation\Request;
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();
$request = Request::createFromGlobals();
$context->fromRequest(Request::createFromGlobals());

// Routing can match routes with incoming requests
$matcher = new UrlMatcher($routes, $context);
try {
$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);

// 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);

This code sets up a basic routing system that uses Symfony components to match incoming requests to appropriate controllers and methods, handles exceptions, and invokes the controller methods with the matched parameters. 

The URL matcher takes in the request URI and checks 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.

Optional query string

In our Controllers, we can easily get the values from optional parameters because of these lines:

$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);

Here is an example:

http://localhost/product/1?value1=true&value2=false

In the Controller:

var_dump($_GET['value1']);
var_dump($_GET['value2']);

Usually, you will use values from the routes, but in some cases, this solution can be useful, for example when you have a lot of parameters because of some filters (i.e. filtering, sorting, and pagination).

Let’s include the routes system in the index.php:

<?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';

We’ve prepared the routing system, we can browse the page /product/1 and see the result. But the values are now empty. Let’s add some fake values to the product, inside ProductController.php:

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;
}

Again, browse the page /product/1 and you will see the data of the product.

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.

Additional notes on routing: Your routes might overlap, especially if you define a general route like /{pageSlug} before more specific routes, such as /register. To prevent this issue, place the general route /{pageSlug} at the end of your route definitions. This way, it acts as a fallback. Alternatively, you can add a prefix to your general route, such as /static/{pageSlug} or /public/{pageSlug}, to avoid conflicts.

The homepage

Now let’s set up the homepage route. Open routes/web.php and add the following route:

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

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 then the 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>
</head>

<body>

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

</html>

Since we use the same code for the header and footer across multiple pages, we can create a layout folder to separate and organize these pieces of HTML (header.php and footer.php).

To view the page, open your browser and navigate to this URL:

http://localhost/

Note: If you installed your project in a subfolder, as many users do, you must set the URL_SUBFOLDER constant up in the config file. For example, if you installed the project inside a subfolder called simple-mvc-php-framework, your final URL would be:

http://localhost/simple-mvc-php-framework/

Improve the PHP MVC framework

Database connections, ORM, sessions, cookies, a more advanced page controller that accepts different parameters, and other features can be added quite easily. However, this article aims to demonstrate how to build a very simple PHP MVC framework.

Download the code

You can easily get started by downloading the zip file or cloning the code from this article on GitHub.

https://github.com/gmaccario/simple-mvc-php-framework

Conclusion

First of all: Thank you for taking the time to read this article! I hope I helped you to get acquainted with the MVC concepts, and that you’re now ready to start a real project, using a popular and well-known framework such as Symfony, or Laravel.

Once you feel confident with the MVC paradigm, I highly recommend exploring the documentation for Symfony or Laravel and getting hands-on experience. You’ll quickly see that development becomes much faster than using pure PHP.

If you’d like to try out the project, feel free to fork it on GitHub. If you encounter any issues, check the Issues tab or open a new one if it’s something that hasn’t been addressed yet.

Do you have any questions? You can also reach out to me directly, but I prefer to respond on GitHub to share the knowledge with others. 

The original version of my article is on my website. I’ve decided to close the comments section on the page due to a high volume of spam and duplicate questions. I hope you understand!

Note: This project is a simple starter kit, designed to work well if your environment is properly configured. I don’t use XAMPP or any pre-configured stack package. In the past (…more than 10 years ago!), I used to build my LAMP stack by installing all the software individually. Now, I use Docker. If you run into any environment-related issues, you’ll need to troubleshoot them on your own.

Once again, this is just a basic starter kit aimed at helping people understand the core concepts behind MVC. If you’re looking to build a professional project, I highly recommend using Symfony or Laravel.

If you enjoyed this article, you can rate it using the star rating system, or you can clap for it on Medium:

https://medium.com/@me_38439/how-to-build-a-simple-php-mvc-framework-d0f86acc8f9d