Pull to refresh

Создаем собственный фреймворк на основе Symfony2. (Часть 2)

Symfony *
Translation
Original author: Fabien Potencier
Оглавление

Часть 1
Часть 2
Часть 3
Часть 4
Часть 5

Прежде чем мы углубимся в рефакторинг кода, я сначала хочу сделать шаг назад и взглянуть на то, почему вы хотели бы использовать фреймворк вместо того, чтобы писать ваше приложение на чистом PHP. Почему использовать фреймворк на самом деле хорошая идея, даже для простейшего фрагмента кода, и почему создание собственного фреймворка на основе компонентов Symfony2 лучше, чем создавать фреймворк с нуля.
  • Я не буду говорить об очевидных преимуществах использования фреймворков при работе с большими приложениями и с более чем несколькими разработчиками; в сети уже множество хороших ресурсов по этой теме.


Хотя «приложение» которые мы написали в прошлый раз было достаточно простое, у него есть несколько недостатков:
<?php

// framework/index.php

$input = $_GET['name'];

printf('Hello %s', $input);


Во-первых, если переменная name не задана в параметре запроса — будет выдаваться предупреждение PHP. Исправим это:
<?php

// framework/index.php

$input = isset($_GET['name']) ? $_GET['name'] : 'World';

printf('Hello %s', $input);


Верите или нет, даже этот небольшой кусок кода уязвим для одной из самых распространенных уязвимостей — XSS (Cross-Site Scripting). Вот более защищенная версия:
<?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'));

  • Как вы могли заметить защищать код с помощью htmlspecialchars() очень утомительно и может легко привести к опечатке. Это одна из причин использовать шаблонизатор такой как Twig, где автоматическое экранирование включено по умолчанию.


Как видите, изначально простой код становиться более сложным при добавлении проверок на безопасность и избежании предупреждений/уведомлений PHP.

Помимо безопасности, этот код так же не является легко тестируемым. Даже если там не так много, чтобы тестировать, мне кажется, что написание юнит-тестов для простейшего фрагмента PHP-кода не является естественным и будет выглядеть уродливо. Вот пример модульного теста PHPUnit для приведенного выше кода:
<?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);
    }
}


На данный момент, если вы не уверены в том, что безопасность и тестирование действительно две очень веские причины, чтобы не писать код по-старому, а адаптировать его под свой фреймворк, вы можете прекратить чтение этой серии в настоящее время и вернуться к коду, над которым вы работали ранее.
  • Конечно, использование фреймворка должно дать вам больше, чем просто безопасность и тестирование. Но более важно, что нужно иметь в виду, что фреймоврк должен позволить вам писать лучший код быстрее.


Переходим на ООП с использованием компонента HttpFoundation. (Going OOP with the HttpFoundation Component)



Написание веб-ориентированного кода это по сути работа с HTTP протоколом. Так что основные принципы нашего фремворка должны быть основаны на спецификации HTTP.

Спецификация HTTP описывает как клиент(например браузер) взаимодействует с сервером(нашим приложением через веб-сервер). Диалог между клиентом и сервером, является четко определенными сообщениями, запросов и ответов: клиент отправляет запрос на сервер и на основе этого запроса, сервер возвращает ответ.

В PHP запрос представляется в виде глобальных переменных ($_GET, $_POST, $_FILE, $_COOKIE, $_SESSION...), а ответ генерируется с помощью функций (echo, header, setcookie, ...).

Первый шаг к лучшему коду, это использование объектно-ориентированного подхода. Основная цель компонента HttpFoundation заменить глобальные переменные PHP объектно-ориентированным слоем.

Что бы использовать данный компонент, добавьте его в зависимости проекта в файле composer.json после сего выполните команду
composer update


Наконец, в нижней части файла autoload.php, добавьте код, необходимый для автоматической загрузки компонента:
<?php

// framework/autoload.php

$loader->registerNamespace('Symfony\\Component\\HttpFoundation', __DIR__.'/vendor/symfony/http-foundation');


Теперь, перепишем наше приложение с помощью классов "Request" и "Response":
<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();

Метод createFromGlobals() создает экземпляр класса "Request" на основе текущих значений глобальных переменных PHP.
Метод send() отправляет данные объекта класса "Response" обратно клиенту (сначала идут HTTP заголовки за которыми следует контент).

  • Подсказка: перед вызовом метода send() мы должны вызвать метод prepare() ($response->prepare($request);) что бы удостовериться что "Response" совместим с HTTP спецификацией. Например, если мы получаем страницу методом «HEAD», то нужно удалить контент с тела ответа.


