Пользовательские типы в PHP

в 15:11, , рубрики: php, дженерики, массивы, типы данных

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

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

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

image

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. А «дженерики» (хотя это не совсем дженерики в привычном понимании) могут пригодиться для ограничения типов массивов в аргументах с помощью уже готовых инструментов.

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

image

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

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

Автор: Алексей Максимов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js