Skip to content

Commit 9bc692f

Browse files
committed
added part 12
1 parent 0b7581d commit 9bc692f

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed

book/part12.rst

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
Create your own framework... on top of the Symfony2 Components (part 12)
2+
========================================================================
3+
4+
In the last installment of this series, we have emptied the
5+
``Simplex\\Framework`` class by extending the ``HttpKernel`` class from
6+
Symfony. Seeing this empty class, you might be tempted to move some code from
7+
the front controller to it::
8+
9+
<?php
10+
11+
// example.com/src/Simplex/Framework.php
12+
13+
namespace Simplex;
14+
15+
use Symfony\Component\HttpKernel\HttpKernel;
16+
use Symfony\Component\Routing;
17+
use Symfony\Component\HttpKernel;
18+
use Symfony\Component\EventDispatcher\EventDispatcher;
19+
20+
class Framework extends HttpKernel
21+
{
22+
public function __construct($routes)
23+
{
24+
$context = new Routing\RequestContext();
25+
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
26+
$resolver = new HttpKernel\Controller\ControllerResolver();
27+
28+
$dispatcher = new EventDispatcher();
29+
$dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));
30+
$dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
31+
32+
parent::__construct($dispatcher, $resolver);
33+
}
34+
}
35+
36+
The front controller code would become more concise::
37+
38+
<?php
39+
40+
// example.com/web/front.php
41+
42+
require_once __DIR__.'/../vendor/.composer/autoload.php';
43+
44+
use Symfony\Component\HttpFoundation\Request;
45+
46+
$request = Request::createFromGlobals();
47+
$routes = include __DIR__.'/../src/app.php';
48+
49+
$framework = new Simplex\Framework($routes);
50+
51+
$framework->handle($request)->send();
52+
53+
Having a more concise front controller means that you can have more than one
54+
for a single application. Why would it be useful? To allow having different
55+
configuration for the development environment and the production one for
56+
instance. In the development environment, you might want to have error
57+
reporting turned on and errors displayed in the browser to ease debugging::
58+
59+
ini_set('display_errors', 1);
60+
error_reporting(-1);
61+
62+
... but you certainly won't want that same configuration on the production
63+
environment. Having two different front controllers gives you the opportunity
64+
to have a slightly different configuration for each of them.
65+
66+
So, moving code from the front controller to the framework class makes our
67+
framework more configurable, but at the same time, it introduces a lot of
68+
issues:
69+
70+
* We are not able to register custom listeners anymore as the dispatcher is
71+
not available outside the Framework class (an easy workaround could be the
72+
adding of a ``Framework::getEventDispatcher()`` method);
73+
74+
* We have lost the flexibility we had before; you cannot change the
75+
implementation of the ``UrlMatcher`` or of the ``ControllerResolver``
76+
anymore;
77+
78+
* Related to the previous point, we cannot test our framework easily anymore
79+
as it's impossible to mock internal objects;
80+
81+
* We cannot change the charset passed to ResponseListener anymore (a
82+
workaround could be to pass it as a constructor argument).
83+
84+
The previous code did not exhibit the same issues because we used dependency
85+
injection; all dependencies of our objects were injected into their
86+
constructors (for instance, the event dispatcher were injected into the
87+
framework so that we had total control of its creation and configuration).
88+
89+
Does it means that we have to make a choice between flexibility,
90+
customization, ease of testing and not having to copy and paste the same code
91+
into each application front controller? As you might expect, there is a
92+
solution. We can solve all these issues and some more by using the Symfony2
93+
dependency injection container:
94+
95+
.. code-block:: json
96+
97+
{
98+
"require": {
99+
"symfony/class-loader": "2.1.*",
100+
"symfony/http-foundation": "2.1.*",
101+
"symfony/routing": "2.1.*",
102+
"symfony/http-kernel": "2.1.*",
103+
"symfony/event-dispatcher": "2.1.*",
104+
"symfony/dependency-injection": "2.1.*"
105+
},
106+
"autoload": {
107+
"psr-0": { "Simplex": "src/", "Calendar": "src/" }
108+
}
109+
}
110+
111+
Create a new file to host the dependency injection container configuration::
112+
113+
<?php
114+
115+
// example.com/src/container.php
116+
117+
use Symfony\Component\DependencyInjection;
118+
use Symfony\Component\DependencyInjection\Reference;
119+
120+
$sc = new DependencyInjection\ContainerBuilder();
121+
$sc->register('context', 'Symfony\Component\Routing\RequestContext');
122+
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
123+
->setArguments(array($routes, new Reference('context')))
124+
;
125+
$sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');
126+
127+
$sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
128+
->setArguments(array(new Reference('matcher')))
129+
;
130+
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
131+
->setArguments(array('UTF-8'))
132+
;
133+
$sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener')
134+
->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction'))
135+
;
136+
$sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher')
137+
->addMethodCall('addSubscriber', array(new Reference('listener.router')))
138+
->addMethodCall('addSubscriber', array(new Reference('listener.response')))
139+
->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
140+
;
141+
$sc->register('framework', 'Simplex\Framework')
142+
->setArguments(array(new Reference('dispatcher'), new Reference('resolver')))
143+
;
144+
145+
return $sc;
146+
147+
The goal of this file is to configure your objects and their dependencies.
148+
Nothing is instantiated during this configuration step. This is purely a
149+
static description of the objects you need to manipulate and how to create
150+
them. Objects will be created on-demand when you access them from the
151+
container or when the container needs them to create other objects.
152+
153+
For instance, to create the router listener, we tell Symfony that its class
154+
name is ``Symfony\Component\HttpKernel\EventListener\RouterListeners``, and
155+
that its constructor takes a matcher object (``new Reference('matcher')``). As
156+
you can see, each object is referenced by a name, a string that uniquely
157+
identifies each object. The name allows us to get an object and to reference
158+
it in other object definitions.
159+
160+
.. note::
161+
162+
By default, every time you get an object from the container, it returns
163+
the exact same instance. That's because a container manages your "global"
164+
objects.
165+
166+
The front controller is now only about wiring everything together::
167+
168+
<?php
169+
170+
// example.com/web/front.php
171+
172+
require_once __DIR__.'/../vendor/.composer/autoload.php';
173+
174+
use Symfony\Component\HttpFoundation\Request;
175+
176+
$routes = include __DIR__.'/../src/app.php';
177+
$sc = include __DIR__.'/../src/container.php';
178+
179+
$request = Request::createFromGlobals();
180+
181+
$response = $sc->get('framework')->handle($request);
182+
183+
$response->send();
184+
185+
.. note::
186+
187+
If you want a light alternative for your container, consider `Pimple`_, a
188+
simple dependency injection container in about 60 lines of PHP code.
189+
190+
Now, here is how you can register a custom listener in the front controller::
191+
192+
$sc->register('listener.string_response', 'Simplex\StringResponseListener');
193+
$sc->getDefinition('dispatcher')
194+
->addMethodCall('addSubscriber', array(new Reference('listener.string_response')))
195+
;
196+
197+
Beside describing your objects, the dependency injection container can also be
198+
configured via parameters. Let's create one that defines if we are in debug
199+
mode or not::
200+
201+
$sc->setParameter('debug', true);
202+
203+
echo $sc->getParameter('debug');
204+
205+
These parameters can be used when defining object definitions. Let's make the
206+
charset configurable::
207+
208+
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
209+
->setArguments(array('%charset%'))
210+
;
211+
212+
After this change, you must set the charset before using the response listener
213+
object::
214+
215+
$sc->setParameter('charset', 'UTF-8');
216+
217+
Instead of relying on the convention that the routes are defined by the
218+
``$routes`` variables, let's use a parameter again::
219+
220+
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
221+
->setArguments(array('%routes%', new Reference('context')))
222+
;
223+
224+
And the related change in the front controller::
225+
226+
$sc->setParameter('routes', include __DIR__.'/../src/app.php');
227+
228+
We have obviously barely scratched the surface of what you can do with the
229+
container: from class names as parameters, to overriding existing object
230+
definitions, from scope support to dumping a container to a plain PHP class,
231+
and much more. The Symfony dependency injection container is really powerful
232+
and is able to manage any kind of PHP classes.
233+
234+
Don't yell at me if you don't want to have a dependency injection container in
235+
your framework. If you don't like it, don't use it. It's your framework, not
236+
mine.
237+
238+
This is (already) the last part of my series on creating a framework on top of
239+
the Symfony2 components. I'm aware that many topics have not been covered in
240+
great details, but hopefully it gives you enough information to get started on
241+
your own and to better understand how the Symfony2 framework works internally.
242+
243+
If you want to learn more, I highly recommend you to read the source code of
244+
the Silex micro-framework, and especially its `Application`_ class.
245+
246+
Have fun!
247+
248+
~~ FIN ~~
249+
250+
*P.S.:* If there is enough interest (leave a comment on this post), I might
251+
write some more articles on specific topics (using a configuration file for
252+
routing, using HttpKernel debugging tools, using the build-in client to
253+
simulate a browser are some of the topics that come to my mind for instance).
254+
255+
.. _`Pimple`: https://github.com/fabpot/Pimple
256+
.. _`Application`: https://github.com/fabpot/Silex/blob/master/src/Silex/Application.php

0 commit comments

Comments
 (0)