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

Lines changed: 332 additions & 0 deletions
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)
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