How to build a simple PHP MVC framework
By Giuseppe Maccario 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 Solutions
for 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>
.htaccess
is a configuration file for the Apache web server, and the mod_rewrite
directive tells Apache that every request will end up in the index.php
located 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.php
under 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:
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.php
in 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 useApp
as 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
, anddelete
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 Models
under app
folder. Then let’s create a new file called Product.php
under 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 $product
and 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
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 product
that 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 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);
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