PHP
Article

Build your own PHP Framework with Symfony Components

By Léonard Hetsch

You’ve probably met Symfony in your PHP career – or have at least heard of it. What you may not know is that Symfony is, at its core, composed of separate libraries called components, which can be reused in any PHP application.

For example, the popular PHP framework Laravel was developed using several Symfony components we will also be using in this tutorial. The next version of the popular CMS Drupal is also being built on top of some of the main Symfony components.

We’ll see how to build a minimal PHP framework using these components, and how they can interact to create a basic structure for any web application.

Note: This tutorial won’t cover every Symfony component and every feature of each one. We’ll see only the main things that we need to build a minimal functional framework. If you want to go deeper into Symfony components, I encourage you to read their excellent documentation.

Creating the project

We’ll start from scratch with a simple index.php file at the root of our project directory, and use Composer to install the dependencies.

For now, our file will only contain this simple piece of code:

switch($_SERVER['PATH_INFO']) {
        case '/':
            echo 'This is the home page';
            break;
        case '/about':
            echo 'This is the about page';
            break;   
        default:
            echo 'Not found!';
    }

This code just maps the requested URL (contained in $_SERVER['PATH_INFO']) to the right echo instruction. It’s a very, very primitive router.

The HttpFoundation component

HttpFoundation acts as a top-level layer for dealing with the HTTP flow. Its most important entrypoints are the two classes Request and Response.

Request allows us to deal with the HTTP request information such as the requested URI or the client headers, abstracting default PHP globals ($_GET, $_POST, etc.). Response is used to send back response HTTP headers and data to the client, instead of using header or echo as we would in “classic” PHP.

Install it using composer :

php composer.phar require symfony/http-foundation 2.5.*

This will place the library into the vendor directory. Now put the following into the index.php file:

// Initializes the autoloader generated by composer
    $loader = require 'vendor/autoload.php';
    $loader->register();

    use Symfony\Component\HttpFoundation\Request;
    
    $request = Request::createFromGlobals();
    
    switch($request->getPathInfo()) {
        case '/':
            echo 'This is the home page';
            break;
        case '/about':
            echo 'This is the about page';
            break;   
        default:
            echo 'Not found!';
    }

What we did here is pretty straightforward:

  • Create a Request instance using the createFromGlobals static method. Instead of creating an empty object, this method populates a Request object using the current request information.
  • Test the value returned by the getPathInfo method.

We can also replace the different echo commands by using a Response instance to hold our content, and send it to the client (which basically outputs the response headers and content).

$loader = require 'vendor/autoload.php';
    $loader->register();

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    
    $request = Request::createFromGlobals();
    $response = new Response();
    
    switch ($request->getPathInfo()) {
    case '/':
    	$response->setContent('This is the website home');
    	break;
    
    	case '/about':
    		$response->setContent('This is the about page');
    		break;
    
    	default:
    		$response->setContent('Not found !');
    	$response->setStatusCode(Response::HTTP_NOT_FOUND);
    }
    
    $response->send();

Use HttpKernel to wrap the framework core

php composer.phar require symfony/http-kernel 2.5.*

For now, as simple as it is, the framework logic is still located in our front controller, the index.php file. If we wanted to add more code, it would be better to wrap it into another class, which would become the “core” of our framework.

The HttpKernel component was conceived with that goal in mind. It is intended to work with HttpFoundation to convert the Request instance to a Response one, and provides several classes for us to achieve this. The only one we will use, for the moment, is the HttpKernelInterface interface. This interface defines only one method: handle.

This method takes a Request instance as an argument, and is supposed to return a Response. So, each class implementing this interface is able to process a Request and return the appropriate Response object.

Let’s create the class Core of our framework that implements the HttpKernelInterface. Now create the Core.php file under the lib/Framework directory:

<?php
    
    namespace Framework;

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\HttpKernelInterface;

    class Core implements HttpKernelInterface
    {
    	public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    	{
    		switch ($request->getPathInfo()) {
    			case '/':
    				$response = new Response('This is the website home');
    				break;
    
    			case '/about':
    				$response = new Response('This is the about page');
    				break;
    
    			default:
    				$response = new Response('Not found !', Response::HTTP_NOT_FOUND);
    		}
    
    		return $response;
    	}
    }

Note: The handle method takes two more optional arguments: the request type, and a boolean indicating if the kernel should throw an exception in case of error. We won’t use them in this tutorial, but we need to implement the exact same method defined by HttpKernelInterface, otherwise PHP will throw an error.

The only thing we did here is move the existing code into the handle method. Now we can get rid of this code in index.php and use our freshly created class instead:

