[recovery mode] Пользовательские типы в PHP

Habrahabr

В отношении данных, которые программа получает извне, принято следовать правилу trustno1. Это справедливо не только в отношении данных, получаемых непосредственно от пользователя, но и в отношении данных, которые передаёт в подпрограммы клиентский код.

PHP 7 оснащён расширенной системой контроля типов аргументов, включающей не только классы, но и скаляры. Однако в том, что касается сложных структур данных, ничего не изменилось — для них существует единственный тип array, который в PHP может содержать всё, что угодно.

Я надеюсь, что новые версии PHP исправят ситуацию. А на данный момент я хочу поделиться с сообществом некоторыми своими наработками в этой области:

image

perspectea/typedef perspectea/generics

typedef

Репозиторий на GitHub: https://github.com/perspectea/typedef Версия PHP: 7.0

Эта библиотека предназначена непосредственно для работы с типами.

Вы можете определить собственный тип данных с помощью функции Tea\typedef:

function typedef(string $aName, IType $aType): IType;
Вы можете как создать и инстанцировать собственный класс, реализующий интерфейс Tea\Typedef\IType, так и использовать встроенные.

Для обращения к типу предназначена функция Tea\type:

function type(string $aName): IType;
Она принимает в качестве аргумента имя типа (аргумент aName функции typedef), и возвращает соответствующий объект.

Чтобы проверить значение на соответствие типу, воспользуйтесь функцией Tea\is:

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.

Членами структурного типа являются поля — объекты класса, реализующего интерфейс Tea\Typedef\IField. Переданное для валидации значение является валидным, если оно является массивом или объектом с массивным доступом (в соответствии с дополнительными ограничениями) и проходит валидацию всех полей.

Определены следующие встроенные виды полей:

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

Эта библиотека вводит некоторое подобие дженериков. Основными являются два вида объектов-массивов:

Tea\Generics\IndexedArray(array $aValues = null, callable $aValueConstraintCallback = null);
Обычный массив с упорядоченными индексами. Для него может быть задано ограничение значений элементов — функция со следующей сигнатурой:
function ($aValue): bool;
Tea\Generics\AssocArray(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. А «дженерики» (хотя это не совсем дженерики в привычном понимании) могут пригодиться для ограничения типов массивов в аргументах с помощью уже готовых инструментов.

Можно было бы добавить типизированные свойства объектов, оформить библиотеки в виде расширений для улучшения производительности или сделать ещё что-нибудь необдуманное, но пока я не вижу в этом острой необходимости.

image

Так же я буду рад выслушать конструктивную критику и что-то улучшить в этих несложных инструментах или узнать про какой-нибудь silver-bullet, просвистевший мимо меня.

Благодарю за ваше внимание!