Skip to content

Commit bde64c8

Browse files
committed
made small tweaks
1 parent 54e1b08 commit bde64c8

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed

book/part09.rst

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
Create your own framework... on top of the Symfony2 Components (part 9)
2+
=======================================================================
3+
4+
Our framework is still missing a major characteristic of any good framework:
5+
*extensibility*. Being extensible means that the developer should be able to
6+
easily hook into the framework life cycle to modify the way the request is
7+
handled.
8+
9+
What kind of hooks are we talking about? Authentication or caching for
10+
instance. To be flexible, hooks must be plug-and-play; the ones you "register"
11+
for an application are different from the next one depending on your specific
12+
needs. Many software have a similar concept like Drupal or Wordpress. In some
13+
languages, there is even a standard like `WSGI`_ in Python or `Rack`_ in Ruby.
14+
15+
As there is no standard for PHP, we are going to use a well-known design
16+
pattern, the *Observer*, to allow any kind of behaviors to be attached to our
17+
framework; the Symfony2 EventDispatcher Component implements a lightweight
18+
version of this pattern:
19+
20+
.. code-block:: json
21+
22+
{
23+
"require": {
24+
"symfony/class-loader": "2.1.*",
25+
"symfony/http-foundation": "2.1.*",
26+
"symfony/routing": "2.1.*",
27+
"symfony/http-kernel": "2.1.*",
28+
"symfony/event-dispatcher": "2.1.*"
29+
},
30+
"autoload": {
31+
"psr-0": { "Simplex": "src/", "Calendar": "src/" }
32+
}
33+
}
34+
35+
How does it work? The *dispatcher*, the central object of the event dispatcher
36+
system, notifies *listeners* of an *event* dispatched to it. Put another way:
37+
your code dispatches an event to the dispatcher, the dispatcher notifies all
38+
registered listeners for the event, and each listener do whatever it wants
39+
with the event.
40+
41+
As an example, let's create a listener that transparently adds the Google
42+
Analytics code to all responses.
43+
44+
To make it work, the framework must dispatch an event just before returning
45+
the Response instance::
46+
47+
<?php
48+
49+
// example.com/src/Simplex/Framework.php
50+
51+
namespace Simplex;
52+
53+
use Symfony\Component\HttpFoundation\Request;
54+
use Symfony\Component\HttpFoundation\Response;
55+
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
56+
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
57+
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
58+
use Symfony\Component\EventDispatcher\EventDispatcher;
59+
60+
class Framework
61+
{
62+
protected $matcher;
63+
protected $resolver;
64+
protected $dispatcher;
65+
66+
public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $resolver)
67+
{
68+
$this->matcher = $matcher;
69+
$this->resolver = $resolver;
70+
$this->dispatcher = $dispatcher;
71+
}
72+
73+
public function handle(Request $request)
74+
{
75+
try {
76+
$request->attributes->add($this->matcher->match($request->getPathInfo()));
77+
78+
$controller = $this->resolver->getController($request);
79+
$arguments = $this->resolver->getArguments($request, $controller);
80+
81+
$response = call_user_func_array($controller, $arguments);
82+
} catch (ResourceNotFoundException $e) {
83+
$response = new Response('Not Found', 404);
84+
} catch (\Exception $e) {
85+
$response = new Response('An error occurred', 500);
86+
}
87+
88+
// dispatch a response event
89+
$this->dispatcher->dispatch('response', new ResponseEvent($response, $request));
90+
91+
return $response;
92+
}
93+
}
94+
95+
Each time the framework handles a Request, a ``ResponseEvent`` event is
96+
now dispatched::
97+
98+
<?php
99+
100+
// example.com/src/Simplex/ResponseEvent.php
101+
102+
namespace Simplex;
103+
104+
use Symfony\Component\HttpFoundation\Request;
105+
use Symfony\Component\HttpFoundation\Response;
106+
use Symfony\Component\EventDispatcher\Event;
107+
108+
class ResponseEvent extends Event
109+
{
110+
private $request;
111+
private $response;
112+
113+
public function __construct(Response $response, Request $request)
114+
{
115+
$this->response = $response;
116+
$this->request = $request;
117+
}
118+
119+
public function getResponse()
120+
{
121+
return $this->response;
122+
}
123+
124+
public function getRequest()
125+
{
126+
return $this->request;
127+
}
128+
}
129+
130+
The last step is the creation of the dispatcher in the front controller and
131+
the registration of a listener for the ``response`` event::
132+
133+
<?php
134+
135+
// example.com/web/front.php
136+
137+
require_once __DIR__.'/../vendor/.composer/autoload.php';
138+
139+
// ...
140+
141+
use Symfony\Component\EventDispatcher\EventDispatcher;
142+
143+
$dispatcher = new EventDispatcher();
144+
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
145+
$response = $event->getResponse();
146+
147+
if ($response->isRedirection()
148+
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
149+
|| 'html' !== $event->getRequest()->getRequestFormat()
150+
) {
151+
return;
152+
}
153+
154+
$response->setContent($response->getContent().'GA CODE');
155+
});
156+
157+
$framework = new Simplex\Framework($dispatcher, $matcher, $resolver);
158+
$response = $framework->handle($request);
159+
160+
$response->send();
161+
162+
.. note::
163+
164+
The listener is just a proof of concept and you should add the Google
165+
Analytics code just before the body tag.
166+
167+
As you can see, ``addListener()`` associates a valid PHP callback to a named
168+
event (``response``); the event name must be the same as the one used in the
169+
``dispatch()`` call.
170+
171+
In the listener, we add the Google Analytics code only if the response is not
172+
a redirection, if the requested format is HTML, and if the response content
173+
type is HTML (these conditions demonstrate the ease of manipulating the
174+
Request and Response data from your code).
175+
176+
So far so good, but let's add another listener on the same event. Let's say
177+
that I want to set the ``Content-Length`` of the Response if it is not already
178+
set::
179+
180+
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
181+
$response = $event->getResponse();
182+
$headers = $response->headers;
183+
184+
if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
185+
$headers->set('Content-Length', strlen($response->getContent()));
186+
}
187+
});
188+
189+
Depending on whether you have added this piece of code before the previous
190+
listener registration or after it, you will have the wrong or the right value
191+
for the ``Content-Length`` header. Sometimes, the order of the listeners
192+
matter but by default, all listeners are registered with the same priority,
193+
``0``. To tell the dispatcher to run a listener early, change the priority to
194+
a positive number; negative numbers can be used for low priority listeners.
195+
Here, we want the ``Content-Length`` listener to be executed last, so change
196+
the priority to ``-255``::
197+
198+
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) {
199+
$response = $event->getResponse();
200+
$headers = $response->headers;
201+
202+
if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
203+
$headers->set('Content-Length', strlen($response->getContent()));
204+
}
205+
}, -255);
206+
207+
.. tip::
208+
209+
When creating your framework, think about priorities (reserve some numbers
210+
for internal listeners for instance) and document them thoroughly.
211+
212+
Let's refactor the code a bit by moving the Google listener to its own class::
213+
214+
<?php
215+
216+
// example.com/src/Simplex/GoogleListener.php
217+
218+
namespace Simplex;
219+
220+
class GoogleListener
221+
{
222+
public function onResponse(ResponseEvent $event)
223+
{
224+
$response = $event->getResponse();
225+
226+
if ($response->isRedirection()
227+
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
228+
|| 'html' !== $event->getRequest()->getRequestFormat()
229+
) {
230+
return;
231+
}
232+
233+
$response->setContent($response->getContent().'GA CODE');
234+
}
235+
}
236+
237+
And do the same with the other listener::
238+
239+
<?php
240+
241+
// example.com/src/Simplex/ContentLengthListener.php
242+
243+
namespace Simplex;
244+
245+
class ContentLengthListener
246+
{
247+
public function onResponse(ResponseEvent $event)
248+
{
249+
$response = $event->getResponse();
250+
$headers = $response->headers;
251+
252+
if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) {
253+
$headers->set('Content-Length', strlen($response->getContent()));
254+
}
255+
}
256+
}
257+
258+
Our front controller should now look like the following::
259+
260+
$dispatcher = new EventDispatcher();
261+
$dispatcher->addListener('response', array(new Simplex\ContentLengthListener(), 'onResponse'), -255);
262+
$dispatcher->addListener('response', array(new Simplex\GoogleListener(), 'onResponse'));
263+
264+
Even if the code is now nicely wrapped in classes, there is still a slight
265+
issue: the knowledge of the priorities is "hardcoded" in the front controller,
266+
instead of being in the listeners themselves. For each application, you have
267+
to remember to set the appropriate priorities. Moreover, the listener method
268+
names are also exposed here, which means that refactoring our listeners would
269+
mean changing all the applications that rely on those listeners. Of course,
270+
there is a solution: use subscribers instead of listeners::
271+
272+
$dispatcher = new EventDispatcher();
273+
$dispatcher->addSubscriber(new Simplex\ContentLengthListener());
274+
$dispatcher->addSubscriber(new Simplex\GoogleListener());
275+
276+
A subscriber knowns about all the events it is interested in and pass this
277+
information to the dispatcher via the ``getSubscribedEvents()`` method. Have a
278+
look at the new version of the ``GoogleListener``::
279+
280+
<?php
281+
282+
// example.com/src/Simplex/GoogleListener.php
283+
284+
namespace Simplex;
285+
286+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
287+
288+
class GoogleListener implements EventSubscriberInterface
289+
{
290+
// ...
291+
292+
public static function getSubscribedEvents()
293+
{
294+
return array('response' => 'onResponse');
295+
}
296+
}
297+
298+
And here is the new version of ``ContentLengthListener``::
299+
300+
<?php
301+
302+
// example.com/src/Simplex/ContentLengthListener.php
303+
304+
namespace Simplex;
305+
306+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
307+
308+
class ContentLengthListener implements EventSubscriberInterface
309+
{
310+
// ...
311+
312+
public static function getSubscribedEvents()
313+
{
314+
return array('response' => array('onResponse', -255));
315+
}
316+
}
317+
318+
.. tip::
319+
320+
A single subscriber can host as many listeners as you want on as many
321+
events as needed.
322+
323+
To make your framework truly flexible, don't hesitate to add more events; and
324+
to make it more awesome out of the box, add more listeners. Again, this series
325+
is not about creating a generic framework, but one that is tailored to your
326+
needs. Stop whenever you see fit, and further evolve the code from there.
327+
328+
.. _`WSGI`: http://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides
329+
.. _`Rack`: http://rack.rubyforge.org/

0 commit comments

Comments
 (0)