require 'lib/Framework/Core.php';
  
    $request = Request::createFromGlobals();
    
    // Our framework is now handling itself the request
    $app = new Framework\Core();
    
    $response = $app->handle($request);
    $response->send();

A better routing system

There is still a problem with our class: it is holding the routing logic of our application. If we wanted to add more URLs to match, we would have to modify the code inside our framework – which is clearly not a good idea. Moreover, this would mean adding a case block for each new route. No, we definitely don’t want to go down that dirty road.

The solution is to add a routing system to our framework. We can do this by creating a map method that binds a URI to a PHP callback that will be executed if the right URI is matched:

class Core implements HttpKernelInterface
	{
		protected $routes = array();
	
		public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
		{
			$path = $request->getPathInfo();
			
			// Does this URL match a route?
			if (array_key_exists($path, $this->routes)) {
			    // execute the callback
				$controller = $routes[$path];
				$response = $controller();
			} else {
			    // no route matched, this is a not found.
				$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
			}
	
			return $response;
		}
		
		// Associates an URL with a callback function
		public function map($path, $controller) {
			$this->routes[$path] = $controller;
		}
	}

Now application routes can be set directly in the front controller:

$app->map('/', function () {
		return new Response('This is the home page');
	});
	
	$app->map('/about', function () {
		return new Response('This is the about page');
	});
	
	$response = $app->handle($request);

This tiny routing system is working well, but it has major flaws: what if we wanted to match dynamic URLs that hold parameters? We could imagine a URL like posts/:id where :id is a variable parameter that could map to a post ID in a database.

We need a more flexible and powerful system: that’s why we’ll use the Symfony Routing component.

php composer.phar require symfony/routing 2.5.*

Using the Routing component allows us to load Route objects into a UrlMatcher that will map the requested URI to a matching route. This Route object can contain any attributes that can help us execute the right part of the application. In our case, such an object will contain the PHP callback to execute if the route matches. Also, any dynamic parameters contained in the URL will be present in the route attributes.

In order to implement this, we need to do the following changes:

  • Replace the routes array with a RouteCollection instance to hold our routes.
  • Change the map method so it registers a Route instance into this collection.
  • Create a UrlMatcher instance and tell it how to match its routes against the requested URI by providing a context to it, using a RequestContext instance.
use Symfony\Component\Routing\Matcher\UrlMatcher;
		use Symfony\Component\Routing\RequestContext;
		use Symfony\Component\Routing\RouteCollection;
		use Symfony\Component\Routing\Route;
		use
		Symfony\Component\Routing\Exception\ResourceNotFoundException;

		class Core implements HttpKernelInterface
		{
			/** @var RouteCollection */
			protected $routes;

			public function __construct()
			{
				$this->routes = new RouteCollection();
			}
		
			public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
			{
			    // create a context using the current request
				$context = new RequestContext();
				$context->fromRequest($request);
				
				$matcher = new UrlMatcher($this->routes, $context);
		
				try {
					$attributes = $matcher->match($request->getPathInfo());
					$controller = $attributes['controller'];
					$response = $controller();
				} catch (ResourceNotFoundException $e) {
					$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
				}
		
				return $response;
			}
		
			public function map($path, $controller) {
				$this->routes->add($path, new Route(
					$path,
					array('controller' => $controller)
				));
			}
		}

The match method tries to match the URL against a known route pattern, and returns the corresponding route attributes in case of success. Otherwise it throws a ResourceNotFoundException that we can catch to display a 404 page.

We can now take advantage of the Routing component to retrieve any URL parameters. After getting rid of the controller attribute, we can call our callback function by passing other parameters as arguments (using the call_user_func_array function):

try {
			$attributes = $matcher->match($request->getPathInfo());
			$controller = $attributes['controller'];
			unset($attributes['controller']);
			$response = call_user_func_array($controller, $attributes);
		} catch (ResourceNotFoundException $e) {
			$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
		}

		return $response;
	}

We can now easily handle dynamic URLs like this:

$app->map('/hello/{name}', function ($name) {
		return new Response('Hello '.$name);
	});

Note that this is very similar to what the Symfony full-stack framework is doing: we inject URL parameters into the right controller.

Hooking into the framework

The Symfony framework also provides various way to hook into the request lifecycle and to change it. A good example is the security layer intercepting a request which attempts to load an URL between a firewall.

All of this is possible thanks to the EventDispatcher component, which allows different components of an application to communicate implementing the Observer pattern.

php composer.phar require symfony/event-dispatcher 2.5

At the core of it, there is the EventDispatcher class, which registers listeners of a particular event. When the dispatcher is notified of an event, all known listeners of this event are called. A listener can be any valid PHP callable function or method.

