Магический объект для хранения и передачи разнородных данных с проверкой типов и значений

В PHP для хранения и передачи разнородных данных (конфигурации компонентов, наборы параметров для функций, опции для виджетов и т.п.) обычно используют массивы — их универсальность и легкость использования весьма способствует этому, однако при этом возникают следующие проблемы:

  1. При разработке даже в продвинутых IDE (системах разработки) и при выполнении приложения отсутствует какой-либо контроль за структурой и типами данных в массиве.
  2. IDE ничем не может помочь при разработке, так что названия возможных ключей массивов придется вспоминать-печатать или где-то искать-копировать-вставлять, что кроме неудобства и снижения производительности повышает вероятность опечатки.
  3. Сложно контролировать где-как используются отдельные элементы этого массива и соответственно сложно рефакторить, даже несмотря на мощь современных IDE.


Для небольших проектов эти проблемы могут быть еще несущественны, там проще все проконтролировать, но с ростом объема кода они становятся все заметнее.


Один из известных подходов который позволяет решить проблемы 2 и 3 — создание объектов на основе этаких псевдоклассов расширяющих stdClass. Описанные в PHPDoc свойства класса и указание везде где используется объект его класса (в PHPDoc) позволяет работать автокомплиту и проверке возможных имен свойств, контролировать места применения свойств и при необходимости легко их рефакторить:
/**
* @property integer   $integer
* @property integer[] $integers
*/
class DataStorage extend stdClass
{
}

$ds=new DataStorage;
$ds->integer=13;
$ds->integers=[1,2,3];

Но проблема с контролем значений так не решается, данные пришедшие извне IDE точно не проконтролирует, не говоря уж о том, что предупреждение в IDE кто-то может и проигнорировать, в результате неверные данные могут привести к неожиданной ошибке в неожиданном месте. Кроме того, насколько мне известно, PHPDoc пока что не позволяет указывать список возможных значений свойств, что часто бывает важно.

Добавим немного PHP-магии: StrictDataStorage

Не найдя подходящего решения этих проблем разработал класс который позволяет выполнять проверку данных на основе PHPDoc, при этом как дополнительный бонус проверяет значения и по списку возможных. Работает класс аналогично решению приведенному выше с stdClass — создается класс отнаследованный от StrictDataStorage и у него в PHPDoc через @property описываются типы свойств. При присвоение значений свойствам производится проверка значений на соответствие типа описанному и по результату проверки или происходит присвоение, или срабатывает обработчик ошибки — по умолчанию выбрасывается исключение. При проверке поддерживает все типы данных PHPDoc кроме callable, перечисление нескольких возможных типов через | и массивы описанные через [] в конце типа.
Кроме элемента @property обрабатывается @enum и @options.

@enum

Позволяет указывать список возможных значений свойства. Исследование имеющейся в интернете информации привело к выводу, что никакого стандарта и даже просто соглашения по этому поводу на настоящий момент не существует, поэтому пока что придумал свой вариант не конфликтующий с @property. Синтаксис @enum похож на @property — сначала идет описание возможных значений, потом название свойства. Возможные значения указываются двумя способами
  1. Списком в квадратных скобках — содержимое скобок разбирается через json_decode(), поэтому должно соответствовать по формату, в первую очередь это означает использование двойных кавычек для обрамления строк.
  2. Названием класса в котором хранятся значения, этот класс должен поддерживать интерфейс EnumArrayableInterface, который лежит в том же репозитории, можно в принципе сделать и поддержку SplEnum.


@options

Содержит настройки работы класса через запятую, в настоящий момент поддерживается два значения:
  • PhpDocNotRequired — по умолчанию если свойство не описано элементом @enum или @property, то ему невозможно присвоить значение, наличие PhpDocNotRequired в @options включает менее строгий режим проверки — свойство м.б. присвоено даже если не описано. Этот режим позволяет описывать только те свойства для которых важна проверка значения.
  • StrictNumberTypeCheck — по умолчанию если свойство описано как integer или float, то строки содержащие числа также проходят проверку, как это применяется в PHP повсеместно, добавление StrictNumberTypeCheck в @options активирует более строгий режим проверки чисел — строки не пройдут!


