Templating
Warning: You are browsing the documentation for Symfony 7.0, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
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 $request): Response
{
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 $request): string {
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 $request): Response {
// $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 $request): Response
{
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 27
// example.com/src/app.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
function is_leap_year(?int $year = null): bool
{
if (null === $year) {
$year = (int)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 $request): Response {
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.