Типобезопасная работа с массивами PHP, часть 3
Всем привет! Расскажу о большом обновлении в пакете sbwerewolf/language-specific
.
Для тех кто не знаком с этим пакетом коротко опишу его назначение.
Допустим вы по API получили JSON с большой вложенностью, и вам нужно достать из JSON какое то значение которое зарыто поглубже. Что делать ? конечно преобразовать JSON строку в массив, а дальше что делать ? Конечно прописать все индексы до искомого элемента, получиться что то такое:
$formatted = (string)$response["response"]["GeoObjectCollection"]["featureMember"]
[0]["GeoObject"]["metaDataProperty"]["GeocoderMetaData"]["Address"]["formatted"]
?? "Адрес не найден";
Получилась строка кода длиной 200 символов. Не очень удобное такое читать. Самое неприятное, что элемента с индексом 0
может и не быть, то есть сначала надо проверить, что он есть, а потом уже читать дальше, и каждый раз придётся приписывать весь это хвост из индексов, поэтому код будет заграмождён индексами, работать с таким кодом не удобно.
Пакет sbwerewolf/language-specific
, позволяет избавить код от сплошного перечисления индексов, замороченных выражений, скобочек и операторов ??
.
С ним можно будет написать так:
$data = (new AdvancedArrayFactory())->makeAdvancedArray($response);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted")
->default("Адрес не найден")
->str();
// 1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates
И ни каких забот о том что по пути какого то элемента может не быть, или элемент "formatted"
может внезапно оказаться с типом bool, вместо string.
Назначение пакета sbwerewolf/language-specific
Суть пакета в том чтобы безопасно, без ошибок пройти по массиву до нужного элемента, и получить значение этого элемента с нужным типом, без бойлерплейта.
Что значит безопасно?
Вместо:
$value = $response["GeocoderMetaData"]["Address"]["formatted"] ?? 'Адрес не найден';
Пишем:
$data = (new AdvancedArrayFactory())->makeAdvancedArray($response);
$value = $data->pull("GeocoderMetaData")->pull("Address")->get("formatted");
$value->default("Адрес не найден");
Чем это лучше? Это лучше тем, что стена текста из перечисления индексов, разбиваеться на отдельные индексы, которые визуально чётко разделены, такой код легче читать.
Что значит с нужным типом, без бойлерплейта?
Это значит что нам не надо писать код для преобразования типа на случай если тип элемента отличаеться от ожидаемого.
Вместо:
$formatted = (string)$value;
Пишем:
$formatted = $value->str();
В целом разница будет такой, было две строчки кода:
$formatted = (string)$response["GeocoderMetaData"]["Address"]["formatted"]
?? 'Адрес не найден';
стало семь строчек:
$data = (new AdvancedArrayFactory())->makeAdvancedArray($response);
$formatted = $data
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted")
->default("Адрес не найден")
->str();
Код получился более человекочитаемый, менее машиночитаемый (теперь IDE не понимает, что мы идём по индексам массива и теперь IDE не пожет предсказать структуру массива).
Строчек кода стало больше, понятность выше. Вместо компактности получили понятность. По моему стало лучше.
Многословный, но понятный код, намного лучше компактного кода, который работает не пойми как, пока его не запустишь, да можно ограничиться запуском у себя в голове, но зачем лишний раз напрягать мозг ? Это отвлекает внимание от основной задачи, не надо так делать, берегите своё «мыслетопливо»!
Предыстория
Я несколько лет назад опубликовал на Packagist composer-пакет sbwerewolf/language-specific
, и написал на Хабре два статьи об этом пакете (статья1, статья2).
С августа 2023 года пакет начали активно скачивать, в среднем за месяц 500 установок, на январь 2025 года уже 8 000 установок.
Альтернатива
В декабре 2024 на Хабре была опубликована статья «PHP Typed» про библиотеку которая помогает получить значения глубоко вложенных элементов PHP-массивов или глубоко вложенных свойств PHP-классов (stdClass). Это ровно то что делает мой пакет sbwerewolf/language-specific
.
Конечно мне было интересно сравнить эти два решения. Я написал бенчмарк, вывод: Typed
на простых случаях в два раза быстрей, в сложных, наверное будет больше, не проверял.
Я заглянул под капот в Typed
, посмотрел что у меня написано в ArrayHandler
.
Разница между Typed
и ArrayHandler
в том, что Typed
только выдаёт значение элемента в занном типе. ArrayHandler
не только выдаёт значение в заданном типе, но и позволяет принять решение в ситуации когда значения нет, или поверить фактический тип элемента, и в зависимости от этого провести дальнейшую обработку. Например если элемент это bool
со значением false
, то ни чего не делаем, а если элемент это array
, то проводим дальнейшую обработку всех элементов.
Typed
это расширение базового набора PHP для работы с массивами.
ArrayHandler
это работа с массивом в ООП стиле, ArrayHandler
объединяет в себе данные и методы для работы с ними.
Если вы уверены в структуре массива с которым работает — используйте Typed
.
Если структура массива имеет большую вариантивность и в зависмости от варианта должна быть обработана по разному — используйте ArrayHandler
.
Доработки
Когда я сравнивал Typed
и ArrayHandler
, я увидел что у ArrayHandler
покрытие тестами хромает: покрытие кода упало до 76%, потому что после добавления поддержки интерфейсов ArrayAccess, Iterator, JsonSerializable, код методов добавился, а код тестов — нет.
Я решил это дело исправить, хотел добавить пару простых тестов, понял что ArrayHandler
перегружен функционалом, поделил его на несколько классов, переименовал методы и пошло поехало. Добавил интерфейсы, добавил фабрики. Добавил пакеты для статического анализа, короче в Новый Год развлекался на полную.
Как установить
composer require sbwerewolf/language-specific
Как пользоваться
Собственно основной use-case был показан выше. Допустим нам из API пришёл большой JSON, и мы хотим из него часть информации записать себе в БД, для этого надо эту часть информации извлечь из JSON, конечно с обработкой вариантов когда JSON не содержит нужной нам информации.
Запросим в Яндекс Геокодер адрес для «Mohammed Bin Rashid Boulevard 1»:
$response = file_get_contents('https://geocode-maps.yandex.ru/1.x/?apikey=d3893dc1-c136-4084-ab9c-4db26b00463e&geocode=Mohammed+Bin+Rashid+Boulevard+1&lang=en_US&format=json');
var_dump( $response);
/* string(3197) "{"response":{"GeoObjectCollection":{"metaDataProperty":{"GeocoderResponseMetaData":{"request":"Mohammed Bin Rashid Boulevard 1","results":"10","found":"2"}},"featureMember":[{"GeoObject":{"metaDataProperty":{"GeocoderMetaData":{"precision":"exact","text":"1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","kind":"house","Address":{"country_code":"AE","formatted":"1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","Components":[{"kind":"country","name":"United Arab Emirates"},{"kind":"province","name":"Dubai"},{"kind":"area","name":"Sector 3"},{"kind":"district","name":"Downtown Dubai"},{"kind":"district","name":"Downtown Dubai"},{"kind":"street","name":"Mohammed Bin Rashid Boulevard"},{"kind":"house","name":"1"}]},"AddressDetails":{"Country":{"AddressLine":"1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","CountryNameCode":"AE","CountryName":"United Arab Emirates","AdministrativeArea":{"AdministrativeAreaName":"Dubai","SubAdministrativeArea":{"SubAdministrativeAreaName":"Sector 3","Locality":{"DependentLocality":{"DependentLocalityName":"Downtown Dubai","DependentLocality":{"DependentLocalityName":"Downtown Dubai","Thoroughfare":{"ThoroughfareName":"Mohammed Bin Rashid Boulevard","Premise":{"PremiseNumber":"1"}}}}}}}}}}},"name":"1, Mohammed Bin Rashid Boulevard","description":"Downtown Dubai, Dubai, United Arab Emirates","boundedBy":{"Envelope":{"lowerCorner":"55.270141 25.193445","upperCorner":"55.278352 25.200915"}},"uri":"ymapsbm1://geo?data=CgoyMTE3NTQxODgxEocB2KfZhNil2YXYp9ix2KfYqiDYp9mE2LnYsdio2YrYqSDYp9mE2YXYqtit2K_YqSwg2KXZhdin2LHYqSDYr9io2YosINmI2LPYtyDZhdiv2YrZhtipINiv2KjZiiwg2KjZiNmE2YrZgdin2LHYryDZhdit2YXYryDYqNmGINix2KfYtNivLCAxIgoN1BhdQhXVk8lB","Point":{"pos":"55.274247 25.19718"}}},{"GeoObject":{"metaDataProperty":{"GeocoderMetaData":{"precision":"street","text":"Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","kind":"street","Address":{"country_code":"AE","formatted":"Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","Components":[{"kind":"country","name":"United Arab Emirates"},{"kind":"province","name":"Dubai"},{"kind":"area","name":"Sector 3"},{"kind":"district","name":"Downtown Dubai"},{"kind":"street","name":"Mohammed Bin Rashid Boulevard"}]},"AddressDetails":{"Country":{"AddressLine":"Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates","CountryNameCode":"AE","CountryName":"United Arab Emirates","AdministrativeArea":{"AdministrativeAreaName":"Dubai","SubAdministrativeArea":{"SubAdministrativeAreaName":"Sector 3","Locality":{"DependentLocality":{"DependentLocalityName":"Downtown Dubai","Thoroughfare":{"ThoroughfareName":"Mohammed Bin Rashid Boulevard"}}}}}}}}},"name":"Mohammed Bin Rashid Boulevard","description":"Downtown Dubai, Dubai, United Arab Emirates","boundedBy":{"Envelope":{"lowerCorner":"55.277606 25.195668","upperCorner":"55.279807 25.199027"}},"uri":"ymapsbm1://geo?data=Cgo1ODA2NzkyODQ1EkpVbml0ZWQgQXJhYiBFbWlyYXRlcywgRHViYWksIERvd250b3duIER1YmFpLCBNb2hhbW1lZCBCaW4gUmFzaGlkIEJvdWxldmFyZCIKDXQdXUIVGZTJQQ,,","Point":{"pos":"55.278765 25.197311"}}}]}}}" */
$object = json_decode(json: $response, associative: true, flags: JSON_THROW_ON_ERROR);
$data = new AdvancedArrayFactory()->makeAdvancedArray($object);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted")
->default("Адрес не найден");
$val = $formatted->str();
var_dump( $val);
/*
string(77)
"1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates"
*/
$isReal = $formatted->isReal();
var_dump( $isReal);
/* bool(true) */
Распарсим ответ:
преобразуем строковое представление JSON в массив
из массива с помощью фабрики делаем экземпляр
AdvancedArray
AdvancedArray::pull()
— получаем экземплярAdvancedArray
для вложенного массива, делаем так пока не дойдём до нужного элементаAdvancedArray::get()
— получаем экземплярCommonValue
для элементаformatted
(не массив)CommonValue::str()
— получаем значение элемента:»1, Mohammed Bin Rashid Boulevard, Downtown Dubai, Dubai, United Arab Emirates»CommonValue::isReal()
— поверяем что значение действительное, а не подставлено по умолчанию: true — значение действительное
Запросим в Яндекс Геокодер адрес для «Адский остров» (данные отсутствуют):
$response = file_get_contents('https://geocode-maps.yandex.ru/1.x/?apikey=d3893dc1-c136-4084-ab9c-4db26b00463e&geocode=%D0%B0%D0%B4%D1%81%D0%BA%D0%B8%D0%B9+%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B2&lang=en_US&format=json');
var_dump( $response);
/*
string(172)
"{"response":{"GeoObjectCollection":{"metaDataProperty":{"GeocoderResponseMetaData":
{"request":"адский остров","results":"10","found":"0"}},"featureMember":[]}}}"
*/
$object = json_decode(json: $response, associative: true, flags: JSON_THROW_ON_ERROR);
$data = (new AdvancedArrayFactory())->makeAdvancedArray($object);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted")
->default("Адрес не найден");
$value = $formatted->str();
var_dump( $value);
/* string(28) "Адрес не найден" */
$isReal = $formatted->isReal();
var_dump( $isReal);
/* bool(false) */
Распарсим ответ:
преобразуем строковое представление JSON в массив
из массива с помощью фабрики делаем экземпляр
AdvancedArray
AdvancedArray::pull()
— получаем экземплярAdvancedArray
для вложенного массива, делаем так пока не дойдём до нужного элементаAdvancedArray::get()
— получаем экземплярCommonValue
для элементаformatted
(не массив)CommonValue::str()
— получаем значение элемента: «Адрес не найден»CommonValue::isReal()
— поверяем что значение действительное, а не подставное: false — значение не действительное, подставлено значение по умолчанию.
Как видите код абсолютно линеен, нет ни каких ветвлений и хитростей: получили значения, взяли нужное, передали дальше в обработку куда нам надо было, за соответствие типов не переживаем.
Разобраться в том как происходит парсинг легко — это важно для понимания и поддержки кода.
Алгоритм очень простой:
Последовательным вызовом метода
AdvancedArray::pull($index)
получить нужный массивМетодом
AdvancedArray::get($index)
получить значение элементаС помощью соответствующего метода приведения типа, получить значение с нужным типом
Дополнительные возможности
Дополнительные возможности пакета sbwerewolf/language-specific
это возможность проверки наличия элемента по индексу и встроенная возможность проверить тип элемента, так же можно получить значение элемента без преобразования типа, то есть получить значение как есть.
Кроме того можно получить все «элементы-значения» (не массивы), и соответствено, можно получить все элементы-массивы.
Проверить наличие элемента (существование)
$data = (new AdvancedArrayFactory())->makeAdvancedArray($array);
$element = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
$hasElement = $element->has("formatted"); // bool
Можно проще:
$data = (new AdvancedArrayFactory())->makeAdvancedArray($array);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted");
$isReal = $formatted->isReal(); // bool
Получить тип
$data = (new AdvancedArrayFactory())->makeAdvancedArray($array);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted");
$type = $formatted->type();
// NULL если элемента нет и если элемент есть, то "1, Mohammed Bin Rashid ..."
В общем случае будет выдан тип элемента, в случае с ГеоКодером это string.
Если элемент отсутствует, то тип будет NULL.
Если задано значение по умолчанию и элемент отсутствует, то будет выдан тип значения по умолчанию.
Если задано значение по умолчанию и элемент присутствует, то будет выдан тип значения элемента и не факт что он будет совпадать с типом значения по умолчанию.
Получить значение как есть, без приведения типа
$data = (new AdvancedArrayFactory())->makeAdvancedArray($array);
$formatted = $data
->pull("response")
->pull("GeoObjectCollection")
->pull("featureMember")
->pull()
->pull("GeoObject")
->pull("metaDataProperty")
->pull("GeocoderMetaData")
->pull("Address")
->get("formatted")
;
$value = $formatted->asIs();
// NULL если элемента нет, если элемент есть, то "1, Mohammed Bin Rashid ..."
Значение элемента будте выдано как есть, если элемента нет, то будет выдано значение null
, если задано значение по умолчанию, то будет выдано значение по умолчанию.
Получить все элементы-значения
$data = [1, ['first', 'next',],
2, ['next','last']];
$fabric = new AdvancedArrayFactory();
$handler = (new AdvancedArrayFactory())->makeAdvancedArray($data);
$index = 0;
$item = $fabric::makeDummyAdvancedArray();
foreach ($handler->values() as $item) {
var_dump( $item->asIs());
}
/*
int(1)
int(2)
*/
Получить все элементы-массивы
$data = [1, ['first', 'next',],
2, ['next','last']];
$fabric = new AdvancedArrayFactory();
$handler = (new AdvancedArrayFactory())->makeAdvancedArray($data);
$index = 0;
$item = $fabric::makeDummyAdvancedArray();
foreach ($handler->arrays() as $item) {
var_dump( $item->raw());
}
/*
array(2) {
[0]=>
string(5) "first"
[1]=>
string(4) "next"
}
array(2) {
[0]=>
string(4) "next"
[1]=>
string(4) "last"
}
*/
Состав пакета sbwerewolf/language-specific
Пакет состоит из 4 классов:
CommonValue
предоставляет значение элемента, тип значения будет приведён к заданномуBaseArray
обёртка над массивом, реализация интерфейсовIterator
иJsonSerializable
CommonArray
класс наследник отBaseArray
, реализация интерфейсаArrayAccess
AdvancedArray
класс наследник отCommonArray
, работа с вложенными массивамиТри фабрики для создания экземпляров классов надлежащим образом, это:
CommonValueFactory
,ArrayFactory
,AdvancedArrayFactory
один вспомогательный класс и один класс DTO, интерфейсы на всё (больше ради «православного» тестирования)
CommonValue
Этот класс является сутью пакета, именно этот класс обеспечивает предоставление значения с заданным типом, класс гарантирует, что:
значение будет приведено к заданному типу,
при необходимости будет подставлено значение по умолчанию,
и маленький бонус, всегда можно понять, что предоставленное значение являться или подставным, или действительным, то есть появляться возможность завязать на это какую то логику без дополнительных телодвижений
Методы класса CommonValue
CommonValue::default()
задать значение по умолчаниюCommonValue::isReal()
проверяет, что значение является действительным (исследовательский метод)CommonValue::type()
получить действительный тип значения (исследовательский метод)CommonValue::asIs()
предоставить значение как есть, без приведения типаCommonValue::double()
предоставить значение приведённое к типуfloat
, методы называется double, потому что вызовgettype((float)null)
вернётdouble
CommonValue::str()
вернёт значение приведённое к типуstring
CommonValue::int()
вернёт значение приведённое к типуint
CommonValue::bool()
вернёт значение приведённое к типуbool
CommonValue::array()
вернёт значение приведённое кarray
CommonValue::object()
вернёт значение приведённое кobject
BaseArray
BaseArray
возвращает элементы массива только как экземпляры CommonValueInterface
, то есть перебрать вложенные массивы не получиться.
Класс является реализаций интерфейсов Iterator
и JsonSerializable
.
Можно применить json_encode()
, массив будет конвертирован в JSON-строку.
Можно прогонять экземпляр через forech
, можно вручную перебрать элементы массива (с помощью методов next()
key()
current()
rewind()
).
Пример с foreach
:
$data = (new AdvancedArrayFactory())->makeAdvancedArray([2,3,4,[5,'q'],'w',['e','r']]);
foreach ($data as $element){ var_dump($element->asIs()); }
/*
int(2)
int(3)
int(4)
array(2) {
[0]=>
int(5)
[1]=>
string(1) "q"
}
string(1) "w"
array(2) {
[0]=>
string(1) "e"
[1]=>
string(1) "r"
}
*/
До версии 8.4 при переборе элементов массива с помощью
foreach
возвращалось собственно значение, в версии 8.4 поведение было изменено, теперь значение возвращаеться как экземпляр класса с интерфейсомCommonValueInterface
, для получения значения без приведения типа служит методCommonValueInterface::asIs()
С помощью метода BaseArray::raw():array
можно вернуть исходный массив целиком.
В целом ни чего интересного, класс сделан что бы в основном классе AdvancedArray
не было «неинтересного кода».
Для создания экземпляров класса BaseArray
можно использовать фабрику ArrayFactory
.
CommonArray
Ещё один неинтересный класс, расширяет функционал класса BaseArray
методами интерфейса ArrayAccess
(работа с элементами массив через квадрантные скобочки) при этом вызов методов для изменения или удаления элементов массива приведёт к вызову исключений ListIsImmutableException
и ValueIsImmutableException
, значения элементов менять запрещено, удалять элементы запрещено.
Класс добавляет два метода аналогичных методам интерфейса ArrayAccess
:
CommonArray::has
(string|int|float|bool|null $key = null): bool проверить существование элемента по индексу, если вызвать без аргументов, то вернётtrue
если в массиве есть хотя бы один элемент, метод являтьеся аналогомArrayAccess::offsetExists()
CommonArray::get
(string|int|float|bool|null $key = null) получить элемент по индексу (элемент будет предоставлен как экземплярCommonValue
), если индекс не задавать, то будет возвращён случайный элемент (если в массиве есть хотя бы один элемент), если в массиве нет элементов, то метод вернётCommonValue
с недействительным значением (CommonValue::isReal()
=>false
), метод являтьеся аналогомArrayAccess::offsetGet()
Для создания экземпляров класса CommonArray
можно использовать фабрику ArrayFactory
.
AdvancedArray
Класс реализует доступ к элементам вложенных массивов, является наследником от CommonArray
.
Методы класса AdvancedArray
AdvancedArray::isDummy()
проверяет, что массив является подставным (заглушка для не существующих массивов) (исследовательский метод)AdvancedArray::pull
(int|bool|string|null|float $key = null) вытягивает вложенный массив, то есть возвращает новый экземплярAdvancedArrayInterface
для вложенного массива, если элемент с заданным индексом не являеться массивом, то будет возвращёнAdvancedArrayInterface::isDummy()
=>True
, если индекс не задавать, то будет возвращён новый экземплярAdvancedArrayInterface
от первого попавшегося вложенного массива, если вложенных массивов нет, то будет возвращёнAdvancedArrayInterface::isDummy()
=>True
AdvancedArray::arrays()
метод последовательно выдаёт все элементы-массивы, будет возвращёнGenerator
, который выдаёт новый экземплярAdvancedArrayInterface
для всех вложенных массивов (следует использовать как foreach (AdvancedArray::arrays()
as$k
=>$v
))AdvancedArray::values()
метод последовательно выдаёт все элементы-значения (не массивы), будет возвращёнGenerator
, который выдаёт новый экземплярCommonValueInterface
для всех элементов не являющимися массивами (следует использовать как foreach (AdvancedArray::values()
as$k
=>$v
))
Для создания экземпляров класса AdvancedArray
можно использовать фабрику AdvancedArrayFactory
.
Ведение версий пакета
Версии пакета привязаны к минимальной версии PHP. Semantic Versioning не соблюдается.
Версии пакета это минимальная версия PHP, нет разделения на major и minor. Изменение в minor могут нарушать обратную совместимость. Это важное обстоятельство, если будите использовать пакет sbwerewolf/language-specific
, то пожалуйста указывайте точную версию, что бы у вашего кода не было проблем при обновлении пакетов (composer update).
Заключение
Прошу делиться в коментах вашими способами «типобезопасной работы» с массивами в PHP. Своим способом я поделился в этой статье. Может быть есть аккие то более зрелые способы ? или все моим опасения устарели и я отстал от жизни ? Пишите комментарии. Но без оффтопика.
Написанного выше более чем достаточно, что бы понять возможности sbwerewolf/language-specific
, если у вас остались вопросы о том как этот пакет можно применить в конкретных use-case, то пишите мне в личку в ВК, потому что в коментах я могу отвечать один раз в сутки (спасибо всем кто ставит минусы, спасибо Хабру за лимиты на общение), и вы просто не дождётесь моего ответа.
спасибо за чтение.