Templating ¶
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's mission is to generate a Response based on the information conveyed by the client's Request.
Change the template rendering part of the framework to read as follows:
1 2 3 4 5 6 7 8 9 10 11 |
// example.com/web/front.php
// ...
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$response = call_user_func('render_template', $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$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 is a way to 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:
1 2 3 4 5 6 7 8 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$routes->add('hello', new Routing\Route('/hello/{name}', [
'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 $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
|
A route can now be associated with any controller and within a controller, you
can still use the render_template()
to render a template:
1 2 3 4 5 6 |
$routes->add('hello', new Routing\Route('/hello/{name}', [
'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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$routes->add('hello', new Routing\Route('/hello/{name}', [
'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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// example.com/web/front.php
require_once __DIR__.'/../vendor/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 $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$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, create a new
app.php
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// example.com/src/app.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
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}', [
'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 does little: 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.