We can implement this in our framework by adding a property dispatcher that will hold an EventDispatcher instance, and an on method, to bind an event to a PHP callback. We’ll use the dispatcher to register the callback, and to fire the event later in the framework.

use Symfony\Component\Routing\Matcher\UrlMatcher;
		use Symfony\Component\Routing\RequestContext;
		use Symfony\Component\Routing\RouteCollection;
		use Symfony\Component\Routing\Route;
		use Symfony\Component\Routing\Exception\ResourceNotFoundException;
		use Symfony\Component\EventDispatcher\EventDispatcher

		class Core implements HttpKernelInterface
		{
			/** @var RouteCollection */
			protected $routes;

			public function __construct()
			{
				$this->routes = new RouteCollection();
				$this->dispatcher = new EventDispatcher();
			}
			
			// ... 

			public function on($event, $callback)
			{
				$this->dispatcher->addListener($event, $callback);
			}
		}

We are now able to register listeners, which are just simple PHP callbacks. Let’s write now a fire method which will tell our dispatcher to notify all the listeners he knows when some event occurs.

public function fire($event)
    {
	    return $this->dispatcher->dispatch($event);
	}

In less than ten lines of code, we just added a nice event listener system to our framework, thanks to the EventDispatcher component.

The dispatch method also takes a second argument, which is the dispatched event object. Every event inherits from the generic Event class, and is used to hold any information related to it.

Let’s write a RequestEvent class, which will be immediately fired when a request is handled by the framework. Of course, this event must have access to the current request, using an attribute holding a Request instance.

namespace Framework\Event;
	
	use Symfony\Component\HttpFoundation\Request;
	use Symfony\Component\EventDispatcher\Event;

	class RequestEvent extends Event
	{
		protected $request;

		public function setRequest(Request $request)
		{
			$this->request = $request;
		}
	
		public function getRequest()
		{
			return $this->request;
		}
	}

We can now update the code in the handle method to fire a RequestEvent event to the dispatcher every time a request is received.

public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
			{
				$event = new RequestEvent();
				$event->setRequest($request);
	
				$this->dispatcher->dispatch('request', $event);
				// ...
			}

This way, all called listeners will be able to access the RequestEvent object and also the current Request. For the moment, we wrote no such listener, but we could easily imagine one that would check if the requested URL has restricted access, before anything else happens.

$app->on('request', function (RequestEvent $event) {
		// let's assume a proper check here
		if ('admin' == $event->getRequest()->getPathInfo()) {
			echo 'Access Denied!';
			exit;
		}
	});

This is a very basic security system, but you could imagine implementing anything you want, because we now have the ability to hook into the framework at any moment, which makes it much more scalable.

Conclusion

You’ve seen, by reading this tutorial, that Symfony components are great standalone libraries. Moreover, they can interact together to build a framework that fits your needs. There are many more of them which are really interesting, like the DependencyInjection component or the Security component.

