DEV Community

Cover image for Build Your Own PHP Framework Step By Step - Part 1
Robbie Cahill
Robbie Cahill

Posted on • Originally published at softwareengineeringstandard.com

Build Your Own PHP Framework Step By Step - Part 1

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Hello World

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
vlayosek profile image
Vladimir Kožíšek

Hi Robbie, thanks for this post (sorry for my english, i speak spanish), please help me, when i write composer install, i get this message "A non-empty PSR-4 prefix must end with a namespace separator"

and when i try up the server, i get this message require(C:\laragon\www\php_framework/vendor/autoload.php): because the autoload.php was not created... i dont know how resolve this error...

thanks so much

Collapse
 
emicheldev profile image
e.Michel

Cool

Collapse
 
thomasmoraes02 profile image
Thomas Moraes

It's cool !!

Collapse
 
777marc profile image
Marc Mendoza

Great stuff here @robbiecahill !

Collapse
 
sudipmodi profile image
SUDIP MODI

This was great thanks!