В отношении данных, которые программа получает извне, принято следовать правилу trustno1. Это справедливо не только в отношении данных, получаемых непосредственно от пользователя, но и в отношении данных, которые передаёт в подпрограммы клиентский код.
PHP 7 оснащён расширенной системой контроля типов аргументов, включающей не только классы, но и скаляры. Однако в том, что касается сложных структур данных, ничего не изменилось — для них существует единственный тип array, который в PHP может содержать всё, что угодно.
Я надеюсь, что новые версии PHP исправят ситуацию. А на данный момент я хочу поделиться с сообществом некоторыми своими наработками в этой области:
perspectea/typedef
perspectea/generics
typedef
Репозиторий на GitHub: https://github.com/perspectea/typedef
Версия PHP: 7.0
Эта библиотека предназначена непосредственно для работы с типами.
Вы можете определить собственный тип данных с помощью функции Teatypedef:
function typedef(string $aName, IType $aType): IType;
Вы можете как создать и инстанцировать собственный класс, реализующий интерфейс TeaTypedefIType, так и использовать встроенные.
Для обращения к типу предназначена функция Teatype:
function type(string $aName): IType;
Она принимает в качестве аргумента имя типа (аргумент aName функции typedef), и возвращает соответствующий объект.
Чтобы проверить значение на соответствие типу, воспользуйтесь функцией Teais:
function is($aValue, IType $aType): bool;
или методом validate самого объекта типа:
function IType::validate($aValue): bool;
Определены следующие встроенные типы (пространство имён Tea):
function bool(): BoolType;
Логическое значение true/false.
function number(float $aMin = null, float $aMax = null): NumericType;
function int(int $aMin = null, int $aMax = null): IntType;
function uint(int $aMax = null): UIntType;
Числовые типы.
Тип NumericType соответствует PHP-типам int и float.
Являющийся его наследником тип IntType соответствует только PHP-типу int.
Оба типа могут быть ограничены минимальным и максимальным значениями.
Тип UIntType, являющийся наследником IntType, соответствует целым числам без знака — его минимальным значением является 0, а максимальное может быть определено.
function string(int $aLength = null): StringType;
Строковый тип, может быть ограничен по максимальной длине.
Вы можете использовать self-return метод fix, чтобы сделать ограничение по длине строгим — в этом случае будут допустимы только строки, длина которых равна заданной.
function regexp(string $aRegularExpression): RegExpType;
Регулярное выражение.
function enum(...$aValues): EnumType;
Перечислимый тип.
Ограничивает множество допустимых значений заданным набором.
function object(string $aClass = null): ObjectType;
Объектный тип.
Значение может быть только объектом заданного класса (интерфейсы так же допустимы).
Я не до конца уверен, стоит ли для данного типа давать возможность проверки использования трэйта — на мой взгляд, это нарушило бы инкапсуляцию.
function nullable(IType $aType): NullableType;
Nullable-тип.
Дополняет множество допустимых значений дочернего типа значением null.
function any(IType ...$aTypes): MultiType;
Множественный тип.
Объединяет множества допустимых значений всех дочерних типов.
function lot(int $aLength = null): ArrayType;
Массивный тип (ключевое слово array не допустимо в качестве имени функции), может быть ограничен по максимальной длине.
Значение может быть массивом или объектом, реализующим интерфейсы ArrayAccess, Countable и Traversable (вы можете дополнительно ограничить множество допустимых значений с помощью self-return методов acceptArray и acceptObject).
Чтобы задать допустимый тип значений массива, используйте self-return метод of(IType), а для ключей используйте self-return метод by(IType). Если вы зададите тип ключей, отличный от PHP-типов int и string, тип будет иметь смысл только в отношении объектов, поскольку у массивов PHP не может быть ключей других типов.
Так же, как и для строкового типа, вы можете использовать self-return метод fix, чтобы сделать ограничение по длине строгим.
function struct(IField ...$aFields): StructType;
Структурный тип.
Значение, так же как и в случае массивного типа, может быть массивом или объектом с массивным доступом, и так же может быть дополнительно ограничено с помощью self-return методов acceptArray и acceptObject.
Членами структурного типа являются поля — объекты класса, реализующего интерфейс TeaTypedefIField. Переданное для валидации значение является валидным, если оно является массивом или объектом с массивным доступом (в соответствии с дополнительными ограничениями) и проходит валидацию всех полей.
Определены следующие встроенные виды полей:
function field(string $aName, IType $aType = null): Field;
Обычное поле. Не является самостоятельным типом.
При валидации проверяется, содержит ли переданное значение ключ, соответствующий имени поля, а так же соответствует ли значение этого ключа указанному типу, если он задан.
function optional(IField $aField): OptionalField;
Опциональное поле. Не является самостоятельным типом.
Допускает отсутствие в переданном значении ключа, соответствующего дочернему полю.
function union(IField ...$aFields): Union;
Объединение. Является самостоятельным типом.
Для успешной валидации переданного значения необходимо, чтобы оно обязательно проходило валидацию только одного из дочерних полей.
Для наглядной демонстрации работы библиотеки рассмотрим следующий пример:
typedef('input', struct(
field('name', string()),
field('authors', any(
string(),
lot()->of(string())
)),
optional(union(
field('text', string()),
field('content', struct(
field('title', string(255)),
optional(field('annotation', string(65535))),
field('text', string()),
optional(field('pages', nullable(uint(5000))))
))
)),
field('read', enum(false, true, 0, 1, 'yes', 'no'))
));
if (PHP_SAPI === 'cli') {
$input = [];
parse_str(implode('&', array_slice($argv, 1)), $input);
} else {
$input = $_GET;
}
echo "Validation: " . (is($input, type('input')) ? 'success' : 'failed') . "n";
Этот код проверяет корректность переданного описания элемента книжной серии:
- Обязательный параметр name должен быть строкой произвольной длины.
- Обязательный параметр authors должен быть строкой произвольной длины или массивом таких строк.
- Может быть передан параметр text, являющийся строкой произвольной длины, либо составной параметр content.
- Обязательный параметр read должен иметь одно из указанных значений.
Такой набор параметров будет валидным:
name="The Lord of the Rings"
authors[]="J. R. R. Tolkien"
content[title]="The Return of the King"
content[text]=...
read=yes
А такой не пройдёт проверку:
name="The Lord of the Rings"
authors[]="J. R. R. Tolkien"
text=...
content[title]="The Return of the King"
content[text]=...
read=yes
generics
Репозиторий на GitHub: https://github.com/perspectea/generics
Версия PHP: 7.0
Эта библиотека вводит некоторое подобие дженериков. Основными являются два вида объектов-массивов:
TeaGenericsIndexedArray(array $aValues = null, callable $aValueConstraintCallback = null);
Обычный массив с упорядоченными индексами. Для него может быть задано ограничение значений элементов — функция со следующей сигнатурой:
function ($aValue): bool;
TeaGenericsAssocArray(array $aValues = null, callable $aKeyConstraintCallback = null, callable $aValueConstraintCallback = null);
Ассоциативный массив. Для него аналогичным образом могут быть заданы ограничения значений ключей и элементов. Ключами ассоциативного массива могут быть любые значения, а не только целые числа и строки.
Так же определены следующие встроенные конструкторы (пространство имён Tea):
function values(...$aValues): IndexedArray;
Индексированный массив с произвольными значениями.
function numbers(float ...$aValues): NumericArray;
function integers(int ...$aValues): IntArray;
function cardinals(int ...$aValues): UIntArray
Индексированный массив чисел. Соответственно любых (float и int), целых (int) и беззнаковых целых (int >= 0).
function strings(string ...$aValues): StringArray
Индексированный массив строк.
function objects(string $aClass, array $aValues = null): ObjectArray;
Индексированный массив объектов заданного класса (интерфейса).
function map(array $aItems = null): AssocArray;
Ассоциативный массив с произвольными ключами и значениями.
function dict(array $aItems = null): Dictionary;
Ассоциативный массив со строковыми ключами и произвольными значениями.
function hash(array $aItems = null): StringDictionary;
Ассоциативный массив со строковыми ключами и значениями.
function collection(IType $aType, array $aValues = null): Collection;
Индексированный массив значений, соответствующих заданному типу (см. typedef).
Вместо заключения
Хотя всё это — в некоторой степени набор велосипедов, но я надеюсь, что он может кому-то пригодиться в работе. typedef может быть удобен для проверки параметров скрипта вместе с их преобразованием с помощью json_decode. А «дженерики» (хотя это не совсем дженерики в привычном понимании) могут пригодиться для ограничения типов массивов в аргументах с помощью уже готовых инструментов.
Можно было бы добавить типизированные свойства объектов, оформить библиотеки в виде расширений для улучшения производительности или сделать ещё что-нибудь необдуманное, но пока я не вижу в этом острой необходимости.
Так же я буду рад выслушать конструктивную критику и что-то улучшить в этих несложных инструментах или узнать про какой-нибудь silver-bullet, просвистевший мимо меня.
Благодарю за ваше внимание!
Автор: Алексей Максимов