Пример

Класс StrictDataStorage выложен здесь github.com/Yannn/php-strict-data, как он работает проиллюстрирую на примере приведенного там же наследника SampleDataStorage:
<?php
/**
* @property integer $integer
* @property Closure $callback
* @property mixed $mixed
*
* @property integer[] $integers
* @enum SampleEnum[] $integers
*
* @property float|string $enumFloat
* @enum ["1.8","7","9"] $enumFloat
*
* @property string[] $enumArray
* @enum ["one","two"][] $enumArray
*
* @options PhpDocNotRequired|StrictNumberTypeCheck
*/
class SampleDataStorage extends StrictDataStorage
{
}

Т.к. указано @options PhpDocNotRequired у объекта класса SampleDataStorage можно присвоить значение любому свойству, но при присвоении свойств $integer, $callback, $mixed, $integers, $enumFloat, $enumArray значения будут проверятся по описанным в @property и @enum правилам:
  • Значение $integer д.б. обязательно целым числом, если убрать из @options значение StrictNumberTypeCheck, то можно будет присвоить строку содержащую целое число.
  • Значение $callback д.б. обязательно замыканием, т.е. объектом типа Closure.
  • Значение $mixed м.б. любым — тип не проверяется.
  • Значение $integers д.б. массивом целых чисел, при том список возможных значений элементов массива будет получен методом getEnumValues() из SampleEnum.
  • Значение $enumFloat д.б. дробным числом или строкой, при том возможные значения перечислены в списке ["1.8","7","9"].
  • Значение $enumArray д.б. массивом строк, при том возможные значения элементов массива перечислены в списке ["one","two"].

При несоответствии значения указанным правилам будет вызываться исключение. Методы обрабатывающие ошибки реализованы как protected, так что поведение при ошибке легко изменить наследованием.

Требования, совместимость

Т.к. давно использую PHP 5.4, то и класс требует PHP>=5.4, других особых требований вроде бы нет.

Буду рад замечаниям, предложениям и конечно же багрепортам.
Поделиться публикацией

Комментарии 7

    0
    А какой оверхед по производительности? Не проверяли на больших наборах обрабатываемых данных?
      0
      Не проверял, т.к. рассчитывал не на большие наборы данных, а скорее для штучных экземпляров, но результаты выполнения тяжелых операций (получение и парсинг пхпдока) кэшируются в стат. свойствах, так что сильно тормозить не должно.
      0
      Идея интересная. Правда расстраивает что реализацию особо ни как не получится использовать.
      Причины этого:
      1. Не используются namespaces
      2. Нельзя подключить через composer
      3. Нет тестов

      Мне кажется что если исправить эти недостатки, то этим начнут пользоваться.
        0
        Согласен, что стоит добавить все три пункта — сделаю со временем, каким образом мешает отсутствие тестов?
          +1
          Отсутствие тестов не позволит включить эту библиотеку в сколь нибудь важный проект. Как без тестов можно быть уверенным что всё работает? А ведь изменения наверняка будут нужны, новый функционал, рефакторинг. А если тесты есть, и покрытие приличное, то можно спокойнее брать в проект библиотеку.
            0
            В наши дни воспринимать хоть сколько нибудь маленькую библиотеку серьезно при отсутствии тестов никто не будет. Тесты так же хорошая документация, а у вас с этим так же плохо.
          0
          В свое время был такой вот экстеншен, который добавлял в PHP типизированные буферы по аналогии с JS. Учитывая что даже в текущих RFC нету ничего о тайп хинтинге в массивах (да и не особо вижу в этом смысла), то ваше решени имеет право на жизнь, хотя в виде эктеншена смотрелось бы интереснее (на том же зефире). А так, если уж загоняться, есть Hack.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое