Звучит слишком громко? Давайте уточним, чтобы избежать обманутых ожиданий: этот пакет использует немного магии вне Хогвартса, и будет действительно полезен любителям строгой типизации в PHP.
-
Введение
-
Проблемы слабой типизации в PHP
-
Стандартные подходы приведения к типу
-
PHP Typed: утилита для приведения к типу
-
PHP Typed: Примеры использования
-
Заключение
1. Введение
Всем привет! С вами WPLake, агенство по WordPress разработке.
Новый год уже близко, и кажется, все ищут минутку, чтобы подвести итоги уходящего года. Что может быть лучше, чем поделиться решениями распространных проблем, с которыми мы столкнулись, и для которых нам удалось найти удачное решение?
В этом посте речь пойдёт о PHP Typed — Composer пакете (wplake/typed), который мы опубликовали на этой неделе. Давайте разбираться, что к чему.
2. Проблемы слабой типизации в PHP
PHP, как интерпретируемый язык без строгой типизации, изначально был создан для того чтобы ускорить процесс разработки и упростить жизнь разработчикам. Отсутствие компиляции и встроенная система сбора мусора обещали сделать PHP настоящим раем для программистов.
Однако, как и всегда, у каждой медали есть две стороны. Довольно быстро стало очевидно, что слабая типизация PHP, хоть и ускоряет разработку, при этом делает код менее безопасным и более сложным для поддержания.
К примеру:
function getUserData($id) {}
Что здесь происходит? Ожидает ли функция целое число в качестве $id
, или, может быть, токен-строку? Что она возвращает? Объект? Массив? Это невозможно опеределить без ознакомления с реализацией.
Именно поэтому и появился PHPDoc:
/** * @var int $id * @return array */ function getUserData($id) {}
Отлично, так гораздо лучше. PHPDoc стал фактическим стандартом для документирования кода в коммерческих проектах. Более того, начиная с PHP 7.4, сам язык сделал значительный шаг вперёд в поддержке строгой типизации.
Но несмотря на это, мы по-прежнему сталкиваемся с множеством проблем, связанных с типами. Давайте рассмотрим пару примеров распространённых громоздких конструкций, написанных при работе с нетипизированными переменными:
function getUserAge(array $userData): int { return true === isset($userData['meta']['age']) && true === is_numeric($userData['meta']['age']) ? (int)$userData['meta']['age'] : 0; } function upgradeUserById($mixedUserId): void { $userId = true === is_string($mixedUserId) || true === is_numeric($mixedUserId) ? (string)$mixedUserId : ''; }
Хотя сами по себе эти функции бесполезны, они очевидно демонстрируют избыточность кода и потерю ясности, которые часто возникают при работе с нетипизированными переменными.
«Минутку. Почему просто не объявить тип для
$mixedUserId
? Почему не добавить PHPDoc-комментарии, чтобы описать ключи в$userData
?» — глядя на эти примеры, возможно, думаете вы.
В идеальном мире, да, мы бы объявили строчный тип для $mixedUserId
, а $userData
был бы либо экземпляром класса, либо массивом с ключами, описанными в PHPDoc. Однако реальность такова, что программное обеспечение не пишется одним разработчиком и не ограничивается сотней строк кода. В проекте всегда есть сторонние компоненты, внешние библиотеки, поставщики и обширные легаси-кодовые базы.
Когда дело касается массивов, у нас есть ощущение, что эта проблема будет существовать ещё долгие годы. Почти невозможно гарантировать фиксированную структуру массива — достаточно вспомнить $_SERVER
, где значения могут меняться в зависимости от окружения, не говоря уже о пользовательских данных из $_GET
и $_POST
.
Однако довольно о проблемах — давайте поговорим о решениях.
3. Стандартные подходы приведения к типу
Итак, как мы можем справится с задачей приведения к типу? Существует три распространённых подхода, доступных из коробки:
3.1) Использование isset и проверки типа
Как мы уж видели выше:
$age = true === isset($userData['meta']['age']) && true === is_numeric($userData['meta']['age']) ? (int)$userData['meta']['age'] : 0;
Этот подход создает избыточный и неясный код даже на таком простом примере.
3.2) Использование оператора объединения с null (??) и проверки типа
$number = $data['meta']['age'] ?? 10; $number = true === is_numeric($number) ? (int)$number : 10;
Что же происходит здесь? В итоге мы получаем две строки кода, потому что оператор объединения с null не решает проблему проверки типа. Более того, при использовании своего значения по умолчанию с данным оператором мы вынуждены дублировать его значение.
3.3) Использование оператора объединения с null и явного приведения к типу
$number = (int) ($data['meta']['age'] ?? 10);
Вот это дело! Коротко и ясно! Возможно, кто-то из вас сейчас подумает: «Эх, зря я трачу время на чтение вашей статьи».
Тогда наш ответ будет: Подождите минутку. Давайте разберёмся, что здесь на самом деле происходит. Эта строка состоит из двух частей:
Часть 1: $data['meta']['age'] ?? 10
Мы безопасно проверяем наличие переменной и подставляем значение по умолчанию, если её нет. Отлично, идём дальше.
Часть 2: (int) {resultFromNullCoalescing}
Здесь мы просим PHP привести элемент массива (или значение по умолчанию) к целому числу. Это кажется очевидным, но давайте задумаемся, к какому типу переменной мы на самом деле применяем приведение к int
.
Упс, но на самом деле мы не знаем тип.
Что? Да, при написании этого кода вы, вероятно, «ожидали», что элемент [meta][age]
будет либо целым числом, либо хотя бы строкой с числовым значением. Но можете ли вы это гарантировать? В реальном мире ответ — нет. В больших приложений есть бесчисленное количество ветвлений логики, и одно из них может изменить тип этого значения.
Более того, даже если сейчас это целое число, кто знает, что будет завтра? Зависимости обновились, и теперь поле age
— это объект с несколькими свойствами. Бум! Теперь данная строка вызывает фатальную ошибку, потому что PHP не может преобразовать объект в строку, если он явно не реализует метод __toString
.
Еще один случай: преобразование массива в строку
$string = (string)($array['meta']['location'] ?? 'Default one');
Когда значение 'location'
оказывается массивом вместо ожидаемой строки, скрипт:
-
Сгенерирует предупреждение (notice)
-
Продолжит выполнение, используя строку со значением
'Array'
Хотя очевидно, что данные используются некорректно, в этом случае правильная логика должна была бы воспользоваться значением по умолчанию — 'Default one'
.
«Это не моя вина. Пусть будет фатальная ошибка, пусть будет поломанная логика.»
(Скажет какой-нибудь беспечный разработчик.)
Но должна ли это действительно быть фатальная ошибка? Если это всего лишь небольшая ветка кода, отображающая пару незначительных описаний на экране, должна ли она приводить к падению всего приложения в продакшене?
Если вы работали с данными от внешних поставщиков, вы наверняка знаете, насколько непредсказуемыми могут быть их обновления.
Хороший программист не беспечен. По крайней мере, в глазах своего начальника, и в мире где есть best practices. Давайте же не будем «плохими парнями».
Итак, первые два подхода безопасны, но они громоздки и избыточны. Теперь давайте наконец рассмотрим, что может предлогает пакет Typed для решения этой задачи.
4. PHP Typed: утилита для приведения к типу
Как же можно улучшить эту ситуацию? По результатам нашего исследования, не существует готового известного пакета, решающего именно эту задачу. Однако есть несколько обёрток для массивов, упрощающих работу с ними, например, Laravel Collections.
Оборачивание элементов в массивы — это неплохая идея, и она отлично работает в рамках экосистемы фреймворка. Но за её пределами нельзя ожидать, что каждый будет использовать этот пакет. Тогда как использовать целую обёртку для извлечения одной переменной — очевидный перебор.
Значит, нам нужно отдельное решение. Хорошая новость в том, что исследование привело к важному открытию: элегантному подходу для обработки вложенных ключей.
Поскольку мы в основном работаем с массивами и улучшаем приведение типов, было бы здорово заменить конструкции вроде $data['meta']['location']['city']
на что-то более элегантное, например, meta.location.city
— аналогично тому, как это делает Arr::get в Laravel.
Теперь давайте сформулируем логику для вспомогательной функции:
«Верни мне значение запрашиваемого типа из указанного источника по заданному пути или верни значение по умолчанию.»
Решено. Теперь поговорим о том, как это должно выглядеть. Решение должно быть простым и интуитивно понятным — даже для тех, кто впервые видит код использующий данную утилиту.
Изначальная конструкция, хоть и небезопасна, но довольно выразительна:
$number = (int)($data['meta']['age'] ?? 10);
Можем ли мы получить что-то похожее? После ряда раздумий, множества проверок и создания пакета — да! И вот результат:
$number = int($data, 'meta.age', 10);
Выглядит очень похоже, верно? Но погодите — что это за int
обертка? Разве это не зарезервированное ключевое слово в PHP? Это и есть то самое открытие, то самое «нарушение правил», о котором мы упомянули в заголовке статьи:
PHP позволяет использовать имена типов в качестве имён функций.
Вы были уверены, что это запрещено? Не совсем! Хотя определённые имена зарезервированы для классов, интерфейсов и трейтов, для функций таких ограничений нет:
«Эти имена нельзя использовать для названия классов, интерфейсов или трейтов» — PHP Manual: Reserved Other Reserved Words
Это означает, что мы можем писать такие вещи как string($array, 'key')
, что похоже на (string)$array['key']
, но безопаснее и умнее — ведь оно обрабатывает вложенные ключи и значение по умолчанию.
Пакет поддерживает PHP 7.4+ и 8.0+, и распространяется через Composer, поэтому процесс установки стандартный:
composer require wplake/typed // then in your app: require __DIR__ . '/vendor/autoload.php';
Кстати, импорт этих функций не мешает использовать родное приведение типов в PHP. Поэтому, хотя это и бесполезно на практике, следующий код будет работать:
echo (string)string('hello');
Пакет Typed предоставляет набор вспомогательных функций в пространстве имён WPLake/Typed
, поэтому вам не нужно беспокоиться о потенциальных глобальных конфликтах. Код с использованием импортов выглядит так:
use function WPLake\Typed\string; $string = string($array, 'first.second', 'default value');
Конечно, ваш IDE автоматически добавит строку use
за вас.
Для тех, кто не переносит функции и предпочитает статические методы, пакет предлагает и альтернативный вариант:
use WPLake\Typed\Typed; Typed::int($data, 'key');
Как обычно, не обошлось без ложки дёгтя: в отличие от других типов, ключевое слово array
относится к другой категории и не может использоваться в качестве имени функции. Именно поэтому в этом конкретном случае мы использовали название arr
.
5. PHP Typed: Примеры использования
Давайте возьмём одну функцию из пакета Typed и рассмотрим примеры её использования. Пусть это будет упомянутая ранее функция string
. Вот её декларация:
namespace WPLake\Typed; /** * @param mixed $source * @param int|string|array<int,int|string>|null $keys */ function string($source, $keys = null, string $default = ''): string;
Сценарии использования
5.1) Извлечение строки из переменной смешанного типа
По умолчанию возвращается пустая строка, если переменную нельзя преобразовать в строку:
$userName = string($unknownVar); // you can customize the fallback: $userName = string($unknownVar, null, 'custom fallback value');
5.2) Получение строки из массива
Включая вложенные структуры (с использованием точечной нотации или массива ключей):
$userName = string($array, 'user.name'); // Alternatively: $userName = string($array, ['user', 'name']); // custom fallback: $userName = string($array, 'user.name', 'Guest');
5.3) Доступ к строке из объекта
Включая вложенные свойства:
$userName = string($companyObject, 'user.name'); // Alternatively: $userName = string($companyObject, ['user', 'name']); // custom fallback: $userName = string($companyObject, 'user.name', 'Guest');
5.4) Работа со смешанными структурами
(Например, object->arrayProperty['key']->anotherProperty
или ['key' => $object]
)
$userName = string($companyObject, 'users.john.name'); // Alternatively: $userName = string($companyObject, ['users', 'john', 'name']); // custom fallback: $userName = string($companyObject, 'users.john.name', 'Guest');
Во всех случаях значение по умолчанию — это «пустое» значение для конкретного типа (например, 0
, false
, ""
и так далее), но вы всегда можете передать своё значение по умолчанию в качестве третьего аргумента:
$userName = string($companyObject, 'users.john.name', 'Guest');
Пакет включает функции для следующих типов:
-
string
-
int
-
float
-
bool
-
object
-
dateTime
-
arr (stands for array, because it’s a keyword)
-
any (allows to use short dot-keys usage for unknowns)
Дополнительно:
-
boolExtended (true,1,»1″, «on» are treated as true, false,0,»0″, «off» as false)
-
stringExtended (supports objects with __toString)
Для опциональных случаев, когда вам нужно применять сценарий только если элемент существует, каждая функция имеет вариацию OrNull
(например, stringOrNull
, intOrNull
и т.д.), которая возвращает null
, если ключ не существует.
6. Заключение
Мы уже подключили пакет PHP Typed к одному из наших реальных проектов и с радостью обнаружили, что он значительно улучшил ясность, элегантность и интуитивность нашего кода. Надеемся, что он также окажется полезным и для других PHP разработчиков.
Если данный пакет вам понравился, пожалуйста, поддержите его, поставив ему звезду в его GitHub репозитории, и поделитесь им с коллегами, чтобы они также смогли улучшить свой код.
Спасибо, что нашли время прочитать этот пост! Желаем вам отличного Нового года, полного успехов и роста во всех областях разработки.
P.S. У нас есть ещё одна новость: на этой неделе мы выпустили ещё один пакет, который может быть также полезен PHP-разработчикам. На следующей неделе мы опубликуем пост о нём, но если вам не терпится взглянуть на него прямо сейчас, пакет уже доступен, и вот ссылка на него.
ссылка на оригинал статьи https://habr.com/ru/articles/868640/
Добавить комментарий