---
# System prepended metadata

title: Untitled

---


## 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** производить докачку файлов больших. После обрыва связи (соединения) или еще из-за чего-либо.
