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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy