Skip to content

Commit db0ad14

Browse files
committed
added part 5
1 parent bd3ca8e commit db0ad14

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

book/part5.rst

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
Create your own framework... on top of the Symfony2 Components (part 5)
2+
=======================================================================
3+
4+
The astute reader has noticed that our framework hardcodes the way specific
5+
"code" (the templates) is run. For simple pages like the ones we have created
6+
so far, that's not a problem, but if you want to add more logic, you would be
7+
forced to put the logic into the template itself, which is probably not a good
8+
idea, especially if you still have the separation of concerns principle in
9+
mind.
10+
11+
Let's separate the template code from the logic by adding a new layer: the
12+
controller: *The controller mission is to generate a Response based on the
13+
information conveyed by the client Request.*
14+
15+
Change the template rendering part of the framework to read as follows::
16+
17+
<?php
18+
19+
// example.com/web/front.php
20+
21+
// ...
22+
23+
try {
24+
$request->attributes->add($matcher->match($request->getPathInfo()));
25+
$response = call_user_func('render_template', $request);
26+
} catch (Routing\Exception\ResourceNotFoundException $e) {
27+
$response = new Response('Not Found', 404);
28+
} catch (Exception $e) {
29+
$response = new Response('An error occurred', 500);
30+
}
31+
32+
As the rendering is now done by an external function (``render_template()``
33+
here), we need to pass to it the attributes extracted from the URL. We could
34+
have passed them as an additional argument to ``render_template()``, but
35+
instead, let's use another feature of the ``Request`` class called
36+
*attributes*: Request attributes lets you attach additional information about
37+
the Request that is not directly related to the HTTP Request data.
38+
39+
You can now create the ``render_template()`` function, a generic controller
40+
that renders a template when there is no specific logic. To keep the same
41+
template as before, request attributes are extracted before the template is
42+
rendered::
43+
44+
function render_template($request)
45+
{
46+
extract($request->attributes->all(), EXTR_SKIP);
47+
ob_start();
48+
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
49+
50+
return new Response(ob_get_clean());
51+
}
52+
53+
As ``render_template`` is used as an argument to the PHP ``call_user_func()``
54+
function, we can replace it with any valid PHP `callbacks`_. This allows us to
55+
use a function, an anonymous function, or a method of a class as a
56+
controller... your choice.
57+
58+
As a convention, for each route, the associated controller is configured via
59+
the ``_controller`` route attribute::
60+
61+
$routes->add('hello', new Routing\Route('/hello/{name}', array(
62+
'name' => 'World',
63+
'_controller' => 'render_template',
64+
)));
65+
66+
try {
67+
$request->attributes->add($matcher->match($request->getPathInfo()));
68+
$response = call_user_func($request->attributes->get('_controller'), $request);
69+
} catch (Routing\Exception\ResourceNotFoundException $e) {
70+
$response = new Response('Not Found', 404);
71+
} catch (Exception $e) {
72+
$response = new Response('An error occurred', 500);
73+
}
74+
75+
A route can now be associated with any controller and of course, within a
76+
controller, you can still use the ``render_template()`` to render a template::
77+
78+
$routes->add('hello', new Routing\Route('/hello/{name}', array(
79+
'name' => 'World',
80+
'_controller' => function ($request) {
81+
return render_template($request);
82+
}
83+
)));
84+
85+
This is rather flexible as you can change the Response object afterwards and
86+
you can even pass additional arguments to the template::
87+
88+
$routes->add('hello', new Routing\Route('/hello/{name}', array(
89+
'name' => 'World',
90+
'_controller' => function ($request) {
91+
// $foo will be available in the template
92+
$request->attributes->set('foo', 'bar');
93+
94+
$response = render_template($request);
95+
96+
// change some header
97+
$response->headers->set('Content-Type', 'text/plain');
98+
99+
return $response;
100+
}
101+
)));
102+
103+
Here is the updated and improved version of our framework::
104+
105+
<?php
106+
107+
// example.com/web/front.php
108+
109+
require_once __DIR__.'/../vendor/.composer/autoload.php';
110+
111+
use Symfony\Component\HttpFoundation\Request;
112+
use Symfony\Component\HttpFoundation\Response;
113+
use Symfony\Component\Routing;
114+
115+
function render_template($request)
116+
{
117+
extract($request->attributes->all(), EXTR_SKIP);
118+
ob_start();
119+
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
120+
121+
return new Response(ob_get_clean());
122+
}
123+
124+
$request = Request::createFromGlobals();
125+
$routes = include __DIR__.'/../src/app.php';
126+
127+
$context = new Routing\RequestContext();
128+
$context->fromRequest($request);
129+
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
130+
131+
try {
132+
$request->attributes->add($matcher->match($request->getPathInfo()));
133+
$response = call_user_func($request->attributes->get('_controller'), $request);
134+
} catch (Routing\Exception\ResourceNotFoundException $e) {
135+
$response = new Response('Not Found', 404);
136+
} catch (Exception $e) {
137+
$response = new Response('An error occurred', 500);
138+
}
139+
140+
$response->send();
141+
142+
To celebrate the birth of our new framework, let's create a brand new
143+
application that needs some simple logic. Our application has one page that
144+
says whether a given year is a leap year or not. When calling
145+
``/is_leap_year``, you get the answer for the current year, but the you can
146+
also specify a year like in ``/is_leap_year/2009``. Being generic, the
147+
framework does not need to be modified in any way, just create a new
148+
``app.php`` file::
149+
150+
<?php
151+
152+
// example.com/src/app.php
153+
154+
use Symfony\Component\Routing;
155+
use Symfony\Component\HttpFoundation\Response;
156+
157+
function is_leap_year($year = null) {
158+
if (null === $year) {
159+
$year = date('Y');
160+
}
161+
162+
return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100);
163+
}
164+
165+
$routes = new Routing\RouteCollection();
166+
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
167+
'year' => null,
168+
'_controller' => function ($request) {
169+
if (is_leap_year($request->attributes->get('year'))) {
170+
return new Response('Yep, this is a leap year!');
171+
}
172+
173+
return new Response('Nope, this is not a leap year.');
174+
}
175+
)));
176+
177+
return $routes;
178+
179+
The ``is_leap_year()`` function returns ``true`` when the given year is a leap
180+
year, ``false`` otherwise. If the year is null, the current year is tested.
181+
The controller is simple: it gets the year from the request attributes, pass
182+
it to the `is_leap_year()`` function, and according to the return value it
183+
creates a new Response object.
184+
185+
As always, you can decide to stop here and use the framework as is; it's
186+
probably all you need to create simple websites like those fancy one-page
187+
`websites`_ and hopefully a few others.
188+
189+
.. _`callbacks`: http://php.net/callback#language.types.callback
190+
.. _`websites`: http://kottke.org/08/02/single-serving-sites

0 commit comments

Comments
 (0)