Skip to content

Commit fdb195c

Browse files
committed
added part 6
1 parent db0ad14 commit fdb195c

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed

book/part6.rst

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
Create your own framework... on top of the Symfony2 Components (part 6)
2+
=======================================================================
3+
4+
You might think that our framework is already pretty solid and you are
5+
probably right. But let's see how we can improve it nonetheless.
6+
7+
Right now, all our examples use procedural code, but remember that controllers
8+
can be any valid PHP callbacks. Let's convert our controller to a proper
9+
class::
10+
11+
class LeapYearController
12+
{
13+
public function indexAction($request)
14+
{
15+
if (is_leap_year($request->attributes->get('year'))) {
16+
return new Response('Yep, this is a leap year!');
17+
}
18+
19+
return new Response('Nope, this is not a leap year.');
20+
}
21+
}
22+
23+
Update the route definition accordingly::
24+
25+
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
26+
'year' => null,
27+
'_controller' => array(new LeapYearController(), 'indexAction'),
28+
)));
29+
30+
The move is pretty straightforward and makes a lot of sense as soon as you
31+
create more pages but you might have noticed a non-desirable side-effect...
32+
The ``LeapYearController`` class is *always* instantiated, even if the
33+
requested URL does not match the ``leap_year`` route. This is bad for one main
34+
reason: performance wise, all controllers for all routes must now be
35+
instantiated for every request. It would be better if controllers were
36+
lazy-loaded so that only the controller associated with the matched route is
37+
instantiated.
38+
39+
To solve this issue, and a bunch more, let's install and use the HttpKernel
40+
component::
41+
42+
{
43+
"require": {
44+
"symfony/class-loader": "2.1.*",
45+
"symfony/http-foundation": "2.1.*",
46+
"symfony/routing": "2.1.*",
47+
"symfony/http-kernel": "2.1.*"
48+
}
49+
}
50+
51+
The HttpKernel component has many interesting features, but the one we need
52+
right now is the *controller resolver*. A controller resolver knows how to
53+
determine the controller to execute and the arguments to pass to it, based on
54+
a Request object. All controller resolvers implement the following interface::
55+
56+
namespace Symfony\Component\HttpKernel\Controller;
57+
58+
interface ControllerResolverInterface
59+
{
60+
function getController(Request $request);
61+
62+
function getArguments(Request $request, $controller);
63+
}
64+
65+
The ``getController()`` method relies on the same convention as the one we
66+
have defined earlier: the ``_controller`` request attribute must contain the
67+
controller associated with the Request. Besides the built-in PHP callbacks,
68+
``getController()`` also supports strings composed of a class name followed by
69+
two colons and a method name as a valid callback, like 'class::method'::
70+
71+
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
72+
'year' => null,
73+
'_controller' => 'LeapYearController::indexAction',
74+
)));
75+
76+
To make this code work, modify the framework code to use the controller
77+
resolver from HttpKernel::
78+
79+
use Symfony\Component\HttpKernel;
80+
81+
$resolver = new HttpKernel\Controller\ControllerResolver();
82+
83+
$controller = $resolver->getController($request);
84+
$arguments = $resolver->getArguments($request, $controller);
85+
86+
$response = call_user_func_array($controller, $arguments);
87+
88+
.. note::
89+
90+
As an added bonus, the controller resolver properly handles the error
91+
management for you: when you forget to define a ``_controller`` attribute
92+
for a Route for instance.
93+
94+
Now, let's see how the controller arguments are guessed. ``getArguments()``
95+
introspects the controller signature to determine which arguments to pass to
96+
it by using the native PHP `reflection`_.
97+
98+
The ``indexAction()`` method needs the Request object as an argument.
99+
```getArguments()`` knows when to inject it properly if it is type-hinted
100+
correctly::
101+
102+
public function indexAction(Request $request)
103+
104+
// won't work
105+
public function indexAction($request)
106+
107+
More interesting, ``getArguments()`` is also able to inject any Request
108+
attribute; the argument just needs to have the same name as the corresponding
109+
attribute::
110+
111+
public function indexAction($year)
112+
113+
You can also inject the Request and some attributes at the same time (as the
114+
matching is done on the argument name or a type hint, the arguments order does
115+
not matter)::
116+
117+
public function indexAction(Request $request, $year)
118+
119+
public function indexAction($year, Request $request)
120+
121+
Finally, you can also define default values for any argument that matches an
122+
optional attribute of the Request::
123+
124+
public function indexAction($year = 2012)
125+
126+
Let's just inject the ``$year`` request attribute for our controller::
127+
128+
class LeapYearController
129+
{
130+
public function indexAction($year)
131+
{
132+
if (is_leap_year($year)) {
133+
return new Response('Yep, this is a leap year!');
134+
}
135+
136+
return new Response('Nope, this is not a leap year.');
137+
}
138+
}
139+
140+
The controller resolver also takes care of validating the controller callable
141+
and its arguments. In case of a problem, it throws an exception with a nice
142+
message explaining the problem (the controller class does not exist, the
143+
method is not defined, an argument has no matching attribute, ...).
144+
145+
.. note::
146+
147+
With the great flexibility of the default controller resolver, you might
148+
wonder why someone would want to create another one (why would there be an
149+
interface if not). Two examples: in Symfony2, ``getController()`` is
150+
enhanced to support `controllers as services`_; and in
151+
`FrameworkExtraBundle`_, ``getArguments()`` is enhanced to support
152+
parameter converters, where request attributes are converted to objects
153+
automatically.
154+
155+
Let's conclude with the new version of our framework::
156+
157+
<?php
158+
159+
// example.com/web/front.php
160+
161+
require_once __DIR__.'/../vendor/.composer/autoload.php';
162+
163+
use Symfony\Component\HttpFoundation\Request;
164+
use Symfony\Component\HttpFoundation\Response;
165+
use Symfony\Component\Routing;
166+
use Symfony\Component\HttpKernel;
167+
168+
function render_template($request)
169+
{
170+
extract($request->attributes->all());
171+
ob_start();
172+
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
173+
174+
return new Response(ob_get_clean());
175+
}
176+
177+
$request = Request::createFromGlobals();
178+
$routes = include __DIR__.'/../src/app.php';
179+
180+
$context = new Routing\RequestContext();
181+
$context->fromRequest($request);
182+
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
183+
$resolver = new HttpKernel\Controller\ControllerResolver();
184+
185+
try {
186+
$request->attributes->add($matcher->match($request->getPathInfo()));
187+
188+
$controller = $resolver->getController($request);
189+
$arguments = $resolver->getArguments($request, $controller);
190+
191+
$response = call_user_func_array($controller, $arguments);
192+
} catch (Routing\Exception\ResourceNotFoundException $e) {
193+
$response = new Response('Not Found', 404);
194+
} catch (Exception $e) {
195+
$response = new Response('An error occurred', 500);
196+
}
197+
198+
$response->send();
199+
200+
Think about it once more: our framework is more robust and more flexible than
201+
ever and it still has less than 40 lines of code.
202+
203+
.. _`reflection`: http://php.net/reflection
204+
.. _`FrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
205+
.. _`controllers as services`: http://symfony.com/doc/current/cookbook/controller/service.html

0 commit comments

Comments
 (0)