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.
Key Takeaways
- Utilize Symfony components to construct a custom PHP framework, enhancing flexibility and control over your application’s architecture.
- Implement the HttpFoundation component to manage HTTP request and response objects more effectively, replacing traditional PHP globals.
- Enhance routing capabilities by integrating Symfony’s Routing component, allowing for dynamic URL parameter handling and cleaner URL structures.
- Leverage the EventDispatcher component to design a modular and extendable application using the Observer pattern, facilitating easier maintenance and scalability.
- Streamline the process of handling requests and generating responses by using the HttpKernel component, which simplifies the creation of a core framework class that processes all incoming requests.
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 thecreateFromGlobals
static method. Instead of creating an empty object, this method populates aRequest
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 aRouteCollection
instance to hold our routes. - Change the
map
method so it registers aRoute
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 aRequestContext
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.
Frequently Asked Questions (FAQs) about Building a PHP Framework with Symfony Components
What are the key components of Symfony that are essential for building a PHP framework?
Symfony is a set of reusable PHP components that can be used to build robust PHP frameworks. Some of the key components include HttpFoundation, which provides an object-oriented layer for the HTTP specification; EventDispatcher, which allows for designing applications with a Domain-Driven Design approach; and Routing, which maps an HTTP request to a set of configuration variables. Other essential components include DependencyInjection for managing services and configurations, and Console for creating command line interfaces.
How does the HttpFoundation component work in Symfony?
The HttpFoundation component in Symfony replaces the traditional PHP globals, such as $_GET, $_POST, and $_SERVER, with an object-oriented layer. This provides more flexibility and control over the HTTP request and response. It allows you to handle request and response data in a more structured and secure manner, making your application more robust and reliable.
How can I use the EventDispatcher component in Symfony?
The EventDispatcher component in Symfony provides a simple way to implement the Observer pattern in your applications. You can create listeners or subscribers for specific events in your application, and these listeners will be notified whenever those events occur. This allows for a decoupled and modular design, making your application easier to maintain and extend.
What is the role of the Routing component in Symfony?
The Routing component in Symfony is responsible for mapping an HTTP request to a set of configuration variables. It allows you to define routes in your application, which are essentially URLs that are linked to specific controller actions. This provides a clean and user-friendly URL structure for your application, improving its usability and SEO.
How does the DependencyInjection component help in managing services and configurations?
The DependencyInjection component in Symfony provides a powerful and flexible way to manage services and configurations in your application. It allows you to define services as objects that perform specific tasks, and these services can be injected into other services or controllers as dependencies. This promotes a decoupled and testable codebase, making your application more robust and maintainable.
How can I create command line interfaces with the Console component in Symfony?
The Console component in Symfony allows you to create command line interfaces for your application. You can define commands, which are essentially PHP classes, and these commands can be executed from the command line. This provides a powerful way to interact with your application, allowing you to perform tasks such as database migrations, cron jobs, and more.
How can I install and use Symfony components in my PHP project?
Symfony components can be easily installed and used in your PHP project using Composer, a dependency management tool for PHP. You can require the specific Symfony components that you need in your composer.json file, and then run the composer install command to install them. Once installed, you can use these components in your PHP code by including the appropriate namespaces.
How can I handle errors and exceptions in Symfony?
Symfony provides a robust error and exception handling mechanism through its Debug and ErrorHandler components. These components allow you to handle errors and exceptions in a consistent and user-friendly manner, providing detailed error messages and stack traces for debugging purposes. You can also customize the error handling behavior to suit your application’s needs.
How can I secure my Symfony application?
Symfony provides several security features through its Security component. This includes authentication, authorization, CSRF protection, and more. You can configure these security features in your application’s security.yml file, and Symfony will handle the rest. This ensures that your application is secure and resistant to common security vulnerabilities.
How can I test my Symfony application?
Symfony provides a testing framework through its PHPUnitBridge and BrowserKit components. These components allow you to write unit tests and functional tests for your application, ensuring that your code is working as expected. You can run these tests using the phpunit command, and Symfony will provide detailed test reports for debugging and continuous integration purposes.
Leonard is a web developer from France, currently based in London and working as a backend engineer at Once Dating. Working on more personal projects, he loves to experiment with other technologies like Go or Elixir.