In this guide, we'll go over how you how you can build your own PHP framework, with alot less code than you might be expecting. You don't need an advanced understanding of PHP, although it will help. The code is fairly uncomplicated and simple to understand, so this guide should be suitable for most experience levels.
Back in the old days, PHP had no frameworks. The code was clunky and had no strucutre. Application and model code was mixed in with the views. The code for the first version of Facebook, which has since been leaked to the web was like this. If you search the web, you'd probably able to find a copy without too much effort, but I wont link to it here.
From these dark ages, along came frameworks. Frameworks let you split your application, model and view code into separate layers and provide a basic structure for your code.
Most PHP projects use one of the popular frameworks, like Symfony or Zend. But like all "good" things in life, using a framework someone else built has downsides.
- Most frameworks come packaged with lots of libraries that you will never use
- Frameworks lock you in and are difficult to migrate away from and upgrade. Try upgrading from Zend 1 to the latest version, or Yii 1 to Yii 2. Or from Symfony to Zend or vice versa.
- Frameworks encourage Framework specific code for common operations like getting GET/POST input and validation, when you could be using framework agnostic code built into PHP, or
composer
dependencies. This makes lock in worse and makes upgrades and switching frameworks even harder. - Frameworks contain alot of extra processing that you probably don't need and this has a siginficant performance impact.
- You'll be using the libraries that the framework maintainer likes, rather than the ones you like. Prefer RedbeanPHP as your ORM when the framework uses Doctrine? You'll need to go against the framework to implement what you want.
- Adding new people to your team is harder, you need people who have worked with the specific third party framework you are using. Otherwise, they'll need to learn it on the job.
- The documentation of most frameworks is not in a good state and there are alot of undocumented quirks you might run into.
But we still need a framework, otherwise were back to the bad old days of spaghetti code. Whats the solution? Build your own lightweight framework!.
The reason why many frameworks come with so much bloat is that they were originally created in a world before composer
, so the framework tried to provide everything you might ever need.
Nowdays, we have composer
for PHP. Instead of getting a framework that provides lots of libraries that add bloat to your codebase and that you may never use, you can individually install composer
packages as needed. composer
also provides a free PSR autoloader you can use to structure your code.
There is no more need to use a third party framework. composer
dependencies and built in PHP functionality provide everything you need to create your own framework with a minimal amount of code.
Prerequisites
To build a basic MVC framework with PHP, you will need
- PHP 8.1 or later
composer
for dependencies. As you won't be using a pre-built framework, you'll only install the dependencies you'll be using and nothing else.
Set up composer
To build your own PHP framework, you'll need to get some dependencies from composer
.
Create a composer.json
file with the following contents.
{
"name": "cipher-code/phpframework",
"description": "PHP Framework",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"Framework": "src/"
}
},
"require": {
"slim/slim": "4.11.0",
"slim/psr7": " ^1.6",
"php-di/php-di": "^6.4"
}
}
Then run composer install
to install the initial dependencies and initialize your autoloader.
Set up initial public/index.php
Like most PHP applications, you'll need a index.php
script as the "Front Controller" or entry point into your application.
Create the file public/index.php
with the following contents.
<?php
// public/index.php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use DI\Container;
// This will make referencing files by path much simpler
$rootDir = realpath(__DIR__ . '/..');
define('ROOT_DIR', $rootDir);
require ROOT_DIR . '/vendor/autoload.php';
$app = AppFactory::create();
$container = new Container();
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name");
return $response;
});
$app->run();
First, we define ROOT_DIR
as a constant with we can use to refer to all other in codebase file paths in the future. No more doing __DIR__ . '/../../../Folder/file.php
. You can just start from ROOT_DIR
instead, i.e. ROOT_DIR . '/Folder/file.php
.
We then use this ROOT_DIR
constant to load composer
. Behind the scenes, composer
then sets up the autoloader
for you so you can namespace your code.
The Router
A core component of any framework is the Router, which is used to dispatch incoming requests to the right controller action. This is how Symfony and Zend might know how to route /hello/{name}
to HelloController->hello()
for example.
To build your own PHP framework, you need a Router.
We've installed Slim
, a super lightweight framework. In our code, we are only using the Slim
Router to set up the /hello/{name}
route. Slim isn't a full MVC framework, it just provides a basic set of core components such as a Router, which you can use to create your own framework.
This isn't yet a full MVC framework, because its just using a simple callback and doesn't have any Controllers yet. However, its enough to test our setup to make sure everything is wired up properly.
Confirm setup with the PHP built in web server
Start the PHP built in web server with
php -S localhost:8080 -t public public/index.php
Then hit http://localhost:8080/hello/<your name>
in your browser.
You should see a page like the one below. If not, go back and check over your work.
Create src/Controller/HelloController.php
As mentioned earlier, we are using a simple callback to respond to requests, so this isn't a full MVC framework. The next step is to add a controller. So, lets add HelloController
to handle requests to /hello/{name}
.
<?php
namespace Framework\Controller;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class HelloController
{
public function hello(Request $request, Response $response, array $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name");
return $response;
}
}
Wire up your Controller
Update index.php
to match the following. Don't forget to import the new namesapces!
<?php
// public/index.php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use DI\Container;
use Framework\Controller\HelloController;
// This will make referencing files by path much simpler
$rootDir = realpath(__DIR__ . '/..');
define('ROOT_DIR', $rootDir);
require ROOT_DIR . '/vendor/autoload.php';
$app = AppFactory::create();
$container = new Container();
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) use ($container) {
/** @var Framework\Controller\HelloController */
$helloController = $container->get(HelloController::class);
return $helloController->hello($request, $response, $args);
});
$app->run();
Here, we are using PHP-DI Autowiring to get the controller with $container->get(HelloController::class)
, instead of calling new HelloController()
, which you might try to do if I didn't tell you otherwise. This is so that we can inject dependencies with Dependency Injection later on.
Now start the PHP built in web server again. Hit http://localhost:8080/hello/<your name>
again and you should see the same result as before. If not, go back and check over your work.
Create src/Util/Greeter.php
Now we have a controller to dispatch requests, but we have a design issue.
Our controller is doing too much. Not only is it handling the output of the greeting, its also generating it itself! We should be following SOLID Principles and one of these is Single Purpose.
Right now, the greeting isn't too complicated and only supports English. Lets imagine a future where you need to do the greetings in multiple languages!. With the current setup, that means introducing lots of logic into your code. If $language
is Spanish, output the Spanish greeting and so on.
The controller would get alot bigger than it needs to be. A good principle to follow with any framework is "skinny controllers". So, you only put the minimum amount of code in your controller to handle the request. The heavy lifting should be done elsewhere.
If we offload the greeting generation to another class, we can use that as a base for a future structure where we can easily support multiple languages without much logic.
So, lets offload generating the greeting to a new Greeter
class.
<?php
// src/Util/Greeter.php
namespace Framework\Util;
class Greeter
{
public function greet(string $name) : string
{
return "Hello, $name";
}
}
Update HelloController
to use Greeter
.
<?php
// src/Controller/HelloController.php
namespace Framework\Controller;
use Framework\Util\Greeter;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
class HelloController
{
public function __construct(
protected Greeter $greeter
) {}
public function hello(Request $request, Response $response, array $args)
{
$name = $args['name'];
$response->getBody()->write($this->greeter->greet($name));
return $response;
}
}
This is our first use of Dependency Injection. In the constructor, we use PHP property promotion so we only have to define the dependency once. PHP-DI Autowiring uses Reflection to check what the class needs ahead of time, then injects it automatically at runtime.
This makes managing dependencies and adhering to SOLID Principles much easier.
Wrapping up Part 1
Now you have a simple framework with a Router, DI and a Controller. It is mostly following SOLID Principles and we don't have any tests yet.
To implement full MVC, we need a Model and View layer. This will come next in "Build Your Own PHP Framework - Part 2". If you'd like to give me extra motivation to create Part 2 sooner, share this article across all of your socials using the links provided. The more engagement I see, the faster i'll create Part 2!.