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 things” 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:
- app
- config
- public
- views
- 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:
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 is 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');
Autoloader
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.
It was reported that some hosting needs the classmap directive: classmap autoloading will recursively go through .php and .inc files in specified directories and files and sniff for classes in them.
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/"
},
"classmap": [
"app/"
]
}
}
Then, 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 can see a new folder called vendor that contains the autoload.php file and the composer folder.
Open the index.php under the public folder, 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 converting 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 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? 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 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.
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.
Isn’t enough: we must install also this package:
composer require symfony/http-foundation
Here some explanation: https://symfony.com/doc/current/components/http_foundation.html
The HttpFoundation component defines an object-oriented layer for the HTTP specification.
And again 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.
***
OK 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\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);
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.
Optional Querystring
Because of these lines:
$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);
in our Controllers we can easily get the values from optional parameters, like this:
URL:
http://localhost/product/1?value1=true&value2=false
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.
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 (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;
}
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.
Additional notes to the routing: your routes could overlap with each other, for example, if you write /{pageSlug} before any other route, such as /register. You can easily overcome this problem simply by writing your general route /{pageSlug} at the end of all the routes. This route will become your fallback. Or, another solution is to add a prefix, such as /static/{pageSlug} or /public/{pageSlug}.
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.
In order to navigate the page, open your browser and browse this URL:
http://localhost/
The exact URL depends on your settings. I use Docker for my local environment, and I usually set up the port, then my URL could be slightly different from yours, for example, I use
http://localhost:8887/
Note: if you installed your project into a subfolder as many users seem to do, you must set up URL_SUBFOLDER constant in the config file. For example, if you installed this project inside a subfolder called simple-mvc-php-framework, your final URL will be:
http://localhost/simple-mvc-php-framework/
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.
Note: this project is a simple starter kit, and it works well if your environment is correctly configured. I don’t use XAMPP, or another pre-configured stack package. In the past I used to create my LAMP installing the software individually, now I am using Docker. Then if you have some issue with your environment, it’s up on you to solve your issue.
And again: this is a simple starter kit with the goal of helping people to understand some concepts behind the MVC. If you want to create a professional project, go for Laravel or Symfony.