📜 ⬆️ ⬇️

We create our own framework based on symfony2. (Part 2)

Table of contents

Part 1
Part 2
Part 3
Part 4
Part 5

Before we dive into refactoring the code, I first want to take a step back and take a look at why you would like to use the framework instead of writing your application in pure PHP. Why using a framework is actually a good idea, even for the simplest code snippet, and why creating your own framework based on Symfony2 components is better than creating a framework from scratch.


Although the “application” that we wrote last time was quite simple, it has several drawbacks:
<?php // framework/index.php $input = $_GET['name']; printf('Hello %s', $input); 


First, if the name variable is not set in the request parameter, a PHP warning will be issued. Fix this:
 <?php // framework/index.php $input = isset($_GET['name']) ? $_GET['name'] : 'World'; printf('Hello %s', $input); 

')
Believe it or not, even this small piece of code is vulnerable to one of the most common vulnerabilities - XSS (Cross-Site Scripting). Here is a more secure version:
 <?php $input = isset($_GET['name']) ? $_GET['name'] : 'World'; header('Content-Type: text/html; charset=utf-8'); printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')); 



As you can see, initially simple code becomes more complicated when adding security checks and avoiding PHP warnings / notifications.

In addition to security, this code is also not easily testable. Even if there is not so much to test, it seems to me that writing unit tests for the simplest piece of PHP code is not natural and will look ugly. Here is an example of the PHPUnit unit test for the above code:
 <?php // framework/test.php class IndexTest extends \PHPUnit_Framework_TestCase { public function testHello() { $_GET['name'] = 'Fabien'; ob_start(); include 'index.php'; $content = ob_get_clean(); $this->assertEquals('Hello Fabien', $content); } } 


At this point, if you are not sure that security and testing are really two very good reasons for not writing code in the old way, but adapting it to your framework, you can stop reading this series now and return to code, on which you worked earlier.


We move to OOP using the HttpFoundation component. (Going OOP with the HttpFoundation Component)



Writing web-based code is essentially working with the HTTP protocol. So the basic principles of our framework should be based on the HTTP specification .

The HTTP specification describes how a client (for example, a browser) interacts with a server (our application through a web server). The dialogue between the client and the server is clearly 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 as global variables ( $ _GET, $ _POST, $ _FILE, $ _COOKIE, $ _SESSION ...), and the response is generated using functions (echo, header, setcookie, ...).

The first step to better code is to use an object-oriented approach. The main purpose of the HttpFoundation component is to replace PHP global variables with an object-oriented layer.

To use this component, add it to the project dependencies in the composer.json file after this, run the command
 composer update 


Finally, at the bottom of the autoload.php file, add the code needed to automatically download the component:
 <?php // framework/autoload.php $loader->registerNamespace('Symfony\\Component\\HttpFoundation', __DIR__.'/vendor/symfony/http-foundation'); 


Now, we rewrite our application using the " Request " and " Response " classes:
 <php // framework/index.php require_once __DIR__.'/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $request = Request::createFromGlobals(); $input = $request->get('name', 'World'); $response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); $response->send(); 

The createFromGlobals () method creates an instance of the " Request " class based on the current values ​​of the PHP global variables.
The send () method sends the data of the " Response " class object back to the client (first go the HTTP headers followed by the content).



The main difference from the previous code is that you have full control over HTTP messages. You can create any request you want, and you are responsible for sending a reply whenever you see fit.



With the class " Request ", all the requested information is always at your fingertips, thanks to a nice and simple API:
 <?php //  URI (.. /about)    $request->getPathInfo(); //  GET  POST  $request->query->get('foo'); $request->request->get('bar', 'default value if bar does not exist'); //   SERVER $request->server->get('HTTP_HOST'); //    "UploadedFile" ( )   foo $request->files->get('foo'); //    COOKIE $request->cookies->get('PHPSESSID'); //   HTTP  $request->headers->get('host'); $request->headers->get('content_type'); $request->getMethod(); // GET, POST, PUT, DELETE, HEAD $request->getLanguages(); //  ,    


You can also simulate a query:
 $request = Request::create('/index.php?name=Fabien'); 


With the " Response " class, you can easily customize the answer:
 <?php $response = new Response(); $response->setContent('Hello world!'); $response->setStatusCode(200); $response->headers->set('Content-Type', 'text/html'); //   HTTP  $response->setMaxAge(10); 




Last but not least, these classes, like any other class in symfony, have been tested for security by an independent company. And, the Open-Source project also means that many other developers around the world are reading the code and have already fixed potential security issues. When was the last time you ordered a professional security audit for your self-made framework?

Even such a simple operation as returning a client's IP address may not be secure:
 <?php if ($myIp == $_SERVER['REMOTE_ADDR']) { //   ,    } 


This works fine until you add a reverse proxy in front of the production server, for now you have to change the code so that it works on both of your machines (if you do not have a reverse proxy on the development server):
 <?php if ($myIp == $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp == $_SERVER['REMOTE_ADDR']) { //   ,    } 

Using the Request :: getClientIp () method, you will have a working code from day one, regardless of the presence of a proxy.
 <?php $request = Request::createFromGlobals(); if ($myIp == $request->getClientIp()) { //   ,    } 

There is also an added advantage: it is safe by default. What do I mean by security? The end user can manipulate the value of $ _SERVER ['HTTP_X_FORWARDED_FOR'] and cannot be trusted. So, if you use this code in production without a proxy server, it becomes quite easy to abuse your system. This does not happen with the getClientIp () method, since you must explicitly indicate that you trust this header by calling trustProxyData () :
 <?php Request::trustProxyData(); if ($myIp == $request->getClientIp(true)) { //   ,    } 


Thus, getClientIp () works reliably in all circumstances. You can use it in all your projects, regardless of the configuration, it will behave properly and safely. This is one of the goals of using the framework. If you were writing a framework from scratch, you would have to think about all these cases yourself. So why not use technology that already works?

Believe it or not, now you have the first framework. You can stop at this, if you want of course. Using the Symfony2 component HttpFoundation already allows you to write better and more testable code. It also allows you to write code faster, as most daily problems are already solved for you.

In fact, projects such as Drupal have adopted (for upcoming version 8) the HttpFoundation component; if it works for them, it will probably work for you. Do not reinvent the wheel.

I almost forgot to mention one additional advantage: using the HttpFoundation component, you can achieve better interaction between frameworks and applications that use it (today Symfony2 , Drupal 8 , PhpBB 4 , Silex , Midgard CMS , Zikula ...).

Source: https://habr.com/ru/post/136430/


All Articles