Skip to content

Commit 2c79d42

Browse files
committed
added part 2
1 parent 13ba87c commit 2c79d42

File tree

1 file changed

+332
-0
lines changed

1 file changed

+332
-0
lines changed

book/part2.rst

+332
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
Create your own framework... on top of the Symfony2 Components (part 2)
2+
=======================================================================
3+
4+
Before we dive into the code refactoring, I first want to step back and take a
5+
look at why you would like to use a framework instead of keeping your
6+
plain-old PHP applications as is. Why using a framework is actually a good
7+
idea, even for the simplest snippet of code and why creating your framework on
8+
top of the Symfony2 components is better than creating a framework from
9+
scratch.
10+
11+
.. note::
12+
13+
I won't talk about the obvious and traditional benefits of using a
14+
framework when working on big applications with more than a few
15+
developers; the Internet has already plenty of good resources on that
16+
topic.
17+
18+
Even if the "application" we wrote yesterday was simple enough, it suffers
19+
from a few problems::
20+
21+
<?php
22+
23+
// framework/index.php
24+
25+
$input = $_GET['name'];
26+
27+
printf('Hello %s', $input);
28+
29+
First, if the ``name`` query parameter is not given in the URL query string,
30+
you will get a PHP warning; so let's fix it::
31+
32+
<?php
33+
34+
// framework/index.php
35+
36+
$input = isset($_GET['name']) ? $_GET['name'] : 'World';
37+
38+
printf('Hello %s', $input);
39+
40+
Then, this *application is not secure*. Can you believe it? Even this simple
41+
snippet of PHP code is vulnerable to one of the most widespread Internet
42+
security issue, XSS (Cross-Site Scripting). Here is a more secure version::
43+
44+
<?php
45+
46+
$input = isset($_GET['name']) ? $_GET['name'] : 'World';
47+
48+
header('Content-Type: text/html; charset=utf-8');
49+
50+
printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'));
51+
52+
.. note::
53+
54+
As you might have noticed, securing your code with ``htmlspecialchars`` is
55+
tedious and error prone. That's one of the reasons why using a template
56+
engine like `Twig`_, where auto-escaping is enabled by default, might be a
57+
good idea (and explicit escaping is also less painful with the usage of a
58+
simple ``e`` filter).
59+
60+
As you can see for yourself, the simple code we had written first is not that
61+
simple anymore if we want to avoid PHP warnings/notices and make the code
62+
more secure.
63+
64+
Beyond security, this code is not even easily testable. Even if there is not
65+
much to test, it strikes me that writing unit tests for the simplest possible
66+
snippet of PHP code is not natural and feels ugly. Here is a tentative PHPUnit
67+
unit test for the above code::
68+
69+
<?php
70+
71+
// framework/test.php
72+
73+
class IndexTest extends \PHPUnit_Framework_TestCase
74+
{
75+
public function testHello()
76+
{
77+
$_GET['name'] = 'Fabien';
78+
79+
ob_start();
80+
include 'index.php';
81+
$content = ob_get_clean();
82+
83+
$this->assertEquals('Hello Fabien', $content);
84+
}
85+
}
86+
87+
.. note::
88+
89+
If our application were just slightly bigger, we would have been able to
90+
find even more problems. If you are curious about them, read the `Symfony2
91+
versus Flat PHP`_ chapter of the Symfony2 documentation.
92+
93+
At this point, if you are not convinced that security and testing are indeed
94+
two very good reasons to stop writing code the old way and adopt a framework
95+
instead (whatever adopting a framework means in this context), you can stop
96+
reading this series now and go back to whatever code you were working on
97+
before.
98+
99+
.. note::
100+
101+
Of course, using a framework should give you more than just security and
102+
testability, but the more important thing to keep in mind is that the
103+
framework you choose must allow you to write better code faster.
104+
105+
Going OOP with the HttpFoundation Component
106+
-------------------------------------------
107+
108+
Writing web code is about interacting with HTTP. So, the fundamental
109+
principles of our framework should be centered around the `HTTP
110+
specification`_.
111+
112+
The HTTP specification describes how a client (a browser for instance)
113+
interacts with a server (our application via a web server). The dialog between
114+
the client and the server is specified by well-defined *messages*, requests
115+
and responses: *the client sends a request to the server and based on this
116+
request, the server returns a response*.
117+
118+
In PHP, the request is represented by global variables (``$_GET``, ``$_POST``,
119+
``$_FILE``, ``$_COOKIE``, ``$_SESSION``...) and the response is generated by
120+
functions (``echo``, ``header``, ``setcookie``, ...).
121+
122+
The first step towards better code is probably to use an Object-Oriented
123+
approach; that's the main goal of the Symfony2 HttpFoundation component:
124+
replacing the default PHP global variables and functions by an Object-Oriented
125+
layer.
126+
127+
To use this component, open the ``composer.json`` file and add it as a
128+
dependency for the project:
129+
130+
.. code-block:: json
131+
132+
# framework/composer.json
133+
{
134+
"require": {
135+
"symfony/class-loader": "2.1.*",
136+
"symfony/http-foundation": "2.1.*"
137+
}
138+
}
139+
140+
Then, run the composer ``update`` command:
141+
142+
.. code-block:: sh
143+
144+
$ php composer.phar update
145+
146+
Finally, at the bottom of the ``autoload.php`` file, add the code needed to
147+
autoload the component::
148+
149+
<?php
150+
151+
// framework/autoload.php
152+
153+
$loader->registerNamespace('Symfony\\Component\\HttpFoundation', __DIR__.'/vendor/symfony/http-foundation');
154+
155+
Now, let's rewrite our application by using the ``Request`` and the
156+
``Response`` classes::
157+
158+
<php
159+
160+
// framework/index.php
161+
162+
require_once __DIR__.'/autoload.php';
163+
164+
use Symfony\Component\HttpFoundation\Request;
165+
use Symfony\Component\HttpFoundation\Response;
166+
167+
$request = Request::createFromGlobals();
168+
169+
$input = $request->get('name', 'World');
170+
171+
$response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
172+
173+
$response->send();
174+
175+
The ``createFromGlobals()`` method creates a ``Request`` object based on the
176+
current PHP global variables.
177+
178+
The ``send()`` method sends the ``Response`` object back to the client (it
179+
first outputs the HTTP headers followed by the content).
180+
181+
.. tip::
182+
183+
Before the ``send()`` call, we should have added a call to the
184+
``prepare()`` method (``$response->prepare($request);``) to ensure that
185+
our Response were compliant with the HTTP specification. For instance, if
186+
we were to call the page with the ``HEAD`` method, it would have removed
187+
the content of the Response.
188+
189+
The main difference with the previous code is that you have total control of
190+
the HTTP messages. You can create whatever request you want and you are in
191+
charge of sending the response whenever you see fit.
192+
193+
.. note::
194+
195+
We haven't explicitly set the ``Content-Type`` header in the rewritten
196+
code as the Response object defaults to ``UTF-8`` by default.
197+
198+
With the ``Request`` class, you have all the request information at your
199+
fingertips thanks to a nice and simple API::
200+
201+
<?php
202+
203+
// the URI being requested (e.g. /about) minus any query parameters
204+
$request->getPathInfo();
205+
206+
// retrieve GET and POST variables respectively
207+
$request->query->get('foo');
208+
$request->request->get('bar', 'default value if bar does not exist');
209+
210+
// retrieve SERVER variables
211+
$request->server->get('HTTP_HOST');
212+
213+
// retrieves an instance of UploadedFile identified by foo
214+
$request->files->get('foo');
215+
216+
// retrieve a COOKIE value
217+
$request->cookies->get('PHPSESSID');
218+
219+
// retrieve an HTTP request header, with normalized, lowercase keys
220+
$request->headers->get('host');
221+
$request->headers->get('content_type');
222+
223+
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD
224+
$request->getLanguages(); // an array of languages the client accepts
225+
226+
You can also simulate a request::
227+
228+
$request = Request::create('/index.php?name=Fabien');
229+
230+
With the ``Response`` class, you can easily tweak the response::
231+
232+
<?php
233+
234+
$response = new Response();
235+
236+
$response->setContent('Hello world!');
237+
$response->setStatusCode(200);
238+
$response->headers->set('Content-Type', 'text/html');
239+
240+
// configure the HTTP cache headers
241+
$response->setMaxAge(10);
242+
243+
.. tip::
244+
245+
To debug a Response, cast it to a string; it will return the HTTP
246+
representation of the response (headers and content).
247+
248+
Last but not the least, these classes, like every other class in the Symfony
249+
code, have been `audited`_ for security issues by an independent company. And
250+
being an Open-Source project also means that many other developers around the
251+
world have read the code and have already fixed potential security problems.
252+
When was the last you ordered a professional security audit for your home-made
253+
framework?
254+
255+
Even something as simple as getting the client IP address can be insecure::
256+
257+
<?php
258+
259+
if ($myIp == $_SERVER['REMOTE_ADDR']) {
260+
// the client is a known one, so give it some more privilege
261+
}
262+
263+
It works perfectly fine until you add a reverse proxy in front of the
264+
production servers; at this point, you will have to change your code to make
265+
it work on both your development machine (where you don't have a proxy) and
266+
your servers::
267+
268+
<?php
269+
270+
if ($myIp == $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp == $_SERVER['REMOTE_ADDR']) {
271+
// the client is a known one, so give it some more privilege
272+
}
273+
274+
Using the ``Request::getClientIp()`` method would have given you the right
275+
behavior from day one (and it would have covered the case where where you have
276+
chained proxies)::
277+
278+
<?php
279+
280+
$request = Request::createFromGlobals();
281+
282+
if ($myIp == $request->getClientIp()) {
283+
// the client is a known one, so give it some more privilege
284+
}
285+
286+
And there is an added benefit: it is *secure* by default. What do I mean by
287+
secure? The ``$_SERVER['HTTP_X_FORWARDED_FOR']`` value cannot be trusted as it
288+
can be manipulated by the end user when there is no proxy. So, if you are
289+
using this code in production without a proxy, it becomes trivially easy to
290+
abuse your system. That's not the case with the ``getClientIp()`` method as
291+
you must explicitly trust this header by calling ``trustProxyData()``::
292+
293+
<?php
294+
295+
Request::trustProxyData();
296+
297+
if ($myIp == $request->getClientIp(true)) {
298+
// the client is a known one, so give it some more privilege
299+
}
300+
301+
So, the ``getClientIp()`` method works securely in all circumstances. You can
302+
use it in all your projects, whatever the configuration is, it will behave
303+
correctly and safely. That's one of the goal of using a framework. If you were
304+
to write a framework from scratch, you would have to think about all these
305+
cases by yourself. Why not using a technology that already works?
306+
307+
.. note::
308+
309+
If you want to learn more about the HttpFoundation component, you can have
310+
a look at the `API`_ or read its dedicated `documentation`_ on the Symfony
311+
website.
312+
313+
Believe or not but we have our first framework. You can stop now if you want.
314+
Using just the Symfony2 HttpFoundation component already allows you to write
315+
better and more testable code. It also allows you to write code faster as many
316+
day-to-day problems have already been solved for you.
317+
318+
As a matter of fact, projects like Drupal have adopted (for the upcoming
319+
version 8) the HttpFoundation component; if it works for them, it will
320+
probably work for you. Don't reinvent the wheel.
321+
322+
I've almost forgot to talk about one added benefit: using the HttpFoundation
323+
component is the start of better interoperability between all frameworks and
324+
applications using it (as of today Symfony2, Drupal 8, phpBB 4, Silex,
325+
Midguard CMS, ...).
326+
327+
.. _`Twig`: http://twig.sensiolabs.com/
328+
.. _`Symfony2 versus Flat PHP`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html
329+
.. _`HTTP specification`: http://tools.ietf.org/wg/httpbis/
330+
.. _`API`: http://api.symfony.com/2.0/Symfony/Component/HttpFoundation.html
331+
.. _`documentation`: http://symfony.com/doc/current/components/http_foundation.html
332+
.. _`audited`: http://symfony.com/blog/symfony2-security-audit

0 commit comments

Comments
 (0)