Of course, full-stack frameworks such as Symfony itself or Laravel have pushed these components to their limits, to create the powerful tools we know today.

  • http://rezashadman.ir/ Reza Shadman

    Greeat article!

  • http://www.yairodriguez.com Yair Rodríguez

    Cool! ;)

  • exfromtheleft

    makes me think of silex?

    • Leonard Hetsch

      Indeed this is very similar of what does Silex. This is sort of intended, because Silex is another great example of what you can build using the components

  • Romain G.

    Nice!!

  • http://mmoreram.com Marc Morera Merino

    IMHO has no sense to build a framework these days. There are lot of frameworks, already good frameworks, that work in the top of Symfony Components (Symfony Framework, Silex…), so starting a new one should be only a good exercise to practice and understand all these components.

    Good article, btw :)

    • Leonard Hetsch

      You’re definitely right, it would not be a good idea to build such a home-made framework in a real production context. As you said, this is only for the purpose of learning :)

      • http://mmoreram.com Marc Morera Merino

        I really like the idea of doing that kind of exercises, I think it is the best way to learn these concepts :D

        • exfromtheleft

          The best way to understand a framework like symfony and to use it well, is to go through such an exercise

    • Peter Tasker

      I disagree, I think that this kind of thinking is great. If I only need a small portion of Symfony for my app, why would I include the whole framework that has tonnes of features I’m not using? Rolling your own framework with prebuilt, tested components allows you to have a stable base while keeping performance in mind. Great article.

      • http://mmoreram.com Marc Morera Merino

        I have the impression that lot of people think that when you are working with a framework you must use every part of it. Otherwise you are doing wrong. And this, imho, is not right enough.

        A framework is a set of tools, right, but the greatest feature of a framework is that provides an environment, and this environment is what really needs to be tested. Symfony is very big, yes, have tons of possibilities, some of them useless in some kind of projects, but this is what a Framework should be, an environment.

        Then you must use what you really need. You need something smaller? Right, maybe Silex, or Laravel, why not, but if you work with your own framework… I’ll tell you something…

        Welcome to the jungle.

        * Think it ( Maybe your decisions are better… but just maybe )
        * Develop it ( Maybe you have a lot of time and resources… but just maybe )
        * Test it ( Just double your resources )
        * Test it okay ( Double them again )
        * Call 1 million friends to make them use it, just to be sure that your framework is already tested properly ( I do not have these million friends… )
        * Then… the best part… Document it! Dude, is the most amazing part of a project, for U, for your actual coworkers, for your future coworkers…
        * Finally… maintain it :)

        Oh wait… are you sure you will really have time for what is really important of your work? Your project?

        Conclusion. Do not create another framework. You better should fit your needs in an existing framework with a great community, know the framework as much as you can, just to be sure you’re doing really right, and finally, dedicate your time in what is really important. Your project :)

  • Jory Geerts

    Nice article that shows the most fundamental parts of a Symfony2 based web framework.
    Fabien Potencier (the guy who created Symfony) wrote a series called “Create your own framework.. on top of Symfony2 components” that really digs into the process of creating a simple framework on top of Symfony2. It is a much longer read, but very much worth it if you have the time and interest. http://fabien.potencier.org/article/50/create-your-own-framework-on-top-of-the-symfony2-components-part-1
    Also, Igor Wiedler has a “Silex Anatomy” at the PHP Benelux conference a few year ago in which he discusses something similar to this. https://www.youtube.com/watch?v=9VUoIruQNMg

  • http://maastermedia.com Peter Kokot

    Magic and usefulness of Symfony components is astonishing, really.

  • Garrett W.

    The code snippets here promote some bad anti-patterns, including “courier” and a lack of dependency injection. You don’t have to implement Symfony’s DI component to exemplify how basic DI should be done. Just move all of the “new” keywords out of the constructors.

  • http://setkyar.github.io Set Kyar Wa Lar

    I just follow with your tutorials but I can’t pass `Interface ‘FrameworkHttpKernelInterface’ not found in lib/Framework/Core.php on line 13` :( What is wrong with me?

    • http://www.permanaj.net/ Permana Jayanta

      I think you need to require /vendor/autoload.php before require framework/core.php

  • Alagu

    Hi,

    It is really a nice article. It helped me to create such a nice code. I am facing some problems when I try to create a function call like this,

    index.php:
    ————
    $maincontroller = new MainController();
    $app->map(‘/about’, $maincontroller->test());

    And in the MainController class, return the response like this,

    MainController.php:
    ————————
    class MainController {
    function test() { return new Response(‘Success’); }
    }

    But I am getting 2 errors like,

    error1:
    call_user_func_array() expects parameter 1 to be a valid callback, no array or string given in….. $response = call_user_func_array($controller, $attributes);…….FrameworkCore.php

    error2:

    Call to a member function send() on a non-object in……………..$response->send();………………. index.php

    Could you please suggest me, the mistakes or how can I call own class functions from frontcontroller(index.php)…?

  • astroanu

    This is exactly how Laravel was written

  • https://www.hrace009.com hrace009

    when i create this code:
    switch($_SERVER['PATH_INFO']) {
    case '/':
    echo 'This is the home page';
    break;
    case '/about':
    echo 'This is the about page';
    break;
    default:
    echo 'Not found!';
    }

    i got :

    Notice: Undefined index: PATH_INFO in F:XAMPPhtdocspublicdomain_comindex.php on line 3
    Not found!

    # php -v
    PHP 5.6.8 (cli) (built: Apr 15 2015 15:07:09)
    Copyright (c) 1997-2015 The PHP Group
    Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies

  • cobenan J

    Nice article that shows how to make use of Symfony components. I was looking for this to build my own toolkit for my personal use. Many thanks.

  • Kim Stalsberg Steinhaug

    First the PATH_INFO is not set before a url is triggered, so you could mention that you try requesting the page in the broser like: http://localhost/index.php/about

    Then I got to the “chapter” A better routing system before I got undefined variable routes and fatal error function name must be a string.

    It would be real nice if you could include sample files or atleast the complete source for each file as I did not understand it well enough to get through your tutorial.

    Im guessing this framework is great – and I am having the feeling I am about to undertand it one of theese days….

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.