Основное отличие от предидущего кода, это то, что у вас есть полный контроль над HTTP сообщениями. Вы можете создать любой запрос который вы хотите, и вы отвечаете за отправку ответа всякий раз, когда вы посчитаете нужным.

  • Мы не устанавливали заголовок «Content-Type» в переписанном коде, так как в объекте "Response" кодировкой по-умолчанию является UTF-8.


С классом "Request", вся запрашиваемая информация всегда находится у вас под рукой, благодаря приятному и простому 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(); // массив языков, приемлемых для клиента


Вы также можете имитировать запрос:
$request = Request::create('/index.php?name=Fabien');


С классом "Response", вы можете легко настроить ответ:
<?php

$response = new Response();

$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');

// настройка заголовка HTTP кеша
$response->setMaxAge(10);


  • Подсказка: в целях отладки "Response", приведите объект к строковому типу; это вернет HTTP представления ответа (заголовки и содержание)


Последнее, но не менее важное, эти классы, как и любой другой класс в Symfony, были проверены по вопросам безопасности независимой компанией. И, Open-Source проект также означает, что многие другие разработчики по всему миру читают код и уже исправили потенциальные проблемы безопасности. Когда в последний вы заказывали профессиональный аудит безопасности для вашего самодельного фреймворка?

Даже такая простая операция как возвращение IP-адресса клиента может быть не безопасной:
<?php

if ($myIp == $_SERVER['REMOTE_ADDR']) {
    // мы знаем клиента, дадим ему привилегии
}


Это работает прекрасно, пока вы не добавите реверс прокси перед сервером на продакшне, на данный момент, вам придется изменить код, чтобы он работал на обеих ваших машинах(если у вас на сервере разработки нет реверс прокси):
<?php

if ($myIp == $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp == $_SERVER['REMOTE_ADDR']) {
   // мы знаем клиента, дадим ему привилегии
}

Используя метод Request::getClientIp() у вас будет рабочий код с первого же дня, вне зависимости от наличия прокси.
<?php

$request = Request::createFromGlobals();

if ($myIp == $request->getClientIp()) {
    // мы знаем клиента, дадим ему привилегии
}

Также есть дополнительное преимущество: он является безопасным по умолчанию. Что я подразумеваю под безопасностью? Конечный пользователь может манипулировать значением $_SERVER['HTTP_X_FORWARDED_FOR'] и ему нельзя доверять. Итак, если вы используете этот код в продакшне без прокси-сервера, становится довольно просто злоупотребить им вашей системе. Этого не происходит с методом getClientIp (), так как вы должны явно указать что доверяете этому заголовку, вызвав trustProxyData ():
<?php

Request::trustProxyData();

if ($myIp == $request->getClientIp(true)) {
    // мы знаем клиента, дадим ему привилегии
}


Таким образом, getClientIp() работает надежно в любых обстоятельствах. Вы можете использовать его во всех ваших проектах, независимо от конфигурации, он будет вести себя правильно и безопасно. Это одна из целей использования фреймворка. Если бы вы писали фреймворк с нуля, вам пришлось бы думать обо всех этих случаях самим. Так почему не использовать технологию которая уже работает?
  • Если вы хотите узнать больше о компоненте HttpFoundation, вы можете взглянуть на API или прочитать документацию на сайте Symfony.

Верите или нет, но теперь у вас есть первый фреймворк. Вы можете остановиться и на этом, если хотите конечно. Использование компонента Symfony2 HttpFoundation уже позволяет писать вам лучший и более тестируемый код. Так же он позволяет писать код быстрее, так как большинство ежедневных проблем уже решены вместо вас.

На самом деле, проекты, такие как Drupal, приняли (для готовящейся к выходу версии 8) HttpFoundation компонент; если он работает для них, то вероятно, будет работать и для вас. Не изобретайте велосипед.

Я чуть не забыл упомянуть об одном дополнительном преимуществе: с помощью компонента HttpFoundation можно добиться лучшего взаимодействия фреймворков и приложений, использующих его (на сегодняшний день Symfony2, Drupal 8, PhpBB 4, Silex, Midgard CMS, Zikula … ).
Tags:
Hubs:
Total votes 18: ↑14 and ↓4 +10
Views 7.2K
Comments 5
Comments Comments 5

Posts