## PSR-7 фреймворк урок 1 из 7 "Структура и работа с HTTP"
### Общие принципы фреймворков, их типы и основное отличие от библиотек (компонентов)
Сегодня мы начинаем серию уроков по написанию **компонентного http-фреймворка**. Для чего это нам может быть нужно? Чтобы хорошо понять и изучить какую-то сложную архитектуру, необходимо пройти всю эволюцию развития такого рода продукта.
И только пройдя этот же путь (что прошел автор) можно осознать и понять многие глубинные вещи таких архитектур. Мы досконально разберемся в устройстве всех современных фреймворков и микрофреймворков.
Для начала разберемся с **понятием фреймворк** и его **отличием от такого понятия, как библиотека**.
Например, возьмем какой-то абстрактный проект, использующий две библиотеки (это могут быть библиотеки маршрутизатора и шаблонизатора). Таким образом, получается наш проект зависит от этих двух библиотек. Они находятся как бы внутри него. А **фреймворк – это когда мы создаем проект и он будет работать как бы внутри этого фреймворка**. То есть как бы ситуация наоборот – наизнанку. **Фреймворк определяет более высокоуровневую абстракцию**.
И фреймворки обычно задают свои правила по структуре проекта, по его архитектуре и также определяют места куда мы можем вставлять наш код (писать логику нашего приложения).
Разрабатывая свои классы (свою логику) мы как бы опираемся на какие-то базовые классы самого фреймворка. Например, создавая свой контроллер мы можем наследоваться от базового класса – Controller. Или создавая класс модели, мы наследуемся от базового класса - Model.
**Фреймворки предоставляют нам некую экосистему (некую конституцию, свод правил) придерживаясь которой мы строим свой проект**.
Обычно у всех современных фреймворков сам исходный код не навязывает какую-то определенную архитектуру по построению проектов. Но эти фреймворки обычно содержат свой **skeleton-приложения**. Некая отправная точка по построению наших проектов. И вот она уже содержит и архитектуру и структуру (философию), которой можно либо жестко придерживаться, либо видоизменять частично или полностью.
Фреймворки обычно подразделяются на **монолитные – это когда практически нельзя поменять ни архитектуру проекта, ни добавить или подменить какую-либо функциональную часть** (заменить тот же шаблонизатор или еще что-то такое) и **гибкие (компонентные) – это когда можно изменить в любое время и архитектуру проекта и заменить (подменить) какую-либо его часть функционал**.
### Кратко о развитии языка javascript
Абстрагируемся и кратко взглянем на **эволюцию языка javascript**. Когда этот язык только создавался, то максимально на что его хватало – это сделать какую-то простенькую динамику на сайте (вывести часики, навигационное выпадающее или еще какое меню и т.д.). В процессе развития появилась мощная библиотека – **jquery**. С помощью нее можно было строить динамичные полноценные приложения. Работать гибко с DOM-моделью документа, отправлять AJAX-запросы, делать анимации и многое другое.
Впоследствии с помощью этого языка стало возможным разрабатывать не только клиентские вещи (изначально javascript был только клиентским языком), но и писать полноценные серверные приложения. Это стало возможным благодаря появлению технологии – **node-js**. Это также позволило развитию этого языка в сторону консольных приложений (те же сборщики, например, **gulp-js** & **webpack-js**).
У javascript появился свой пакетный менеджер (ведь без этого инструмента в современной разработке сложно выстраивать и поддерживать архитектуру приложения) – **npm** (впоследствии появился аналог от Facebook’а **yarn**).
**Благодаря всем этим технологиям язык javascript в последние годы сумел захватить немалую часть всей веб-разработки**. Т.е. язык сумел превратиться из "гадкого" утёнка в более менее красивого лебедя.
### Кратко о развитии языка php
Примерно похожей эволюции придерживается и язык **php**. Вначале своего пути этот язык тоже задумывался и использовался, как мелкий скриптовый язык. Для генерации какой-то мелкой динамики на сайтах. Те же часики и что-то подобное. И название у него вначале было соответствующее – **personal home page tools** (т.е. по сути некий набор утилит для создания и поддержки персональных страничек).
Но приобретя популярность язык вырос в полноценный язык на котором стали писать полноценные динамичные сайты. Это всевозможные гостевые книги, форумы, социальные сети и многое другое. И название языка сменило себя на более серьезное. Он стал называться, как **hypertext preprocessor** (дословно препроцессор гипертекста).
С появлением четвертой версии (PHP 4) языка появилась возможность строить проекты с применением **парадигмы ООП** (**объектно-ориентированное программирование**). И только в 2004 году с появлением версии пятой языка стало возможным полноценно применять эту парадигму в своих проектах.
В 2011 или 2012 году (только тогда) язык обзавелся своим пакетным менеджером – **composer**. И он позволил строить приложения (проекты), включающие в себя десятки (сотни и даже тысячи) зависимостей. И разворачивать такие проекты буквально одной командой.
### Опыт от "старших" братьев, социальный кодинг и пакетные менеджеры и их главное преимущество
Резюмируя все вышесказанное можно сказать о том, что **языки javascript** & **php достаточно молодые языки**. Которые получили заслуженное призвание только в последние там 5-7 лет. И **появлению многих современных features** (того же **ООП**) **они обязаны своим старшим собратьям**. Тому же **c++**, **java** и многим другим серьезным и "старым" языкам программирования. Новички это воспринимают достаточно болезненно и считают, что из их привычного скриптового языка пытаются сделать какую-то пародию той же java. Но с этим надо смирится и как-то научиться жить!
Социальный кодинг так называемый пришел в эти языки благодаря развитию **git** & **github.com**. И если раньше своим кодом делились мало и только в рамках каких-то архивов немногочисленных. То в последние годы **появилось очень много open source решений** (**библиотек**). И это стало возможным благодаря мощному развитию этих технологий. **Главные плюсы этого подхода стало – удобное выкладывание (sharing) своего кода и совместная поддержка таких библиотек**.
Но вручную дергать с того же github’а библиотеки и встраивать их в свои проекты не очень то и удобно. Это все рано или поздно разработчикам надоело. Плюс надо было бороться с возникающими конфликтными ситуациями между совместимостью разных (не связанных) компонентов (библиотек). И именно эта проблема послужила главным "двигателем" к **появлению пакетных менеджеров** (**npm**/**yarn** & **composer**). Эти инструменты позволяли автоматически разруливать зависимости, бороться с конфликтными ситуациями и прочими вещами.
Все это также наложило некоторые требования к осмыслению привычного процесса разработки. Особенно у тех разработчиков, которые начинали с 2004 и более раннего времени разрабатывать проекты на том же языке php. Здесь **потребуется немного изменить привычный образ мышления разработчика**.
### Абстрактный и реальный отличный примеры, когда новые подходы сильно помогают при разработке и это в свою очередь сказывается положительно на конечных результатах
И таким образом, мы имеем следующую ситуацию. Некий разработчик написал свою библиотеку (или компонент) и выложил ее/его на github (или на **packagist’е – это хранилище библиотек пакетного менеджера composer**). Другой разработчик написал свой компонент и также разместил его в публичный доступ. А третий программист сел и написал свою библиотеку, использовав компоненты (библиотеки) двух предыдущих разработчиков. Т.е. он сделал зависимой свою библиотеку от других вещей. А четвертый разработчик вообще разработал свой фреймворк, применив в нем зависимости от второго и третьего программистов.
```html
github.com/разработчик_1/библиотека_1
github.com/разработчик_2/библиотека_2
github.com/разработчик_3/библиотека_3
require: [
github.com/разработчик_1/библиотека_1,
github.com/разработчик_2/библиотека_2
]
github.com/разработчик_4/фреймворк_4
require: [
github.com/разработчик_2/библиотека_2,
github.com/разработчик_3/библиотека_3
require: [
github.com/разработчик_1/библиотека_1,
github.com/разработчик_2/библиотека_2
]
]
```
В итоге мы получаем такую **древовидную сеть с гибким переплетением компонентов между собой**. В одних проектах (ведь разрабатывая свой компонент или библиотеку мы по сути тоже работаем над созданием своего проекта или конечного продукта) мы используем один стек библиотек, а в других другой.
Таким образом, получается, что каждый разработчик занимается построением логики только своего проекта. Не вдаваясь в детализацию алгоритмов "чужого" кода (т.е. кода от которого зависит его данный проект).
Когда-то кто-то заморочился и написал **хорошие компоненты/библиотеки**:
- логирование приложений на языке php ([**Seldaek/monolog**](https://github.com/Seldaek/monolog.git))
- клиентская библиотека http для языка php ([**guzzle/guzzle**](https://github.com/guzzle/guzzle))
На протяжении этого курса мы **научимся использовать "чужие" библиотеки** (внедрять их в наш проект) и также **научимся писать логику проекта так, чтобы когда мы его захотим выложить в публичный доступ им захотели воспользоваться другие** (и нам за это не было стыдно).
### Создание "точки входа" в приложение, понятия запрос-ответ сервера и отправка своих заголовков ответа клиенту
С чего мы обычно начинаем?
С создания файла – **index.php** (его еще **называют точкой входа в приложение**).
```php
<?php
echo 'Hello';
```
Помимо этого мы можем передать в наш скрипт какие-то параметры. Например, имя пользователя (через **$_GET параметр**).
```php
<?php
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
На самом деле под "капотом" этого простого действия скрыты два очень важных действия протокола http. Браузер (клиент) отправляет серверу **http запрос** (**http request**) на что сервер готовит и отдает ему **http ответ** (**http response**).
Вот примерно следующее отправляет браузер (клиент) серверу:
```http
Request Headers
GET / HTTP/1.1
Host: localhost
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=1b7880531e556088778b75cc2e27a27a
```
На что сервер отвечает примерно следующим:
```http
Response Headers
HTTP/1.1 200 OK
Server: nginx/1.15.12
Date: Fri, 09 Aug 2019 20:16:52 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.3.6
Content-Encoding: gzip
```
Мы в свою очередь можем отправить с сервера, используя опять же язык php свои **заголовки ответа** (**headers response**). Например, так:
```php
<?php
$name = $_GET['name'] ?: 'Guest';
header('X-Developer: Denis Kitaev');
echo 'Hello, ' . $name;
```
```http
Response Headers
HTTP/1.1 200 OK
Server: nginx/1.15.12
Date: Fri, 09 Aug 2019 20:20:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.3.6
X-Developer: Denis Kitaev
Content-Encoding: gzip
```
**Важное замечание**! Главная задача любого веб-сервера – это принять запрос от клиента, как-то его проанализировать (обработать) и вернуть ответ обратно клиенту.
### Суперглобальные структуры данных и написание функции по определению языка клиента
В языке php есть **суперглобальные массивы**
```php
$_GET, $_POST, $_SERVER, $_SESSION, $_COOKIE, $_FILES, $_ENV
```
Например, мы хотим реализовать **функционал по определению языка у пользователя** в зависимости от разных условий
```php
<?php
session_start();
chdir(dirname(__DIR__));
## Initializing the application
/**
* The function of determining the language of the client
*
* @param string $default
* @return string
*/
function getLang($default = 'en') : string {
if (!empty($_GET['lang'])) return $_GET['lang'];
else if (!empty($_COOKIE['lang'])) return $_COOKIE['lang'];
else if (!empty($_SESSION['lang'])) return $_SESSION['lang'];
else if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
return substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
else return $default;
}
## Running the application
$lang = getLang();
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name . '! Your lang is "' . $lang . '"';
```
**Важное замечание**! Функция удобная, но работает с суперглобальными массивами внутри себя. Т.е. функция не является "чистой" и лезет наружу. Это не хорошо!
### Рефакторинг функции в "чистую"
**Сделаем функцию "чистой"**
```php
<?php
session_start();
chdir(dirname(__DIR__));
## Initializing the application
/**
* The function of determining the language of the client
*
* @param $get
* @param $cookie
* @param $session
* @param $server
* @param string $default
* @return string
*/
function getLang($get, $cookie, $session, $server, string $default = 'en') : string {
if (!empty($get['lang'])) return $get['lang'];
else if (!empty($cookie['lang'])) return $cookie['lang'];
else if (!empty($session['lang'])) return $session['lang'];
else if (!empty($server['HTTP_ACCEPT_LANGUAGE']))
return substr($server['HTTP_ACCEPT_LANGUAGE'], 0, 2);
else return $default;
}
## Running the application
$lang = getLang($_GET, $_COOKIE, $_SESSION, $_SERVER);
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name . '! Your lang is "' . $lang . '"';
```
**Важное замечание**! Преимущество "чистой" функции в том, что сколько ее не вызывай с одними и теми же параметрами, то результат будет всегда одинаковый. Такую функцию удобно тестировать, поскольку можно на вход ей подать любые "фейковые" данные (эмулирующие те же суперглобальные массивы) и все будет работать точно также.
### Рефакторим аргументы функции - many-to-one (по сути записываем в одну структуру - request)
Но работать и постоянно "таскать" за собой эти суперглобальные массивы, вставляя их во всевозможные нужные места не очень то и удобно.
**Что же делать в этом случае?**
Можно создать массив (**$request**), в который "уложить" все суперглобальные массивы. Например, таким образом
```php
<?php
session_start();
chdir(dirname(__DIR__));
## Initializing the application
/**
* The function of determining the language of the client
*
* @param array $request
* @param string $default
* @return string
*/
function getLang(array $request, string $default = 'en') : string {
if (!empty($request['get']['lang'])) return $request['get']['lang'];
else if (!empty($request['cookie']['lang'])) return $request['cookie']['lang'];
else if (!empty($request['session']['lang'])) return $request['session']['lang'];
else if (!empty($request['server']['HTTP_ACCEPT_LANGUAGE']))
return substr($request['server']['HTTP_ACCEPT_LANGUAGE'], 0, 2);
else return $default;
}
## Running the application
$request = [
'get' => $_GET,
'cookie' => $_COOKIE,
'session' => $_SESSION,
'server' => $_SERVER
];
$lang = getLang($request);
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name . '! Your lang is "' . $lang . '"';
```
**Важное замечание**! Вроде бы все стало гораздо лучше, чем было до того. Но работать с ассоциативными массивами не очень то и безопасно. Поскольку можно допустить где-то опечатку, что-то забыть, не так обозвать и т.д. И логика работы программы нарушится, хотя синтаксически с точки зрения языка все будет правильно.
### Переписываем нашу функцию в объектно-ориентированную парадигму
Потому мы можем написать такой класс (**class Request**)
```php
<?php
/**
*
* Class Request
*/
class Request
{
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $_GET;
}
/**
* Get array $_COOKIE
*
* @return array
*/
public function getCookiesData() : array
{
return $_COOKIE;
}
/**
* Get array $_SESSION
*
* @return array
*/
public function getSessionsData() : array
{
return $_SESSION;
}
/**
* Get array $_SERVER
* @return array
*/
public function getServersData() : array
{
return $_SERVER;
}
}
```
И использовать его в нашей функции по определению языка
```php
<?php
session_start();
chdir(dirname(__DIR__));
## Initializing the application
/**
*
* Class Request
*/
class Request
{
/* ... */
}
/**
* The function of determining the language of the client
*
* @param Request $request
* @param string $default
* @return string
*/
function getLang(Request $request, string $default = 'en') : string {
if (!empty($request->getQueriesParams()['lang'])) return $request->getQueriesParams()['lang'];
else if (!empty($request->getCookiesData()['lang'])) return $request->getCookiesData()['lang'];
else if (!empty($request->getSessionsData()['lang'])) return $request->getSessionsData()['lang'];
else if (!empty($request->getServersData()['HTTP_ACCEPT_LANGUAGE']))
return substr($request->getServersData()['HTTP_ACCEPT_LANGUAGE'], 0, 2);
else return $default;
}
## Running the application
$lang = getLang(new Request());
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name . '! Your lang is "' . $lang . '"';
```
### Перерабатываем нашу структуру приложения
**Поработаем немного со структурой нашего приложения**. Вынесем в отдельную директорию исходники нашего будущего фреймворка
```php
<?php
session_start();
chdir(dirname(__DIR__));
## Initializing the application
require_once '/sources/Framework/Http/Request';
/**
* The function of determining the language of the client
*
* @param Request $request
* @param string $default
* @return string
*/
function getLang(Request $request, string $default = 'en') : string {
if (!empty($request->getQueriesParams()['lang'])) return $request->getQueriesParams()['lang'];
else if (!empty($request->getCookiesData()['lang'])) return $request->getCookiesData()['lang'];
else if (!empty($request->getSessionsData()['lang'])) return $request->getSessionsData()['lang'];
else if (!empty($request->getServersData()['HTTP_ACCEPT_LANGUAGE']))
return substr($request->getServersData()['HTTP_ACCEPT_LANGUAGE'], 0, 2);
else return $default;
}
## Running the application
$lang = getLang(new Request());
$name = $_GET['name'] ?: 'Guest';
echo 'Hello, ' . $name . '! Your lang is "' . $lang . '"';
```
**Важное замечание**! Здесь мы определили корневую директорию нашего приложения, сделав ее рабочей. И вынесли класс запроса в иерархию исходников будущего нашего фреймворка.
Пропишем **namespace** для класса **Request**
```php
<?php
namespace App\Framework\Http;
/**
*
* Class Request
*/
class Request
{
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $_GET;
}
/**
* Get array $_COOKIE
*
* @return array
*/
public function getCookiesData() : array
{
return $_COOKIE;
}
/**
* Get array $_SESSION
*
* @return array
*/
public function getSessionsData() : array
{
return $_SESSION;
}
/**
* Get array $_SERVER
* @return array
*/
public function getServersData() : array
{
return $_SERVER;
}
}
```
### Рефакторим наш класс под основные методы запросов
Существуют также **стандартные методы запроса**
```html
GET, POST, PUT, PATCH, DELETE
```
**Важное замечание**! **GET** (обычно для получения каких-либо данных с сервера из какого-либо хранилища), **POST** (для создания новых записей в каком-то хранилище данных на сервере), **PUT**/**PATCH** (для обновления записей в каком-то хранилище на сервере) и **DELETE** (для удаления записей в каком-то хранилище на сервере).
Причем запросы методами POST/PUT/PATCH/DELETE – могут приходить с различными post-параметрами.
Согласно этому перепишем наш **класс Request**
```php
<?php
namespace App\Framework\Http;
/**
*
* Class Request
*/
class Request
{
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $_GET;
}
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody() : array
{
return $_POST ?: null;
}
}
```
**Важное замечание**! Теперь наш класс запросов работает только с входными данными из запроса клиента. И метод **getParsedBody** у нас в будущем может заниматься распарсиванием каких-либо "сырых" данных. Например, это могут быть не только данные с форм, но и какие-нибудь json-объекты или xml-объекты. Они попадают в такой **псевдопротокол php** – **php://input**.
Код точки входа (**entry point**)
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\Request;
## Initializing the application
require_once '/sources/Framework/Http/Request.php';
$request = new Request();
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
В процессе нашей работы классов у нас будет достаточно много и потому необходимо организовать какую-либо удобную их автозагрузку.
### Работаем с файлом "composer.json", ставим полезную библиотеку и включаем сортировку зависимостей проекта
Удобнее всего это сделать через файл – **composer.json**
```json
{
"config": {
"sort-packages": true
},
"require": {
"php": "^7.3.0"
}
}
```
**Важное замечание**! В секции **require** мы обычно указываем необходимые зависимости (от каких библиотек зависит наше разрабатываемое приложение), версию минимальную языка php и его расширений различных. В секции **config** – **sort-packages** ставим в значение истины, что будет означать выстраивание по алфавиту всех зависимых библиотек в секции require.
Подключим библиотеку – **roave/security-advisories** в
```bash
$ composer require roave/security-advisories:dev-master
```
файл - composer.json
```json
{
"config": {
"sort-packages": true
},
"require": {
"php": "^7.3.0",
"roave/security-advisories": "dev-master"
}
}
```
**Важное замечание**! Эта **зависимость включит определенный список всех библиотек (фреймворков) в версиях которых обнаружены серьезные уязвимости безопасности**. И это поможет composer’у отслеживать версии таких пакетов, которые подтягиваются в наш проект и сигнализировать об этом. Эдакая **защита от случайного подтягивания старых версий и/или опасных версий каких-то библиотек**.
### Подключаем автозагрузку через composer наших исходных классов проекта
Попробуем загрузить **файл автозагрузки composer** в наш проект
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\Request;
## Initializing the application
require_once '/vendor/autoload.php';
$request = new Request();
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
И получим закономерную ошибку, что такой класс не найден
```html
Fatal error: Uncaught Error: Class 'App\Framework\Http\Request' not found in /application/public/index.php on line 11
```
**Важное замечание**! Почему так происходит? Потому, что ничего о нашей папке и внутренних ее директорий и файлов composer не знает мы ему об этом нигде ничего не сказали и не указали, где и как он должен это все искать и находить. Он ищет только то, что лежит внутри каталога "vendor"
Используя **стандарт именования классов PSR-4**, пропишем **автозагрузку наших классов в файл composer.json**
```json
{
"config": {
"sort-packages": true
},
"require": {
"php": "^7.3.0",
"roave/security-advisories": "dev-master"
},
"autoload": {
"psr-4": {
"App\\Framework\\": "sources/Framework/"
}
}
}
```
**Важное замечание**! Обязательно необходимо выполнить команду
```bash
$ composer dump-autoload
```
для перегенерации файлов автозагрузки composer.
Внутри директории vendor в файле **autoload_psr4.php** мы увидим закэшированную нашу директорию
```php
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'App\\Framework\\' => array($baseDir . '/sources/Framework'),
);
```
**Важное замечание**! Это особенно **полезно для Yii-разработчиков**. Там как-то почему то не принято использовать такую конструкцию для автозагрузки файлов в composer-файле.
### Устанавливаем компонент для тестирования проекта - "phpunit" и создаем первый наш тестовый класс
Теперь нам необходимо написанный наш первый **класс запросов** (**Request**) как-то **протестировать**. Или еще говорят на сленге программистов – **покрыть тестами**. Для этого установим фреймворк (библиотеку) **phpunit/phpunit**
```bash
$ composer require phpunit/phpunit --dev
```
Создадим директорию, где будут храниться наши тесты (**application/tests**). И в корне нашего проекта добавим **конфигурационный файл** самого phpunit – **phpunit.xml.dist**
```xml
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="./vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./sources/</directory>
</whitelist>
</filter>
</phpunit>
```
**Важное замечание**! Библиотека phpunit вначале будет искать **файл под названием – phpunit.xml** и если такой не найдется, то считает файл – **phpunit.xml.dist**. Обычно файл phpunit.xml является локальным файлом каждого разработчика (по аналогии с тем же файлом .env), работающего над проектом и создавая такой файл он может перебить глобальные настройки в файле phpunit.xml.dist
А в **.gitignore** мы можем прописать такое сразу
```html
vendor
phpunit.xml
```
**Напишем первый тест** для нашего класса запросов
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\Request;
use PHPUnit\Framework\TestCase;
/**
* Class RequestTest
* @package Tests\Framework\Http
*/
class RequestTest extends TestCase
{
/**
* Void testing
*/
public function testEmpty() : void
{
$_GET = $_POST = [];
$request = new Request();
self::assertEquals([], $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing to get $_GET query parameters
*/
public function testQueryParams() : void
{
$_GET = $data = [
'name' => 'Denis',
'age' => 35
];
$_POST = [];
$request = new Request();
self::assertEquals($data, $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing for $_POST query parameters
*/
public function testParsedBody() : void
{
$_GET = [];
$_POST = $data = [
'name' => 'Denis',
'age' => 35
];
$request = new Request();
self::assertEquals([], $request->getQueriesParams());
self::assertEquals($data, $request->getParsedBody());
}
}
```
### Запускаем наши тесты
Вызовем наши **тесты** командой
```bash
$ vendor/bin/phpunit --colors=always
```
**Важное замечание**! Видим, что все тесты прошли успешно
И можем запустить команду **покрытие тестами** нашего приложения
```bash
$ vendor/bin/phpunit --colors=always --coverage-html tests/coverage
```
**Важное замечание**! Для того, чтобы это все сработало необходимо, чтобы в php было установлено **расширение xdebug**. А так мы видим, что у нас 100% покрытие тестами
### Рефакторим наш тестовый класс, вынося в базовый метод - "setUp" инициализацию тестовых данных
Мы также можем переопределить **стандартный метод библиотеки phpunit** и не в каждом методе тестовом переопределять суперглобальные переменные, а задать их в одном месте. Например, так, используя **метод setUp**
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\Request;
use PHPUnit\Framework\TestCase;
/**
* Class RequestTest
* @package Tests\Framework\Http
*/
class RequestTest extends TestCase
{
/**
* Standard method for initializing the logic under test
*/
public function setUp() : void
{
parent::setUp();
$_GET = $_POST = [];
}
/**
* Void testing
*/
public function testEmpty() : void
{
$request = new Request();
self::assertEquals([], $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing to get $_GET query parameters
*/
public function testQueryParams() : void
{
$_GET = $data = [
'name' => 'Denis',
'age' => 35
];
$request = new Request();
self::assertEquals($data, $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing for $_POST query parameters
*/
public function testParsedBody() : void
{
$_POST = $data = [
'name' => 'Denis',
'age' => 35
];
$request = new Request();
self::assertEquals([], $request->getQueriesParams());
self::assertEquals($data, $request->getParsedBody());
}
}
```
### Разбираемся с флагом "--dev" нашего пакетного менеджера
**Важное замечание**! Мы установили библиотеку phpunit, использовав **флаг "--dev"** и получили этот компонент **в секции "require-dev"**. Что логично, поскольку процесс тестирования обычно нужен только на этапе разработки приложения. Но, если мы запустим команду
```bash
$ composer install
```
то у нас установятся все зависимости и из **секции "require"** и из **секции "require-dev"**. Как же быть? На боевом production-сервере мы можем при разворачивании зависимостей использовать **флаг "--no-dev"**, т.е. полная команда будет
```bash
$ composer install –no-dev
```
### Выносим запуск тестов в скрипты composer или в отдельную консольную команду UNIX
**Важное замечание**! Чтобы не писать всякий раз длинные команды на запуск тех же тестов **"vendor/bin/phpunit"**. Можно занести их либо в специальный **скриптовый файл "makefile"**
```makefile
phpunit:
vendor/bin/phpunit --colors=always
phpunit-coverage:
vendor/bin/phpunit --colors=always --coverage-html tests/coverage
```
либо же **в секцию "scripts" самого файла "composer.json"**
```json
{
"config": {
"sort-packages": true
},
"require": {
"php": "^7.3.0",
"roave/security-advisories": "dev-master"
},
"autoload": {
"psr-4": {
"App\\Framework\\": "sources/Framework/"
}
},
"require-dev": {
"phpunit/phpunit": "^8.3"
},
"scripts": {
"tests": "vendor/bin/phpunit --colors=always",
"tests-coverage": "vendor/bin/phpunit --colors=always --coverage-html tests/coverage",
"tests-all": "composer tests && composer tests-coverage"
}
}
```
**Важное замечание**! Флаг "--colors=always" указали для раскраски вывода результата тестов. И запустив команду
```bash
$ composer tests
```
убедимся, что тесты также проходят успешно
### Разбираемся с текущими "слабыми" местами нашего класса запросов. И почему важно разрабатывая приложение использовать парадигму TTD (test driven development)
Главная проблема нашего класса, что он работает напрямую с глобальными (суперглобальными) переменными. А это значит, что создав два инстанса такого класса мы не получим два независимых от внешнего мира объекта. Поскольку они оба будут иметь дело с сущностью, находящуюся извне.
Такие объекты невозможно нормально тестировать и они легко поддаются ошибкам. Можем поменять что-то в одном и что-то может нарушиться в другом. Это невозможно практически контролировать.
**Важное замечание**! Это так получилось отчасти от того, что вначале мы написали класс (реализующий какую-то сущность), а после стали его тестировать. Если бы сделали наоборот, то такой проблемы бы в 99% случаев не получили.
**Важное замечание**! Когда мы работаем с какой-то сущностью, как с "черным" ящиком, то такой код проще поддается тестированию и невозможно получить конфликтующий код (когда одна сущность что-то поменяла снаружи и другая сущность перестала функционировать). Т.е. аналогия с "чистой" функцией – мы передали что-то на вход и она только с этим работает внутри себя что-то там делает и возвращает итоговый результат. Такие **инстансы классов называются "чистыми"**
### Инкапсулируем внутри нашего класса так называемые данные с которыми работает наша сущность - Request
Давайте, следуя этому принципу перепишем наш класс **Request** и тестирующий его класс **RequestTest** тоже
```php
<?php
namespace App\Framework\Http;
/**
*
* Class Request
*/
class Request
{
private $_queryParams = [];
private $_parsedBody = null;
/**
* Request constructor.
* @param array $queryParams
* @param array|null $parsedBody
*/
public function __construct(array $queryParams = [], $parsedBody = null)
{
$this->_queryParams = (array)$queryParams;
$this->_parsedBody = (array)$parsedBody;
}
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $this->_queryParams;
}
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody() : array
{
return $this->_parsedBody ?: null;
}
}
```
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\Request;
use PHPUnit\Framework\TestCase;
/**
* Class RequestTest
* @package Tests\Framework\Http
*/
class RequestTest extends TestCase
{
/**
* Void testing
*/
public function testEmpty() : void
{
$request = new Request();
self::assertEquals([], $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing to get $_GET query parameters
*/
public function testQueryParams() : void
{
$request = new Request($data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals($data, $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Testing for $_POST query parameters
*/
public function testParsedBody() : void
{
$request = new Request([], $data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals([], $request->getQueriesParams());
self::assertEquals($data, $request->getParsedBody());
}
}
```
И еще изменим нашу **"точку входа"** (**entry point**) в приложение
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\Request;
## Initializing the application
require_once '/vendor/autoload.php';
$request = new Request($_GET, $_POST);
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
**Важное замечание**! И получается, что только из этого файла мы работаем и передаем переменные суперглобальные - $_GET & $_POST. Нигде больше в другом месте мы этого не делаем. И это хороший подход именно так делать!
### Выводим наш класс запросов из состояния "read-only". Добавляем ему "сеттеры" (setters)
**Важное замечание**! Таким образом, наш класс запросов стал полностью универсальным и его можно использовать хоть в нашем приложении, хоть в каком-нибудь другом "чужом". Хоть для эмуляции чего-либо. Ему это все равно, поскольку что ему передадим, с тем он и будет работать. Единственное те данные, что мы ему передаем они остаются внутри него только для чтения. Иногда это не очень удобно. Потому принято в таких случаях добавлять некоторые так **называемые "сеттеры"**
Потому перепишем наш класс запросов, **создав необходимые "сеттеры"**
```php
<?php
namespace App\Framework\Http;
/**
*
* Class Request
*/
class Request
{
private $_queryParams = [];
private $_parsedBody = null;
/**
* Request constructor.
* @param array $queryParams
* @param array|null $parsedBody
*/
public function __construct(array $queryParams = [], $parsedBody = null)
{
$this->_queryParams = (array)$queryParams;
$this->_parsedBody = (array)$parsedBody;
}
/**
* To add a new $_GET query parameters
*
* @param array $queryParams
* @return self
*/
public function withQueryParams(array $queryParams) : self
{
$this->_queryParams = (array)$queryParams;
return $this;
}
/**
* To add new $_POST query parameters
*
* @param array $data
* @return self
*/
public function withParsedBody(array $data) : self
{
$this->_parsedBody = (array)$data;
return $this;
}
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $this->_queryParams;
}
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody() : array
{
return $this->_parsedBody ?: null;
}
}
```
**Важное замечание**! По сути можно было бы убрать из этого класса конструктор. В нем особой необходимости уже нет. Разве что для удобства, когда создаем новый инстанс, то сразу можем передать и необходимые ему данные во внутрь. Но сейчас у нас эту **роль выполняют методы-мутаторы**. Так называют методы, которые что-то изменяют внутри объекта. Как-то изменяют объект.
И **"точку входа"** (**entry point**) тоже можно слегка изменить
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\Request;
## Initializing the application
require_once '/vendor/autoload.php';
$request = (new Request())
->withQueryParams($_GET)
->withParsedBody($_POST);
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
И даже тут есть небольшая погрешность, что при "неправильном" понимании копирования инстансов у классов в языке php можно наскрести кучу проблем.
### Правильное копирование инстансов в языке php
Вот, как следует **правильно копировать инстансы в языке php**
```php
<?php
$request_1 = new Request();
$request_2 = clone $request_1;
$request_2->withQueryParams([
'lang' => 'ru'
]);
echo getLang($request_1, 'en'); // en
echo getLang($request_2, 'en'); // ru
```
**Важное замечание**! Тут при верном клонировании объектов мы получаем закономерный результат. Стоит помнить, что обычные **скалярные величины в языке php копируются по значению**, нежели сложные (объектные).
### Разбираемся с понятием "иммутабельных" объектов
Есть еще такое понятие, как **"иммутабельный объект"**. Которые не меняются внутри себя. Например, так
```php
<?php
$request_1 = new Request();
$request_2 = clone $request_1;
$request_2 = $request_1->withQueryParams([
'lang' => 'ru'
]);
echo getLang($request_1, 'en'); // en
echo getLang($request_2, 'en'); // ru
```
**Важное замечание**! Такого рода объекты они с чем инстанцировались, с тем и работают всегда. У них всегда одинаковое состояние. Свое **внутреннее состояние они не меняют**.
### Отрефакторим наш класс запросов, сделав его "иммутабельным"
Следуя этой логике сделаем наш класс **Request "иммутабельным"**
```php
<?php
namespace App\Framework\Http;
/**
*
* Class Request
*/
class Request
{
private $_queryParams = [];
private $_parsedBody = null;
/**
* Request constructor.
* @param array $queryParams
* @param array|null $parsedBody
*/
public function __construct(array $queryParams = [], $parsedBody = null)
{
$this->_queryParams = (array)$queryParams;
$this->_parsedBody = (array)$parsedBody;
}
/**
* To add a new $_GET query parameters
*
* @param array $queryParams
* @return self
*/
public function withQueryParams(array $queryParams = []) : self
{
$newInstance = clone $this;
$newInstance->_queryParams = (array)$queryParams;
return $newInstance;
}
/**
* To add new $_POST query parameters
*
* @param array|null $data
* @return self
*/
public function withParsedBody($data = null) : self
{
$newInstance = clone $this;
$newInstance->_parsedBody = (array)$data;
return $newInstance;
}
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array
{
return $this->_queryParams;
}
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody()
{
return $this->_parsedBody ?: null;
}
}
```
И теперь у нашей **"точки входа"** вместо одного объекта по цепочке идет **порождение трёх инстансов класса Request**. Например, визуально это можно изобразить так
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\Request;
## Initializing the application
require_once '/vendor/autoload.php';
$request_1 = new Request();
$request_2 = $request_1->withQueryParams($_GET);
$request_3 = $request_2->withParsedBody($_POST);
## Running the application
$name = $request_3->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
### Каждый вызов "мутабельного" метода порождает новый инстанс
**Важное замечание**! На каждом новом вызове мутабельного метода у нас **идет порождение нового инстанса класса**, который наследует своего предка из которого дергается мутабельный метод
```bash
$request_1 => Request {#3 ▼
-_queryParams: []
-_parsedBody: null
}
```
```bash
$request_2 => Request {#2 ▼
-_queryParams: array:2 [▼
"name" => "Denis"
"lang" => "ru"
]
-_parsedBody: null
}
```
```bash
$request_3 => Request {#4 ▼
-_queryParams: array:2 [▼
"name" => "Denis"
"lang" => "ru"
]
-_parsedBody: []
}
```
**Важное замечание**! Ненавистниками "иммутабельных объектов" выступают "борцы за быстродействие кода". Но надо понимать, что в памяти хранится только по сути название объекта и значения его полей.
### Рефакторим наши тесты под "иммутабельность"
Вследствие этого у нас изменятся тесты этого класса **RequestTest**
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\RequestFactory;
use PHPUnit\Framework\TestCase;
/**
* Class RequestTest
* @package Tests\Framework\Http
*/
class RequestTest extends TestCase
{
/**
* Тестирование на пустоту
*/
public function testEmpty(): void
{
$request = RequestFactory::fromGlobals();
self::assertEquals([], $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Тестирование на получение GET-параметров запроса
*/
public function testQueryParams(): void
{
$request = RequestFactory::fromGlobals($data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals($data, $request->getQueriesParams());
self::assertNull($request->getParsedBody());
}
/**
* Тестирование на получение POST-параметров запроса
*/
public function testParsedBody(): void
{
$request = RequestFactory::fromGlobals([], $data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals([], $request->getQueriesParams());
self::assertEquals($data, $request->getParsedBody());
}
}
```
### Для удобства инстанцирования класса может создать класс-фабрику - RequestFactory
Но передавать в инстанс класса запросов каждый суперглобальный массив (там помимо $_GET, $_POST могут быть еще $_SESSION, $_COOKIE и многие другие). Потому можно создать **класс-фабрику RequestFactory**
```php
<?php
namespace App\Framework\Http;
/**
* Class RequestFactory
* @package App\Framework\Http
*/
class RequestFactory
{
/**
* Static method for obtaining superglobal arrays
*
* @param array $queryParams
* @param array|null $parsedBody
* @return Request
*/
public static function fromGlobals(array $queryParams = [], $parsedBody = null) : Request
{
return (new Request())
->withQueryParams($queryParams ?: $_GET)
->withParsedBody($parsedBody ?: $_POST);
}
}
```
Изменим нашу **"точку входа"** (**entry point**)
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\RequestFactory;
## Initializing the application
require_once 'vendor/autoload.php';
$request = RequestFactory::fromGlobals();
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
echo 'Hello, ' . $name;
```
**Важное замечание**! Теперь, если мы передадим что-то во внутрь нашей фабрики (RequestFactory::fromGlobals(...)), то у нас с этим будет работать наш класс запросов. Если ничего не передадим, то будут по умолчанию суперглобальные массивы там.
### Работаем над ответом от сервера - **http response**. В случае, когда работаем через массивы данных
Как мы уже знаем, сервер принимает запрос от клиента и что-то возвращает ему в ответ. В ответе мы получаем ни что иное, как заголовки и какое-то "тело" (контент).
**Если бы мы работали с массивами** теми же, то у нас была бы примерно такая ситуация
```php
<?php
$request = [
'queryParams' => $_GET,
'parsedBody' => $_POST
];
$response = $application->handleRequest($request);
```
**Важное замечание**! Сервер вернул бы нам некий ответ примерно такого содержания
```php
<?php
$response = [
'headers' => [],
'body' => 'Hello, Guest'
];
```
**Важное замечание**! И мы бы могли эту информацию (**$response**) как то вывести
```php
<?php
foreach ($response['headers'] as $name => $value) {
header($name . ': ' . $value);
}
echo $response['body'];
```
**Важное замечание**! Вот с массивами мы работали бы таким образом. У нас бы был массив входных параметров - **$request** и через какой-нибудь класс того же контроллера или еще откуда-нибудь у нас бы возвращался массив выходных данных - **$response**
### Работаем над ответом от сервера - **http response**. В случае, когда работаем через парадигму ООП. Напишем класс ответа - **Response**, тестовый класс - **ResponseTest** и доработаем под новую логику нашу "точку входа" (**entry point**)
```php
<?php
namespace App\Framework\Http;
/**
* Class Response
* @package App\Framework\Http
*/
class Response
{
private $_headers = [];
private $_body = '';
private $_statusCode = 200;
private $_reasonPhrase;
public static $phrases = [
200 => 'OK',
404 => 'Not Found'
];
/**
* Response constructor.
* @param string $body
* @param int $statusCode
*/
public function __construct(string $body = '', int $statusCode = 200)
{
$this->_body = (string)$body ?: '';
$this->_statusCode = (int)$statusCode ?: 200;
$this->_reasonPhrase = (string)self::$phrases[$this->_statusCode];
}
/**
* @param string $body
* @return self
*/
public function withBody(string $body) : self
{
$newInstance = clone $this;
$newInstance->_body = (string)$body;
return $newInstance;
}
/**
* @return string
*/
public function getBody() : string
{
return $this->_body;
}
/**
* @return int
*/
public function getStatusCode() : int
{
return $this->_statusCode;
}
/**
* @param int $statusCode
* @return self
*/
public function withStatusCode(int $statusCode) : self
{
$newInstance = clone $this;
$newInstance->_statusCode = (int)$statusCode;
$newInstance->_reasonPhrase = (string)self::$phrases[$newInstance->_statusCode] ?: (string)self::$phrases[200];
return $newInstance;
}
/**
* @return string
*/
public function getReasonPhrase() : string
{
return $this->_reasonPhrase ?: (string)self::$phrases[200];
}
/**
* @return array
*/
public function getHeaders() : array
{
return $this->_headers;
}
/**
* @param $header
* @return null
*/
public function getHeader($header)
{
return $this->_headers[$header] ?: null;
}
/**
* @param $header
* @return bool
*/
public function hasHeader($header) : bool
{
return isset($this->_headers[$header]);
}
/**
* @param string $name
* @param string $value
* @return self
*/
public function withHeader(string $name, string $value) : self
{
$newInstance = clone $this;
if ($newInstance->_headers[(string)$name]) unset($newInstance->_headers[(string)$name]);
$newInstance->_headers[(string)$name] = (string)$value;
return $newInstance;
}
}
```
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\Response;
use PHPUnit\Framework\TestCase;
class ResponseTest extends TestCase
{
public function testEmpty() : void
{
$response = new Response($body = 'body content');
self::assertEquals($body, $response->getBody());
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('OK', $response->getReasonPhrase());
}
public function testNotFound() : void
{
$response = new Response($body = 'empty', $status = 404);
self::assertEquals($body, $response->getBody());
self::assertEquals($status, $response->getStatusCode());
self::assertEquals('Not Found', $response->getReasonPhrase());
}
public function testHeaders() : void
{
$response = (new Response())
->withHeader($nameDeveloper = 'X-Developer', $valueDeveloper = 'Denis Kitaev');
->withHeader($nameLanguage = 'X-Language', $valueLanguage = 'ru');
self::assertEquals([
$nameDeveloper => $valueDeveloper,
$nameLanguage => $valueLanguage
], $response->getHeaders());
}
}
```
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\RequestFactory;
use App\Framework\Http\Response;
## Initializing the application
require_once 'vendor/autoload.php';
$request = RequestFactory::fromGlobals();
## Running the application
$name = $request->getQueriesParams()['name'] ?: 'Guest';
$response = (new Response('Hello, ' . $name))
->withHeader('X-Developer', 'Denis Kitaev');
## Sending
header('HTTP/1.1 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
foreach ($response->getHeaders() as $name => $value) {
header($name . ': ' . $value);
}
echo $response->getBody();
```
**Важное замечание**! При таком подходе мы прошли весь **цикл взаимодействия "клиент-сервер"**. Мы взяли некие входные данные их как-то сформировали и обработали - **результат положили в $request**. И на основе этих входных данных сформировали ответ обратно клиенту - **результат положили и вывели клиенту в $response**
**Важное замечание**! Теперь мы можем отдать ответ в любом формате - будь то обычный ответ **headers+html**, или же **headers+json** и любой другой формат который захотим вернуть.
### Рефакторим наш код так, чтобы получить полноценный компонент (библиотеку)
Попробуем сделать так, чтобы этим проектом мы могли с кем-нибудь поделиться. Сделаем из него свой компонент или библиотеку.
**Что значит свой компонент или библиотека?**
Например, у нас есть некий компонент (класс) по определению языка клиента
```php
<?php
class LanguageDetector
{
public function getLanguage(App\Framework\Http\Request $request, string $default = 'en') : string
{
return ...;
}
}
```
Мы его создали и теперь хотим использовать в своем проекте
```php
<?php
$request = new App\Framework\Http\Request();
$detector = new LanguageDetector();
echo $detector->getLanguage($request);
```
**Важное замечание**! При таком подходе этот класс ни у кого, кроме нас не заработает. Почему? Потому что у других разработчиков класс запросов может быть другой. А наш компонент понимает только наш класс запросов.
Потому создадим некий **интерфейс** для этого дела
```php
<?php
class LanguageDetector
{
public function getLanguage(App\Framework\Http\ServerRequestInterface $request, string $default = 'en') : string
{
return ...;
}
}
```
**Важное замечание**! Почему именно **ServerRequestInterface** мы обозвали? Потому что запрос запросу рознь. И когда запрос приходит **от клиента** (например, **от браузера**), то помимо стандартных данных запроса в нем еще содержаться будут такие суперглобальные массивы, как **$_SERVER** & **$_SESSION**. А когда мы делаем **запрос посредством того же CURL**, то в таком запросе будут только **$_GET** & **$_POST**.
И чтобы не путать две разные концепции мы и можем обозвать наш интерфейс запросов таким образом.
И наш интерфейс запросов к серверу от клиента может выглядеть так
```php
interface ServerRequestInterface
{
/**
* To add a new $_GET query parameters
*
* @param array $queryParams
* @return static
*/
public function withQueryParams(array $queryParams = []) : ServerRequestInterface;
/**
* To add new $_POST query parameters
*
* @param array|null $data
* @return static
*/
public function withParsedBody($data = null) : ServerRequestInterface;
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array;
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody();
}
```
И теперь любой желающий может использовать наш компонент по определению языка клиента. Необходимо лишь **реализовать наш интерфейс**. Например, **написав какой-нибудь адаптер** под это дело
```php
<?php
$request = new OtherFramework\Request();
$detector = new LanguageDetector();
echo $detector->getLanguage(new RequestAdapter($request));
```
**Важное замечание**! Таким образом сторонний адаптер будет как бы реализовывать наш интерфейс (**ServerRequestInterface**). А сам класс запросов теперь можно "подсунуть" абсолютно любой с любого фреймворка или еще откуда-то.
### Подключение к нашему проекту стандартных интерфейсов по стандарту PSR-7 - компонент **"psr/http-message"**
**Важное замечание**! Таким образом, сообщество разработчиков в мире однажды сели и разработали общие интерфейсы запросов под всевозможные нужды. И заложили их в стандарт **PSR-7**
И теперь, чтобы подключить этот компонент с общими едиными интерфейсами запросов. Надо написать команду
```bash
$ composer require psr/http-message
```
**Важное замечание**! Прежде чем установить эту библиотеку **попробуем написать свои интерфейсы для наших классов запроса и ответа**
```php
<?php
namespace App\Framework\Http;
/**
* Interface ServerRequestInterface
* @package App\Framework\Http
*/
interface ServerRequestInterface
{
/**
* To add a new $_GET query parameters
*
* @param array $queryParams
* @return static
*/
public function withQueryParams(array $queryParams = []) : ServerRequestInterface;
/**
* To add new $_POST query parameters
*
* @param array|null $data
* @return static
*/
public function withParsedBody($data = null) : ServerRequestInterface;
/**
* Get array $_GET
*
* @return array
*/
public function getQueriesParams() : array;
/**
* Get request of the methods POST, PUT, PATCH & DELETE
*
* @return array|null
*/
public function getParsedBody();
}
```
```php
<?php
namespace App\Framework\Http;
/**
* Interface ResponseInterface
* @package App\Framework\Http
*/
interface ResponseInterface
{
/**
* @param string $body
* @return static
*/
public function withBody(string $body) : ResponseInterface;
/**
* @return string
*/
public function getBody() : string;
/**
* @return int
*/
public function getStatusCode() : int;
/**
* @param int $statusCode
* @return static
*/
public function withStatusCode(int $statusCode) : ResponseInterface;
/**
* @return string
*/
public function getReasonPhrase() : string;
/**
* @return array
*/
public function getHeaders() : array;
/**
* @param $header
* @return null
*/
public function getHeader($header);
/**
* @param $header
* @return bool
*/
public function hasHeader($header) : bool;
/**
* @param string $name
* @param string $value
* @return static
*/
public function withHeader(string $name, string $value) : ResponseInterface;
}
```
**Важное замечание**! И как мы увидим далее, написанные наши интерфейсы точная копия интерфейсов, содержащихся в компоненте - **psr/http-message**. Давайте в этом убедимся
Установим их командой
```bash
$ composer require psr/http-message
```
И вот, как они выглядят. **ServerRequestInterface** & **ResponseInterface**
```php
<?php
namespace Psr\Http\Message;
/**
* Representation of an incoming, server-side HTTP request.
*/
interface ServerRequestInterface extends RequestInterface
{
/**
* Retrieve server parameters.
*
* @return array
*/
public function getServerParams();
/**
* Retrieve cookies.
*
* @return array
*/
public function getCookieParams();
/**
* Return an instance with the specified cookies.
*
* @param array $cookies Array of key/value pairs representing cookies.
* @return static
*/
public function withCookieParams(array $cookies);
/**
* Retrieve query string arguments.
*
* @return array
*/
public function getQueryParams();
/**
* Return an instance with the specified query string arguments.
*
* @param array $query Array of query string arguments, typically from
* $_GET.
* @return static
*/
public function withQueryParams(array $query);
/**
* Retrieve normalized file upload data.
*
* @return array An array tree of UploadedFileInterface instances; an empty
* array MUST be returned if no data is present.
*/
public function getUploadedFiles();
/**
* Create a new instance with the specified uploaded files.
*
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
* @return static
* @throws \InvalidArgumentException if an invalid structure is provided.
*/
public function withUploadedFiles(array $uploadedFiles);
/**
* Retrieve any parameters provided in the request body.
*
* @return null|array|object The deserialized body parameters, if any.
* These will typically be an array or object.
*/
public function getParsedBody();
/**
* Return an instance with the specified body parameters.
*
* @param null|array|object $data The deserialized body data. This will
* typically be in an array or object.
* @return static
* @throws \InvalidArgumentException if an unsupported argument type is
* provided.
*/
public function withParsedBody($data);
/**
* Retrieve attributes derived from the request.
*
* @return array Attributes derived from the request.
*/
public function getAttributes();
/**
* Retrieve a single derived request attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $default Default value to return if the attribute does not exist.
* @return mixed
*/
public function getAttribute($name, $default = null);
/**
* Return an instance with the specified derived request attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $value The value of the attribute.
* @return static
*/
public function withAttribute($name, $value);
/**
* Return an instance that removes the specified derived request attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @return static
*/
public function withoutAttribute($name);
}
```
```php
<?php
namespace Psr\Http\Message;
/**
* Representation of an outgoing, server-side response.
*/
interface ResponseInterface extends MessageInterface
{
/**
* Gets the response status code.
*
* @return int Status code.
*/
public function getStatusCode();
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws \InvalidArgumentException For invalid status code arguments.
*/
public function withStatus($code, $reasonPhrase = '');
/**
* Gets the response reason phrase associated with the status code.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @return string Reason phrase; must return an empty string if none present.
*/
public function getReasonPhrase();
}
```
**Важное замечание**! Теперь нам требуется немного переписать наш код наших классов.
```php
<?php
namespace App\Framework\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Class Response
* @package App\Framework\Http
*/
class Response implements ResponseInterface
{
private $_headers = [];
private $_body;
private $_statusCode = 200;
private $_reasonPhrase;
public static $phrases = [
200 => 'OK',
404 => 'Not Found'
];
/**
* Response constructor.
* @param StreamInterface|string $body
* @param int $statusCode
*/
public function __construct($body = '', int $statusCode = 200)
{
$this->_body = $body instanceof StreamInterface ?: new Stream((string)$body);
$this->_statusCode = (int)$statusCode ?: 200;
$this->_reasonPhrase = (string)self::$phrases[$this->_statusCode];
}
/**
* @param StreamInterface $body
* @return self
*/
public function withBody(StreamInterface $body) : ResponseInterface
{
$newInstance = clone $this;
$newInstance->_body = $body;
return $newInstance;
}
/**
* @return StreamInterface
*/
public function getBody() : StreamInterface
{
return $this->_body;
}
/**
* @return int
*/
public function getStatusCode() : int
{
return $this->_statusCode;
}
/**
* @param int $statusCode
* @return self
*/
public function withStatusCode(int $statusCode) : ResponseInterface
{
$newInstance = clone $this;
$newInstance->_statusCode = (int)$statusCode;
$newInstance->_reasonPhrase = (string)self::$phrases[$newInstance->_statusCode] ?: (string)self::$phrases[200];
return $newInstance;
}
/**
* @return string
*/
public function getReasonPhrase() : string
{
return $this->_reasonPhrase ?: (string)self::$phrases[200];
}
/**
* @return array
*/
public function getHeaders() : array
{
return $this->_headers;
}
/**
* @param $header
* @return null
*/
public function getHeader($header)
{
return $this->_headers[$header] ?: null;
}
/**
* @param $header
* @return bool
*/
public function hasHeader($header) : bool
{
return isset($this->_headers[$header]);
}
/**
* @param string $name
* @param string|string[] $value
* @return self
*/
public function withHeader($name, $value) : ResponseInterface
{
$newInstance = clone $this;
if ($newInstance->_headers[(string)$name]) unset($newInstance->_headers[(string)$name]);
$newInstance->_headers[(string)$name] = (string)$value;
return $newInstance;
}
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion()
{
// TODO: Implement getProtocolVersion() method.
}
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion($version)
{
// TODO: Implement withProtocolVersion() method.
}
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine($name)
{
// TODO: Implement getHeaderLine() method.
}
/**
* Return an instance with the specified header appended with the given value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
*/
public function withAddedHeader($name, $value)
{
$newInstance = clone $this;
$newInstance->_headers[(string)$name] = array_merge($newInstance->_headers[$name], (array)$value);
return $newInstance;
}
/**
* Return an instance without the specified header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader($name)
{
$newInstance = clone $this;
if ($newInstance->hasHeader((string)$name)) unset($newInstance->_headers[(string)$name]);
return $newInstance;
}
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws \InvalidArgumentException For invalid status code arguments.
*/
public function withStatus($code, $reasonPhrase = '')
{
// TODO: Implement withStatus() method.
}
}
```
**Важное замечание**! Теперь у метода **withBody** класса **Response** передается не просто строка, а объект класса, реализующий интерфейс **StreamInterface**. Который помимо своего основного метода **getContents()** содержит кучу еще других вспомогательных методов. Которые позволяют перелистывать (как аудиокассету) туда-сюда контент инстанса.
**Для чего это может быть нужно?**
Ну, например, если у нас есть файл в несколько сотен мегабайт и будет странно его сразу считать целиком с сервера и вернуть клиенту. Потому он считывается последовательно порциями. В этом и есть суть интерфейса **StreamInterface** и класса, который его реализует.
**Важное замечание**! Потому работа с потоками (streams) намного более универсальнее, чем простая работа со строками. Потоки могут быть любыми. Мы можем реализовать интерфейс **StreamInterface** в разных классах - это могут быть классы по работе с обычными строками, с файлами и чем-нибудь другим.
И мы реализуем этот интерфейс для примера, написав такой класс - **Stream**
```php
<?php
namespace App\Framework\Http;
use Psr\Http\Message\StreamInterface;
/**
* Class Stream
* @package App\Framework\Http
*/
class Stream implements StreamInterface
{
private $_content = '';
/**
* Stream constructor.
*
* @param string $content
*/
public function __construct(string $content)
{
$this->_content = (string)$content;
}
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString() : string
{
return $this->getContents();
}
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close()
{
// TODO: Implement close() method.
}
/**
* Separates any underlying resources from the stream.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach()
{
// TODO: Implement detach() method.
}
/**
* Get the size of the stream if known.
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize()
{
return mb_strlen($this->_content);
}
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
public function tell()
{
// TODO: Implement tell() method.
}
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
// TODO: Implement eof() method.
}
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
// TODO: Implement isSeekable() method.
}
/**
* Seek to a position in the stream.
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure.
*/
public function seek($offset, $whence = SEEK_SET)
{
// TODO: Implement seek() method.
}
/**
* Seek to the beginning of the stream.
*
* @throws \RuntimeException on failure.
* @link http://www.php.net/manual/en/function.fseek.php
* @see seek()
*/
public function rewind()
{
// TODO: Implement rewind() method.
}
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
// TODO: Implement isWritable() method.
}
/**
* Write data to the stream.
*
* @param string $string The string that is to be written.
*/
public function write($string) : void
{
$this->_content .= (string)$string;
}
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
// TODO: Implement isReadable() method.
}
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws \RuntimeException if an error occurs.
*/
public function read($length)
{
// TODO: Implement read() method.
}
/**
* Returns the remaining contents in a string
*
* @return string
*/
public function getContents() : string
{
return $this->_content;
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
// TODO: Implement getMetadata() method.
}
}
```
**Важное замечание**! Здесь мы в конструкторе будем получать некую строку (наш контент), можем получить длину всей строки - **getSize()**, получить целиком сам контент (все содержимое) - **getContents()** и можем что-то в случае чего дописать в наш поток - **write()**
И соответственно в наших тестах класса **ResponseTest** тоже кое-что изменится
```php
<?php
namespace Tests\Framework\Http;
use App\Framework\Http\Response;
use App\Framework\Http\Stream;
use PHPUnit\Framework\TestCase;
class ResponseTest extends TestCase
{
public function testEmpty() : void
{
$response = new Response($body = 'body content');
self::assertEquals($body, $response->getBody()->getContents());
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('OK', $response->getReasonPhrase());
}
public function testNotFound() : void
{
$response = (new Response())
->withBody(new Stream($body = 'empty'))
->withStatusCode($status = 404);
self::assertEquals($body, $response->getBody()->getContents());
self::assertEquals($status, $response->getStatusCode());
self::assertEquals(Response::$phrases[$status], $response->getReasonPhrase());
}
public function testHeaders() : void
{
$response = (new Response())
->withHeader($nameDeveloper = 'X-Developer', $valueDeveloper = 'Denis Kitaev')
->withHeader($nameLanguage = 'X-Language', $valueLanguage = 'ru');
self::assertTrue($response->hasHeader($nameDeveloper));
self::assertEquals([
$nameDeveloper => $response->getHeader($nameDeveloper),
$nameLanguage => $valueLanguage
], $response->getHeaders());
}
}
```
**Важное замечание**! Мы будем теперь не просто получать через метод **getBody()** наше содержимое, а использовать внутренний метод нашего потокового класса **Stream::getContents()** или можем использовать магический метод языка - **Stream::__toString()**. Т.е. сделав так - **(string)$response->getBody()**, что в свою очередь также вызовет как раз-таки этот магический метод.
**Важное замечание**! Вроде все хорошо, но чтобы полноценно реализовать в нашем проекте у классов запроса и ответа стандарт PSR-7 необходимо дописать реализацию еще многих методов.
### Внедрим в наш проект библиотеку **"zendframework/zend-diactoros"**, которая включает в себя полноценную реализацию классов запроса и ответа по стандарту PSR-7
Но мы этого делать не станем. Почему? Потому что уже умные люди все это реализовали за нас и для нас.
Мы говорим о пакете **zendframework/zend-diactoros**
```bash
$ composer require zendframework/zend-diactoros
```
**Важное замечание**! И эта библиотека добавит в наш проект реализацию интерфейсов **ServerRequestInterface** & **ResponseInterface**. Т.е. у нас будут свои полноценные классы в проекте запроса и ответа по стандарту PSR-7.
**Важное замечание**! Мы уже будем вместо своего класса-фабрики запроса использовать класс **ServerRequestFactory**, а вместо своего класса ответа использовать класс **HtmlResponse**
```php
<?php
session_start();
chdir(dirname(__DIR__));
use Zend\Diactoros\ServerRequestFactory as RequestFactory;
use Zend\Diactoros\Response\HtmlResponse as Response;
## Initializing the application
require_once 'vendor/autoload.php';
$request = RequestFactory::fromGlobals();
## Running the application
$name = $request->getQueryParams()['name'] ?: 'Guest';
$response = (new Response('Hello, ' . $name))
->withHeader('X-Developer', 'Denis Kitaev');
## Sending
header('HTTP/1.1 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
foreach ($response->getHeaders() as $name => $values) {
header($name . ': ' . implode(', ', $values));
}
echo $response->getBody();
```
**Важное замечание**! Наша точка входа теперь полностью переписана для работы с классами библиотеки **zendframework/zend-diactoros**
И также отрефакторим наши тестовые классы
```php
<?php
namespace Tests\Framework\Http;
use Zend\Diactoros\ServerRequestFactory as RequestFactory;
use PHPUnit\Framework\TestCase;
/**
* Class RequestTest
* @package Tests\Framework\Http
*/
class RequestTest extends TestCase
{
/**
* Void testing
*/
public function testEmpty(): void
{
$request = RequestFactory::fromGlobals();
self::assertEquals([], $request->getQueryParams());
self::assertEquals([], $request->getParsedBody());
}
/**
* Testing to get request GET-parameters
*/
public function testQueryParams(): void
{
$request = (RequestFactory::fromGlobals())
->withQueryParams($data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals($data, $request->getQueryParams());
self::assertEquals([], $request->getParsedBody());
}
/**
* Testing to obtain POST-parameters of the request
*/
public function testParsedBody(): void
{
$request = (RequestFactory::fromGlobals())
->withParsedBody($data = [
'name' => 'Denis',
'age' => 35
]);
self::assertEquals([], $request->getQueryParams());
self::assertEquals($data, $request->getParsedBody());
}
}
```
```php
<?php
namespace Tests\Framework\Http;
use Zend\Diactoros\Response\HtmlResponse as Response;
use PHPUnit\Framework\TestCase;
class ResponseTest extends TestCase
{
public function testEmpty() : void
{
$response = new Response($body = 'body content');
self::assertEquals($body, $response->getBody()->getContents());
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('OK', $response->getReasonPhrase());
}
public function testNotFound() : void
{
$response = new Response($body = 'empty', $status = 404);
self::assertEquals($body, $response->getBody()->getContents());
self::assertEquals($status, $response->getStatusCode());
self::assertEquals('Not Found', $response->getReasonPhrase());
}
public function testHeaders() : void
{
$response = (new Response(''))
->withHeader($nameDeveloper = 'X-Developer', $valueDeveloper = 'Denis Kitaev')
->withHeader($nameLanguage = 'X-Language', $valueLanguage = 'ru');
self::assertTrue($response->hasHeader($nameDeveloper));
}
}
```
**Важное замечание**! Мы использовали класс ответа **HtmlResponse**, но также в этом компоненте от компании Zend есть еще много реализаций ответов в различном формате - это и json-ответ (**JsonResponse**) и многие другие.
### Чистка проекта от наших реализаций классов запроса и ответа и тестов под них
**Важное замечание**! Поскольку наш проект сейчас полностью взаимодействует с классами **библиотеки ZendDiactoros**, то вообще нет необходимости хранить наши классы запроса и ответа их интерфейсы и тестовые классы под все это дело.
### "Чистим" в нашей "точке входа" низкоуровневую реализацию логики ответа
```php
<?php
...
## Sending
header('HTTP/1.1 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
foreach ($response->getHeaders() as $name => $values) {
header($name . ': ' . implode(', ', $values));
}
echo $response->getBody();
```
```php
<?php
## Sending
header(sprintf(
'HTTP/%s %d %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
));
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
echo $response->getBody();
```
**Важное замечание**! Попробуем инкапсулировать весь этот код (логику) в нашем отдельном классе **ResponseSender**
```php
<?php
namespace App\Framework\Http;
use Psr\Http\Message\ResponseInterface;
/**
* Class ResponseSender
* @package App\Framework\Http
*/
class ResponseSender
{
/**
* @param ResponseInterface $response
*/
public function send(ResponseInterface $response) : void
{
header(sprintf(
'HTTP/%s %d %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
));
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
echo $response->getBody()->getContents();
}
}
```
**Важное замечание**! Теперь наш передатчик (**emitter**) класс **ResponseSender** может принимать на вход любой PSR-интерфейс и в зависимости от переданного инстанса выдавать в том или ином формате ответ клиенту.
И теперь наша "точка входа" (**entry point**) выглядит так
```php
<?php
session_start();
chdir(dirname(__DIR__));
use App\Framework\Http\ResponseSender;
use Zend\Diactoros\ServerRequestFactory as RequestFactory;
use Zend\Diactoros\Response\HtmlResponse as Response;
## Initializing the application
require_once 'vendor/autoload.php';
$request = RequestFactory::fromGlobals();
## Running the application
$name = $request->getQueryParams()['name'] ?: 'Guest';
$response = (new Response('Hello, ' . $name))
->withHeader('X-Developer', 'Denis Kitaev');
## Sending
$emitter = new ResponseSender();
$emitter->send($response);
```
**Важное замечание**! Но когда мы установили библиотеку **ZendDiactoros**, то в ней уже есть удобный класс-передатчик. Потому мы можем использовать именно его. К сожалению с недавнего времени реализацию "передатчика" вынесли в отдельную библиотеку - **zendframework/zend-httphandlerrunner**.
### Установка библиотеки для использования "передатчика" (**emitter**) ответов - **"zendframework/zend-httphandlerrunner"**
Установим ее следующей командой
```bash
$ composer require zendframework/zend-httphandlerrunner
```
И используем ее в нашем проекте в **"entry point"** нашей
```php
<?php
session_start();
chdir(dirname(__DIR__));
use Zend\Diactoros\ServerRequestFactory as RequestFactory;
use Zend\Diactoros\Response\HtmlResponse as Response;
use Zend\HttpHandlerRunner\Emitter\SapiEmitter;
## Initializing the application
require_once 'vendor/autoload.php';
$request = RequestFactory::fromGlobals();
## Running the application
$name = $request->getQueryParams()['name'] ?: 'Guest';
$response = (new Response('Hello, ' . $name))
->withHeader('X-Developer', 'Denis Kitaev');
## Sending
$emitter = new SapiEmitter();
$emitter->emit($response);
```
**Важное замечание**! Помимо класса **SapiEmitter** есть более так сказать продвинутая его версия - **SapiStreamEmitter**. Она помимо всего прочего может работать с большими объемами контента (крупными файлами и т.д.).
**Важное замечание**! Например, если у нас большой файл по умолчанию, используя класс **SapiStreamEmitter**, мы можем постепенно считывать из него и выводить каждые 8 килобайт данных. И мы решим проблему с выводом результата больших объемных данных.
**Важное замечание**! Также в классе **SapiStreamEmitter** реализована возможность посредством специального заголовка - **Content-Range** производить докачку файлов больших. После обрыва связи (соединения) или еще из-за чего-либо.