Create your own framework... on top of the Symfony2 Components (part 5)

Fabien Potencier

January 11, 2012

This article is part of a series of articles that explains how to create a framework with the Symfony2 Components: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.

The astute reader has noticed that our framework hardcodes the way specific "code" (the templates) is run. For simple pages like the ones we have created so far, that's not a problem, but if you want to add more logic, you would be forced to put the logic into the template itself, which is probably not a good idea, especially if you still have the separation of concerns principle in mind.

Let's separate the template code from the logic by adding a new layer: the controller: The controller mission is to generate a Response based on the information conveyed by the client Request.

Change the template rendering part of the framework to read as follows:

<?php
 
// example.com/web/front.php
 
// ...
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func('render_template', $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 

As the rendering is now done by an external function (render_template() here), we need to pass to it the attributes extracted from the URL. We could have passed them as an additional argument to render_template(), but instead, let's use another feature of the Request class called attributes: Request attributes lets you attach additional information about the Request that is not directly related to the HTTP Request data.

You can now create the render_template() function, a generic controller that renders a template when there is no specific logic. To keep the same template as before, request attributes are extracted before the template is rendered:

function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    return new Response(ob_get_clean());
}
 

As render_template is used as an argument to the PHP call_user_func() function, we can replace it with any valid PHP callbacks. This allows us to use a function, an anonymous function, or a method of a class as a controller... your choice.

As a convention, for each route, the associated controller is configured via the _controller route attribute:

$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => 'render_template',
)));
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 

A route can now be associated with any controller and of course, within a controller, you can still use the render_template() to render a template:

$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => function ($request) {
        return render_template($request);
    }
)));
 

This is rather flexible as you can change the Response object afterwards and you can even pass additional arguments to the template:

$routes->add('hello', new Routing\Route('/hello/{name}', array(
    'name' => 'World',
    '_controller' => function ($request) {
        // $foo will be available in the template
        $request->attributes->set('foo', 'bar');
 
        $response = render_template($request);
 
        // change some header
        $response->headers->set('Content-Type', 'text/plain');
 
        return $response;
    }
)));
 

Here is the updated and improved version of our framework:

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../vendor/.composer/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
 
function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    return new Response(ob_get_clean());
}
 
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 
$response->send();
 

To celebrate the birth of our new framework, let's create a brand new application that needs some simple logic. Our application has one page that says whether a given year is a leap year or not. When calling /is_leap_year, you get the answer for the current year, but you can also specify a year like in /is_leap_year/2009. Being generic, the framework does not need to be modified in any way, just create a new app.php file:

<?php
 
// example.com/src/app.php
 
use Symfony\Component\Routing;
use Symfony\Component\HttpFoundation\Response;
 
function is_leap_year($year = null) {
    if (null === $year) {
        $year = date('Y');
    }
 
    return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100);
}
 
$routes = new Routing\RouteCollection();
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => function ($request) {
        if (is_leap_year($request->attributes->get('year'))) {
            return new Response('Yep, this is a leap year!');
        }
 
        return new Response('Nope, this is not a leap year.');
    }
)));
 
return $routes;
 

The is_leap_year() function returns true when the given year is a leap year, false otherwise. If the year is null, the current year is tested. The controller is simple: it gets the year from the request attributes, pass it to the is_leap_year() function, and according to the return value it creates a new Response object.

As always, you can decide to stop here and use the framework as is; it's probably all you need to create simple websites like those fancy one-page websites and hopefully a few others.

Discussion

gravatar jorge meireles  — January 12, 2012 01:34   #1
Thx alot for writting this series! Can't wait for the next one! big hug
gravatar gabriel alonso  — January 12, 2012 05:04   #2
Thankyou for sharing your knowledgment!
gravatar Nicolas Béhier-Dévigne  — January 12, 2012 08:24   #3
We didn't have our christmas gift (24 hours avent calendar ;-) ), but this book is a so great and useful resource ! Thanks a lot for this community gift !
gravatar Gabriel Birke  — January 12, 2012 14:20   #4
What if the _controller attribute is left out by mistake or I want to have some default function, for example "render_template"? Wouldn't it be better to do

call_user_func($request->attributes->get('_controller', 'render_template'), $request);

Otherwise call_user_func may fail with a nasty error message (which may be a good thing).
gravatar Fabien Potencier  — January 12, 2012 15:20   #5
@Gabriel: Most of your (good) questions are answered in one of the following parts.