Skip to content

Commit

Permalink
added part 5
Browse files Browse the repository at this point in the history
  • Loading branch information
fabpot committed Jan 11, 2012
1 parent bd3ca8e commit db0ad14
Showing 1 changed file with 190 additions and 0 deletions.
190 changes: 190 additions & 0 deletions book/part5.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
Create your own framework... on top of the Symfony2 Components (part 5)
=======================================================================

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

.. _`callbacks`: http://php.net/callback#language.types.callback
.. _`websites`: http://kottke.org/08/02/single-serving-sites

0 comments on commit db0ad14

Please sign in to comment.