Type hinting
В PHP7 появились подсказки типов (type hinting), что позволило IDE проводить более качественный статический анализ кода, качество нашего кода улучшилось (или правильно говорит "стало более лучше"? ).
Конечно и раньше можно было для IDE написать подсказку в коментах к коду, но теперь типы стали частью кода и теперь их стало возможным рефакторить и не бояться того что ты что то где то забудешь (рефакторить конечно в смысле переименовывать классы и интерфейсы).
Кроме того, что стало возможным указывать выходной тип, появилась возможность указывать тип входного аргумента.
Но кроме приятных возможностей type hinting накладывает и обязанности, то есть типы переменных действительно должны быть такими как указано в сигнатуре метода.
Если не проверять типы, то можно получить ошибки в методах и конструкторах (особенно радуют ошибки в конструкторах).
Писать проверки вручную утомительно, я решил это дело автоматизировать, но не через проверку, а через приведение к нужному типу.
В моей работе достаточно часто приходиться писать с нуля, обычно это или прототипы, или парсеры, или ETL для нового источника данных, по сути тоже парсер.
Работаешь конечно с массивами (например когда читаешь из *.csv), работать с базой можно через ORM, но для моих задач это слишком громоздко, мне удобно работать с базой через PDO, которое отдаёт тебе данные опять же в массивах. «Любимый» Bitrix не умеет возвращать данные иначе как в массиве.
Как ни крути приходиться извлекать данных из массивов. Поэтому я написал обёртку для работы с массивами.
Что бы не копипастить код из проекта в проект я оформил пакет для Composer:
composer require sbwerewolf/language-specific
ValueHandler
Первоё моё требование было — всегда знать значение какого типа я получу. Перед этим значение конечно надо бы ещё получить, наверное по индексу, так мы пришли к тому что нам нужен метод get().
И теперь нужны методы для приведения типа, типов в PHP не много, получились такие методы:
- int()
- str()
- bool()
- double()
Иногда попадаются массивы, поэтому пусть будет и для массивов:
- array()
Иногда надо просто получить элемент как он есть:
- asIs()
Иногда элемента с заданным индексом может не быть и тогда надо использовать значение по умолчанию:
- default()
ArrayHandler
Следующим требованием было получить возможность упростить массив из одного значения до ровно этого значения.
Покажу на примере из документации:
$connection = new PDO ($dsn,$login,$password);
$command = $connection->prepare('select name from employee where salary > 10000');
$command->execute();
$data = $command->fetchAll(PDO::FETCH_ASSOC);
/*
$data =
array (
0 =>
array (
'name' => 'Mike',
),
1 =>
array (
'name' => 'Tom',
),
2 =>
array (
'name' => 'Jerry',
),
3 =>
array (
'name' => 'Mary',
)
);
*/
$names = new ArrayHandler($data);
$result = $names->simplify();
echo var_export($result,true);
/*
LanguageSpecific\ArrayHandler::__set_state(array(
'_data' =>
array (
0 => 'Mike',
1 => 'Tom',
2 => 'Jerry',
3 => 'Mary',
),
))
*/
Можно конечно бежать по массиву, который вернётся из запроса, и делать такое присваивание:
$response[] = $element[0];
, но мне так не нравится, пусть это происходит автоматически, так появился метод simplify().
Ну и раз уж у нас есть обёртка над массивом, то добавим метод для проверки наличия индекса — has(), если захочется пробежаться по элементам массива, то поможет метод next().
На этом можно было бы и остановиться, потому что уровень автоматизации достиг комфортного уровня, но иногда приходиться работать с вложенным массивом вложенного массива, и мне удобней сразу получить ArrayHandler для целевого массива, поэтому я добавил метод pull(), который возвращает ArrayHandler для вложенного массива.
Выглядит это так:
$address = new ArrayHandler($item)->pull('metaDataProperty')->pull('GeocoderMetaData')->pull('Address')->asIs();
Можно конечно и так писать:
$address = $item['GeoObject']['metaDataProperty']['GeocoderMetaData']['Address'];
, но у меня в глазах рябит от количества квадратных скобок, мне удобней через pull().
Общие рассуждения
Когда код подключается из Композера это очень удобно, кроме того что вы избавляетесь от необходимости копипастить, вы одной командой получаете свою библиотеку и она всегда под рукой.
Перед тем как делать свой пакет я посмотрел аналоги и ни чего подобного не нашёл, есть несколько проектов, которые просто обёртка над array, и в этих проектах просто оборачивают многие методы для работы с массивами, а типобезопасности ни где нет.
Видимо написать (int) или (bool) перед именем переменной всем просто и удобно и ни кто не видит смысла заморачиваться с отдельным репозиторием под это дело.
Возможности библиотеки чуть шире описанных в статье и больше информации можно получить в документации (README.md).
PHP5 ещё не редкость, поэтому у библиотеки есть отдельная версия для PHP5, отличается от версии для PHP7 названием нескольких методов и конечно весь type hinting только в коментах.
Есть версия библиотеки для PHP7.2, отличается только тем что в сигнатуре у метода object() появляется тип возвращаемого значения — object.
Код полностью покрыт тестами, но в принципе так и ломать не чему :)
Пользуйтесь на здоровье!
Ещё один пример использования
foreach ($featureMember as $item) {
$pointInfo = extract($item);
$info = new ArrayHandler($pointInfo);
$address = $info->get('formatted')->default('Челябинск')->str();
$longitude = $info->get('longitude')->default(61.402554)->double();
$latitude = $info->get('latitude')->default(55.159897)->double();
$undefined = !$info->get('formatted')->has();
$properties = ['longitude' => $longitude, 'latitude' => $latitude, 'address ' => $address ,'undefined'=>$undefined,];
$result = json_encode($properties);
output($result);
}
Смотреть во время отладки на JSON в котором числа это числа, логические значения — логические, мне намного приятней чем только на строки.
А вам как?
Комментарии (45)
vn_sten
16.11.2019 17:46я не предлагал вам писать парсеры используя ларавель, просто то что вы написали умеет делать (и даже больше) коллекции которые вы могли бы выдрать из ларавель, а по теме ключами массива не могут быть float, они преобразуются в int =)
E_STRICT
16.11.2019 20:02+1В PHP7 появились подсказки типов (type hinting)
Всё таки, правильней переводить «type hinting» как «контроль типов». И появился он не в PHP 7, а в PHP 5. В PHP 7 был добавлен контроль скалярных типов и возвращаемых значений. Кроме этого в следующем релизе (7.4) появятся типизированные свойства объектов.
E_STRICT
16.11.2019 20:16Следующим требованием было получить возможность упростить массив из одного значения до ровно этого значения.
Для этого есть специальный режим.
phpdelusions.net/pdo/fetch_modes#FETCH_COLUMN
E_STRICT
16.11.2019 20:24мне удобно работать с базой через PDO, которое отдаёт тебе данные опять же в массивах
А как же PDO::FETCH_OBJ и PDO::FETCH_CLASS?SbWereWolf Автор
16.11.2019 22:49PDO::FETCH_CLASS — Зачем такие сложности? Специфика задач подразумевает очень короткий срок жизни кода. И даже безотноситено этого, как использование PDO::FETCH_CLASS гарантирует мне соблюдение типов?
Автоматического приведения типов не случиться, присвоения значений по умолчанию тоже.
Писать каждый раз логику? вот я и написал библиотеку один раз что бы использовать везде. Будь у меня источник данных PDO или .csv, или json из какого то API, не важно какой источник данных, если он сводиться к массиву то с помощью своей библиотеки у меня всегда на выходе будут переменные строго заданных типов.
FETCH_COLUM по этой же причине не всегда применим, по этой же причине методу simplify можно отдать массив с нужными нам индексами и он вернёт только заданные колонки, это нужно когда мы из .csv файла парсим не все колонки, а только две три.E_STRICT
17.11.2019 08:56PDO::FETCH_CLASS — Зачем такие сложности? Специфика задач подразумевает очень короткий срок жизни кода. И даже безотноситено этого, как использование PDO::FETCH_CLASS гарантирует мне соблюдение типов?
Не совмем представляю, что такое короткий срок жизни кода. Классы как раз упрощают работу с данными и предоставляют типизацию. Если вы не можете использовать ORM, создавайте простые value-объекты через PDO::FETCH_CLASS или из массивов.
final class User { public function getName(): string { return $this->name; } }
См. steemit.com/php/@crell/php-use-associative-arrays-basically-never
FETCH_COLUM по этой же причине не всегда применим
array_columnSbWereWolf Автор
17.11.2019 11:13Идея с классами вполне себе альтернативный вариант, но надо гетеры выписать, это чуть сложней чем просто одна строка get('side_numbers')->int().
И в базу писать массив как то проще, с классом придётся этот массив предварительно создать и наполнить.
array_column() — да, спасибо за подсказку, можно использовать внутри simplify().
Короткий срок жизни кода это когда нам надо завести в базу исходные данные которые позже будут обработаны с помощью SQL.
Моя последняя задача была о том что бы из csv залить в базу точки продаж и с помощью описательного адреса для каждой точки определить гео координаты.
А дальше конечные пользователи CRM с точками продаж должны работать через админку Битрикса.
И кругом у тебя массивы — csv — массив, ответ геокодера — массив, ответ ORM Битрикса — массив.
Соответственно с обработкой этих данных и появлялись новые требования к этой библиотеке и исходя из них развивался функционал.genteelknight
18.11.2019 14:19ORM Битрикса же умеет объекты возвращать
SbWereWolf Автор
18.11.2019 14:38Я нашёл два метода:
- CIBlockElement::GetPropertyValues
- CIBlockElement::GetPropertyValuesArray
Первый возвращает значения с ключом — идентификатором, по которому нельзя понять что внутри, предварительно по ИД надо найти код свойства, то есть предварительно надо словарик свойств получить.
Второй метод возвращает свойства элемента с ключом — кодом, это годный для меня вариант.
Как получить объект?genteelknight
18.11.2019 14:43В новом ядре есть ORM, которая ± сносная и умеет отдавать объекты.
dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=011687
olezh
16.11.2019 23:28Очень плохо написано по-русски
SbWereWolf Автор
17.11.2019 00:12-1Главное что бы код был ок. А пресс релизы пусть пресс секретари пишут и прочие контент менеджеры. Каждоиу своё
koeshiro
17.11.2019 00:42Тут многое можно было бы излагать проще и понятнее на примере интерфейса,. Это так, на будущее.
SbWereWolf Автор
17.11.2019 01:15Последний пример кода на мой вкус очень наглядный.
Изложение конечно скорее развлекательное чем информативное. Но не вижу смысла, мне кажется ни кто не кинется разворачивать либу с помощью композера и тыкать в неё острой палкой, что бы понять на что она способна.
В репозитонии для каждого метода показаны примеры работы, ещё более подробно описано в тестах, было бы желание разобратья.
Вообще конечно статья для получения обратной связи, потому что я конечно доволен как слон, но я всего не знаю и опыт у меня только личный, может быть есть готовые инструменты? Может быть нужен совсем другой подход?
Не знаю. Было бы интересно послушать ответы на эти вопросы.
olezh
17.11.2019 02:40То есть ты сам для себя написал статью, ок
SbWereWolf Автор
17.11.2019 09:54+1Кому надо разберётся. Или вообще публиковать ни чего не надо если стиль излржения кому то может показаться не достаточно доходчивым ?
Hett
18.11.2019 08:13Первое что бросилось в глаза, это метод next() который возвращает генератор. Дальше даже смотреть не стал.
olegmar
17.11.2019 10:27Зачем ArrayHandler->simplify(), когда есть array_column?
SbWereWolf Автор
17.11.2019 12:33Смысл использования ArrayHandlera в том что по get() он выдаёт ValueHandler у которого есть методы для приведения типов.
Можно упрощать массив с помощью array_column(), но для типобезопасности всё равно надо создать экземпляр ArrayHandler.
Смысл использования библиотеки в интеграции функционала.
Можно писать $number = (int)@(vars['key']) всегда на выходе будет int и ни когда не будет ошибки "ключ не найден", но как то не очень нравиться мне такой код.
BeMySlaveDarlin
17.11.2019 15:25Исходя из задачи в начале, можно было обойтись опциями для PDO fetch и json encode/decode, если вопрос касался только скалярных типов.
Hatsu
17.11.2019 15:25foreach ($featureMember as $item) { $pointInfo = extract($item); $address = (string) ($pointInf['address'] ?? 'Челябинск'); $longitude = (float) ($pointInfo['longitude'] ?? 61.402554); $latitude = (float) ($pointInfo['latitude'] ?? 55.159897); $undefined = !array_key_exists('formatted', $pointInfo); $properties = ['longitude' => $longitude, 'latitude' => $latitude, 'address ' => $address ,'undefined'=>$undefined,]; $result = json_encode($properties); output($result); }
Мне кажется тут можно обойтись без библиотекиReDev1L
17.11.2019 15:29Если это делается один раз — да, можно писать такие процедурки. Если это большой ETL проект — код надо делить на дата провайдеры, трансформеры и сериалайзеры, иначе будет каша.
SbWereWolf Автор
17.11.2019 15:50можно обойтись, а можно не обойтись, здесь:
$pointInf['address']
будет лишний варнинг если индекса нет.
Прелесть библиотеки в автодополнении, жмёшь контрл + пробел и выбираешь вариант, на клавиатуре, за секунду, обычно IDE подставляет первыми вариантами то что тебе надо и для того что бы набить код уходит минимум времени, часто пишешь на одном дыхании.
Можно писать так:
$val1 = (int)(array_key_exists('key1', $vars) ? $vars('key1') : $def1); $val2 = (int)(array_key_exists('key'2, $vars) ? $vars('key2') : $def1); $val3 = (float)(array_key_exists('key3', $vars) ? $vars('key3') : $def2); $val4 = (bool)(array_key_exists('key4', $vars) ? $vars('key4') : $def3); $val5 = (int)(array_key_exists('key5', $vars) ? $vars('key5') : $def1);
а мне нравиться писать так:
$vars = new ArrayHandler($vars); $val1 = $vars->get('key1')->default($def1)->int(); $val2 = $vars->get('key2')->default($def1)->int(); $val3 = $vars->get('key3')->default($def2)->double(); $val4 = $vars->get('key4')->default($def3)->bool(); $val5 = $vars->get('key5')->default($def1)->int();
и не надо ни каких ребусов решать с "??" и когда дочитал всю строку выражения не надо вспоминать что результат надо привести к типу который был указан в начале строки.
Всё делается линейно:
$val1 = // в $val1 запиши значение следующего выражения $vars // из массива $vars ->get('key1') // возьми элемент с индексом 'key1' ->default($def1) // используй значение по умолчанию $def1 ->int(); // приведи значение к int
Мне такой код легче заходит, библиотека написана для себя и мне не жалко ей поделиться, кто может обойтись без неё обходитесь, кто же вас неволит?GreedyIvan
17.11.2019 16:13Нормальный этап взросления.
Сначала языковая конструкция кажется ребусом.
Пишется своя реализация, удобная, читаемая.
Потом добавляется в неё сахарок… И где-то начинают крутиться мысли, а что делает мой велосипед такого, что я не использую встроенные в язык конструкции?
?? кинет ворнинг при отсутствии элемента с таким ключом — это фиаско.
Использовать тайп каст, чтобы передавать в функции значения нужных типов — это эпик фейл.
Практически любой сериалайзер сделает всё тоже самое, что и библиотека автора, только скажи в какой объект сложить данные. И к преобразованию типов отнесется со всей серьёзностью, с предсказуемыми ошибками, если данные без потерь не кастятся в целевой тип.
GreedyIvan
17.11.2019 16:22За велосипед с default хочется отправить почитать что-нибудь про Option. Не говоря уже о том, что ValueHandler объединяет в себе и конструктор объекта, и трансформацию, и хранилище. Жуткий класс. Хочется отдать его трем разным людям, чтобы каждый сделал свою часть правильно.
SbWereWolf Автор
17.11.2019 17:09ValueHandler это хранилище, которое может отдать значение приведённое к нужному типу, может выдать тип этого значения, может показать флаг было ли значение задано.
Конструктор объекта в ValueHandler отсутствует, это не фабрика.
У меня не было цели сделать 100500 классов, мне по функционалу хватило двух классов (на самом деле четырёх).
Разделять на классы имеет смысл если планируется активно менять функционал. У меня таких планов нет.
Ради искусства я только прикрутил фабрику для ValueHandler, ещё что то лепить ради красоты уже не хочется.
Но вы можете сделать пул реквест с вашей правильной реализацией.
И кстати можете дать ссылку на «любой сериалайзер», думаю благодарен за неё буду не я один.
И ссылку на почитать про Option.
С методом default() что не так? обычный сеттер, магия в геттерах для значения:
// значение имеется ? $result = $this->has() ? // да, вернуть значение $this->_value // нет, вернуть значение по умолчанию : $this->_default;
в чём криминал?GreedyIvan
17.11.2019 18:05Всё это делается средствами языка. Пример уже приводили. (type) — это каст. ?? — альтернатива, если null.
null ===
gettype / is_ — для получения и проверки типов.
А криминал в том, что для случае null вы, зачем-то, прогоняете альтернативное значение через объект, делая его интерфейс крайне нетривиальным. Например, в каком состоянии будет объект после второго применения default? Можно ли так вообще делать? Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать? И т.д.
SbWereWolf Автор
17.11.2019 19:37вы, зачем-то, прогоняете альтернативное значение через объект, делая его интерфейс крайне нетривиальным
Куда что я прогоняю? Я устанавливаю свойство экземпляра, это свойство используется при выдаче значения.
Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать?
А кто код пишет? если вы ссылку на экземпляр отдадите наружу, то с её помощью экземпляром можно будет воспользоваться. Библиотека почему должна этому препятствовать?
Мне вообще не понятно почему библиотека должна как то ограничивать своего пользователя? можно создать экземпляр с каким то значением и получить его приведённым к чему угодно и сколько угодно раз, и каждый раз перед получением значения можно устанавливать разные значения по умолчанию, почему нет?
Если хочется, то с помощью метода type() можно получить исходный тип.
Библиотека предназначена для парсинга, экземпляр класса живёт одну итерацию цикла, в одной ветке условного оператора, но если вам хочется использовать этот класс глобально или гонять по коду туда сюда, то это ваше дело.GreedyIvan
17.11.2019 20:43Библиотека предназначена для парсинга, экземпляр класса живёт одну итерацию цикла, в одной ветке условного оператора
Вот я и говорю, что у вашего класса довольно нетривиальный интерфейс. Шаг влево, шаг вправо — проведение объекта непредсказуемо.
Это не библиотечный функционал, а хелпер под конкретный, причем типовой, случай.
Т.е. выбранными вами архитектурными решениями, вы неявно резко ограничили применение классов исключительно рамками одной типовой задачи. Это решение может казаться хорошим, пока не захочется переиспользовать данные классы для чего-то другого. И тогда придется решать проблему отсутствия определенных гарантий у получаемых объектов, и весь код сразу станет не таким уж и классным.
Vilaine
18.11.2019 08:28Писать проверки вручную утомительно
Лучше посмотрите в сторону статического анализа кода.SbWereWolf Автор
18.11.2019 13:05имелось в виду бесконечные:
key_exists('key',$array); !empty(value) ? value : $default_val;
Vilaine
18.11.2019 21:18Такие проверки могут понадобиться лишь на стыке слоёв, например, для валидации входящих данных. В этом случае существует множество готовых валидаторов. Если речь о работе с БД, то приведения типов вполне достаточно, так как с БД «жесткий» контракт.
Типобезопасности в вашем решении я не вижу. Аналогов вашей утилиты нет потому, что непонятно, какую задачу вы решаете.
Кстати, Psalm поддерживает ассоциативные массивы psalm.dev/docs/annotating_code/type_syntax/array_types/#object-like-arrays — вот с ним можно добиться кое-какой типобезопасности.SbWereWolf Автор
18.11.2019 23:44Получить с фронта JSON, распарсить и положить в базу в причёсанном виде.
Передать в класс-фабрику значения правильных типов.
Вот это две моих боли которые привели к оформлению кода в библиотеку.
Сначала класс назвался ArrayParser и всё что мог это выдавать значения по индексу.
Меня утомляло выписывать однообразные проверки, тут в коментах уже приводил примеры, вообще использование однообразного кода раздражает, если что то делается одинаково оно должно быть упаковано в метод.
Потом мне надо было получать в API данные и тупо фигачить их в базу, но не в плоскую структуру а в иерархическую, перед тем как значение положить в базу его надо было привести из строки в какой то тип, потом я стал формировать записи БД через методы классов и уже в методы класса-фабрики надо было передавать те же самые значения, и когда ты в аргумент с типом инт отдаёшь переменную с типом стринг, интерпритатор почему то падает.
И вот как то так пришёл к приведению типов.
При чём вся эта крутоверть была не на одном проекте а на четырёх разных за полгода, с разными стеками, общее было только в PHP, даже базы были разные, SQLite Postgres MySql, полный привет, особенно что касается работ с датами, везде свой методы.
Я копипастил класс из проекта в проект, потом сделал первую версию библиотеки, оказалось что устанавливать через композер это очень удобно.
Потом думал о том что ещё можно добавить в класс, и конечно использовал класс в работе, чего не хватало или что элементарного можно было добавить — добавил.
Последний месяц уже специально для публикации готовил код. По репозиторию можно понять.
Да, конечно подход, «всё есть строка» это конечно правильный подход, но не для обработки данных.
Вообще это всё прелюдия к другой очень большой публикации, про работу с данными, ближе к новому году или сразу после него код дойдёт до нужной кондиции и сделаю релиз.
Вышёл в свет с этой библиотечкой, получил массу советов.
Всем спасибо, попробую использовать ваши советы в своей повседневной работе.
E_STRICT
18.11.2019 16:12!empty(value)? value: $default_val;
empty() не подходит здесь, потому что для integer, 0 не пустое значение. Тоже самое для false для boolean.
Проверяйте на null.
$array['key'] ?? $default_value;
vn_sten
Вы смотрели коллекции в ларавель?
SbWereWolf Автор
нет конечно, я только по гихабу поиск делал, репозиторий Ларавея в результаты поиска не попал. с другой стороны, мне парсеры на Ларавеле писать?
dspancov
github.com/tightenco/collect
SbWereWolf Автор
Спасибо за ссылку.
Посмотрел на коллекции. Весь функционал коллекций это работа с массивом. Но основное назначение моей библиотеки это работа со значением.
Во первых понять определено оно или нет.
Во вторых получит значение строго заданного типа, либо получить значение по умолчанию.
Этот функционал не сложно сочинить самому, занимает он пять строчек кода, но когда тебе это надо сделать для 5 — 15 колонок, тебе очень хочется упихать обработку одной колонки в одну строчку и не страшно если это будет цепочка вызовов, и не страшно что под капотом будет работать не 5, а 500 сторок кода, это всё не имеет значения когда тебе просто надо накидать код, который жить будет от силы неделю, а скорей всего уже завтра утратит свою актуальность.
webdev12
а также
Symfony Collections, Symfony Serializer