mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-24 00:32:14 +01:00
302 lines
12 KiB
ReStructuredText
302 lines
12 KiB
ReStructuredText
The HttpFoundation Component
|
|
============================
|
|
|
|
Before diving into the framework creation process, let's first step back and
|
|
let's take a look at why you would like to use a framework instead of keeping
|
|
your plain-old PHP applications as is. Why using a framework is actually a good
|
|
idea, even for the simplest snippet of code and why creating your framework on
|
|
top of the Symfony components is better than creating a framework from scratch.
|
|
|
|
.. note::
|
|
|
|
We won't talk about the traditional benefits of using a framework when
|
|
working on big applications with more than a few developers; the Internet
|
|
already has plenty of good resources on that topic.
|
|
|
|
Even if the "application" we wrote in the previous chapter was simple enough,
|
|
it suffers from a few problems::
|
|
|
|
// framework/index.php
|
|
$name = $_GET['name'];
|
|
|
|
printf('Hello %s', $name);
|
|
|
|
First, if the ``name`` query parameter is not defined in the URL query string,
|
|
you will get a PHP warning; so let's fix it::
|
|
|
|
// framework/index.php
|
|
$name = $_GET['name'] ?? 'World';
|
|
|
|
printf('Hello %s', $name);
|
|
|
|
Then, this *application is not secure*. Can you believe it? Even this simple
|
|
snippet of PHP code is vulnerable to one of the most widespread Internet
|
|
security issue, XSS (Cross-Site Scripting). Here is a more secure version::
|
|
|
|
$name = $_GET['name'] ?? 'World';
|
|
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
|
|
printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'));
|
|
|
|
.. note::
|
|
|
|
As you might have noticed, securing your code with ``htmlspecialchars`` is
|
|
tedious and error prone. That's one of the reasons why using a template
|
|
engine like `Twig`_, where auto-escaping is enabled by default, might be a
|
|
good idea (and explicit escaping is also less painful with the usage of a
|
|
simple ``e`` filter).
|
|
|
|
As you can see for yourself, the simple code we had written first is not that
|
|
simple anymore if we want to avoid PHP warnings/notices and make the code
|
|
more secure.
|
|
|
|
Beyond security, this code can be complex to test. Even if there is not
|
|
much to test, it strikes me that writing unit tests for the simplest possible
|
|
snippet of PHP code is not natural and feels ugly. Here is a tentative PHPUnit
|
|
unit test for the above code::
|
|
|
|
// framework/test.php
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class IndexTest extends TestCase
|
|
{
|
|
public function testHello(): void
|
|
{
|
|
$_GET['name'] = 'Fabien';
|
|
|
|
ob_start();
|
|
include 'index.php';
|
|
$content = ob_get_clean();
|
|
|
|
$this->assertEquals('Hello Fabien', $content);
|
|
}
|
|
}
|
|
|
|
.. note::
|
|
|
|
If our application were just slightly bigger, we would have been able to
|
|
find even more problems. If you are curious about them, read the
|
|
:doc:`/introduction/from_flat_php_to_symfony` chapter of the book.
|
|
|
|
At this point, if you are not convinced that security and testing are indeed
|
|
two very good reasons to stop writing code the old way and adopt a framework
|
|
instead (whatever adopting a framework means in this context), you can stop
|
|
reading this book now and go back to whatever code you were working on before.
|
|
|
|
.. note::
|
|
|
|
Using a framework should give you more than just security and testability,
|
|
but the more important thing to keep in mind is that the framework you
|
|
choose must allow you to write better code faster.
|
|
|
|
Going OOP with the HttpFoundation Component
|
|
-------------------------------------------
|
|
|
|
Writing web code is about interacting with HTTP. So, the fundamental
|
|
principles of our framework should be around the `HTTP specification`_.
|
|
|
|
The HTTP specification describes how a client (a browser for instance)
|
|
interacts with a server (our application via a web server). The dialog between
|
|
the client and the server is specified by well-defined *messages*, requests
|
|
and responses: *the client sends a request to the server and based on this
|
|
request, the server returns a response*.
|
|
|
|
In PHP, the request is represented by global variables (``$_GET``, ``$_POST``,
|
|
``$_FILE``, ``$_COOKIE``, ``$_SESSION``...) and the response is generated by
|
|
functions (``echo``, ``header``, ``setcookie``, ...).
|
|
|
|
The first step towards better code is probably to use an Object-Oriented
|
|
approach; that's the main goal of the Symfony HttpFoundation component:
|
|
replacing the default PHP global variables and functions by an Object-Oriented
|
|
layer.
|
|
|
|
To use this component, add it as a dependency of the project:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ composer require symfony/http-foundation
|
|
|
|
Running this command will also automatically download the Symfony
|
|
HttpFoundation component and install it under the ``vendor/`` directory.
|
|
A ``composer.json`` and a ``composer.lock`` file will be generated as well,
|
|
containing the new requirement.
|
|
|
|
.. sidebar:: Class Autoloading
|
|
|
|
When installing a new dependency, Composer also generates a
|
|
``vendor/autoload.php`` file that allows any class to be `autoloaded`_.
|
|
Without autoloading, you would need to require the file where a class
|
|
is defined before being able to use it. But thanks to `PSR-4`_,
|
|
we can just let Composer and PHP do the hard work for us.
|
|
|
|
Now, let's rewrite our application by using the ``Request`` and the
|
|
``Response`` classes::
|
|
|
|
// framework/index.php
|
|
require_once __DIR__.'/vendor/autoload.php';
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
$request = Request::createFromGlobals();
|
|
|
|
$name = $request->query->get('name', 'World');
|
|
|
|
$response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
|
|
|
|
$response->send();
|
|
|
|
The ``createFromGlobals()`` method creates a ``Request`` object based on the
|
|
current PHP global variables.
|
|
|
|
The ``send()`` method sends the ``Response`` object back to the client (it
|
|
first outputs the HTTP headers followed by the content).
|
|
|
|
.. tip::
|
|
|
|
Before the ``send()`` call, we should have added a call to the
|
|
``prepare()`` method (``$response->prepare($request);``) to ensure that
|
|
our Response were compliant with the HTTP specification. For instance, if
|
|
we were to call the page with the ``HEAD`` method, it would remove the
|
|
content of the Response.
|
|
|
|
The main difference with the previous code is that you have total control of
|
|
the HTTP messages. You can create whatever request you want and you are in
|
|
charge of sending the response whenever you see fit.
|
|
|
|
.. note::
|
|
|
|
We haven't explicitly set the ``Content-Type`` header in the rewritten
|
|
code as the charset of the Response object defaults to ``UTF-8``.
|
|
|
|
With the ``Request`` class, you have all the request information at your
|
|
fingertips thanks to a nice and simple API::
|
|
|
|
// the URI being requested (e.g. /about) minus any query parameters
|
|
$request->getPathInfo();
|
|
|
|
// retrieves GET and POST variables respectively
|
|
$request->query->get('foo');
|
|
$request->getPayload()->get('bar', 'default value if bar does not exist');
|
|
|
|
// retrieves SERVER variables
|
|
$request->server->get('HTTP_HOST');
|
|
|
|
// retrieves an instance of UploadedFile identified by foo
|
|
$request->files->get('foo');
|
|
|
|
// retrieves a COOKIE value
|
|
$request->cookies->get('PHPSESSID');
|
|
|
|
// retrieves an HTTP request header, with normalized, lowercase keys
|
|
$request->headers->get('host');
|
|
$request->headers->get('content-type');
|
|
|
|
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD
|
|
$request->getLanguages(); // an array of languages the client accepts
|
|
|
|
You can also simulate a request::
|
|
|
|
$request = Request::create('/index.php?name=Fabien');
|
|
|
|
With the ``Response`` class, you can tweak the response::
|
|
|
|
$response = new Response();
|
|
|
|
$response->setContent('Hello world!');
|
|
$response->setStatusCode(200);
|
|
$response->headers->set('Content-Type', 'text/html');
|
|
|
|
// configure the HTTP cache headers
|
|
$response->setMaxAge(10);
|
|
|
|
.. tip::
|
|
|
|
To debug a response, cast it to a string; it will return the HTTP
|
|
representation of the response (headers and content).
|
|
|
|
Last but not least, these classes, like every other class in the Symfony
|
|
code, have been `audited`_ for security issues by an independent company. And
|
|
being an Open-Source project also means that many other developers around the
|
|
world have read the code and have already fixed potential security problems.
|
|
When was the last time you ordered a professional security audit for your home-made
|
|
framework?
|
|
|
|
Even something as simple as getting the client IP address can be insecure::
|
|
|
|
if ($myIp === $_SERVER['REMOTE_ADDR']) {
|
|
// the client is a known one, so give it some more privilege
|
|
}
|
|
|
|
It works perfectly fine until you add a reverse proxy in front of the
|
|
production servers; at this point, you will have to change your code to make
|
|
it work on both your development machine (where you don't have a proxy) and
|
|
your servers::
|
|
|
|
if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) {
|
|
// the client is a known one, so give it some more privilege
|
|
}
|
|
|
|
Using the ``Request::getClientIp()`` method would have given you the right
|
|
behavior from day one (and it would have covered the case where you have
|
|
chained proxies)::
|
|
|
|
$request = Request::createFromGlobals();
|
|
|
|
if ($myIp === $request->getClientIp()) {
|
|
// the client is a known one, so give it some more privilege
|
|
}
|
|
|
|
And there is an added benefit: it is *secure* by default. What does it mean?
|
|
The ``$_SERVER['HTTP_X_FORWARDED_FOR']`` value cannot be trusted as it can be
|
|
manipulated by the end user when there is no proxy. So, if you are using this
|
|
code in production without a proxy, it becomes trivially easy to abuse your
|
|
system. That's not the case with the ``getClientIp()`` method as you must
|
|
explicitly trust your reverse proxies by calling ``setTrustedProxies()``::
|
|
|
|
Request::setTrustedProxies(['10.0.0.1'], Request::HEADER_X_FORWARDED_FOR);
|
|
|
|
if ($myIp === $request->getClientIp()) {
|
|
// the client is a known one, so give it some more privilege
|
|
}
|
|
|
|
So, the ``getClientIp()`` method works securely in all circumstances. You can
|
|
use it in all your projects, whatever the configuration is, it will behave
|
|
correctly and safely. That's one of the goals of using a framework. If you were
|
|
to write a framework from scratch, you would have to think about all these
|
|
cases by yourself. Why not use a technology that already works?
|
|
|
|
.. note::
|
|
|
|
If you want to learn more about the HttpFoundation component, you can have
|
|
a look at the ``Symfony\Component\HttpFoundation`` API or read
|
|
its dedicated :doc:`documentation </components/http_foundation>`.
|
|
|
|
Believe it or not but we have our first framework. You can stop now if you want.
|
|
Using just the Symfony HttpFoundation component already allows you to write
|
|
better and more testable code. It also allows you to write code faster as many
|
|
day-to-day problems have already been solved for you.
|
|
|
|
As a matter of fact, projects like Drupal have adopted the HttpFoundation
|
|
component; if it works for them, it will probably work for you. Don't reinvent
|
|
the wheel.
|
|
|
|
I've almost forgotten to talk about one added benefit: using the HttpFoundation
|
|
component is the start of better interoperability between all frameworks and
|
|
`applications using it`_ (like `Symfony`_, `Drupal 8`_, `phpBB 3`_, `Laravel`_
|
|
and `ezPublish 5`_, and `more`_).
|
|
|
|
.. _`Twig`: https://twig.symfony.com/
|
|
.. _`HTTP specification`: https://tools.ietf.org/wg/httpbis/
|
|
.. _`audited`: https://symfony.com/blog/symfony2-security-audit
|
|
.. _`applications using it`: https://symfony.com/components/HttpFoundation
|
|
.. _`Symfony`: https://symfony.com/
|
|
.. _`Drupal 8`: https://www.drupal.org/
|
|
.. _`phpBB 3`: https://www.phpbb.com/
|
|
.. _`ezPublish 5`: https://ez.no/
|
|
.. _`Laravel`: https://laravel.com/
|
|
.. _`autoloaded`: https://www.php.net/autoload
|
|
.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/
|
|
.. _`more`: https://symfony.com/components/HttpFoundation
|