Pull to refresh

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

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

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

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

image

alexeymaximov/fulltype
alexeymaximov/containum

UPD 02.12.2018 — Публикация была обновлена для соответствия приведённой документации текущим версиям библиотек.

Fulltype


GitHub: alexeymaximov/fulltype

Эта библиотека предназначена непосредственно для работы с типами. Вы можете определить собственный тип данных с помощью функции ArsMnemonica\Fulltype\fulltype::define:

function define(string $aName, TypeInterface $aType): TypeInterface;

Вы можете как создать и инстанцировать собственный класс, реализующий интерфейс ArsMnemonica\Fulltype\TypeInterface, так и использовать встроенные.

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

function type(string $aName): TypeInterface;

Она принимает в качестве аргумента имя типа (аргумент aName функции define), и возвращает соответствующий объект.

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

function is($aValue, TypeInterface $aType): bool;

или методом validate самого объекта типа:

function TypeInterface::validate($aValue): bool;

Чтобы проверить значение на соответствие типу, вы также можете воспользоваться функцией ArsMnemonica\Fulltype\fulltype::assert:

function assert($aValue, TypeInterface $aType): mixed;

Эта функция возвращает переданное ей значение $aValue в случае успешной проверки, или выбрасывает исключение, унаследованное от TypeError, сообщение которого содержит описание переданного значения и место вызова функции assert.

Определены следующие встроенные типы (пространство имён ArsMnemonica\Fulltype и класс fulltype):

function bool(): BooleanType;

Логическое значение true/false.

function number(float $aMin = null, float $aMax = null): NumericType;
function int(int $aMin = null, int $aMax = null): IntegerType;
function cardinal(int $aMax = null): CardinalType;

Числовые типы.

Тип NumericType соответствует PHP-типам int и float.

Являющийся его наследником тип IntegerType соответствует только PHP-типу int.

Оба типа могут быть ограничены минимальным и максимальным значениями.

Тип CardinalType, являющийся наследником IntType, соответствует количественным числам (целым числам без знака) — его минимальным значением является 0, а максимальное может быть определено.

function string(int $aLength = null): StringType;

Строковый тип, может быть ограничен по максимальной длине.

function regexp(string $aRegularExpression): RegularExpressionType;

Регулярное выражение.

function enum(...$aValues): EnumerableType;

Перечислимый тип.

Ограничивает множество допустимых значений заданным набором.

function object(string $aBase = null): ObjectType;

Объектный тип.

Значение может быть только объектом заданного класса (интерфейсы так же допустимы).

function null(TypeInterface $aType): NullableType;

Nullable-тип.

Дополняет множество допустимых значений дочернего типа значением null.

function array(int $aLength = null): ArrayType;

Массивный тип (ключевое слово array не допустимо в качестве имени функции), может быть ограничен по максимальной длине.

Значение может быть:
— массивом;
— объектом, реализующим интерфейсы ArrayAccess, Countable и Traversable (должен быть вызван метод objective).

Чтобы задать допустимый тип значений массива, используйте метод of(TypeInterface), а для ключей используйте метод by(TypeInterface). Если вы зададите тип ключей, отличный от PHP-типов int и string, массивный тип будет иметь смысл только в отношении объектов, поскольку у массивов PHP не может быть ключей других типов.

function any(TypeInterface ...$aTypes): MultipleType;

Структурный тип, требующий соответствия переданного для валидации значения хотя бы одному из дочерних типов.

function union(TypeInterface ...$aTypes): ExclusiveType;

Структурный тип, требующий соответствия переданного для валидации значения строго одному из дочерних типов.

function struct(TypeInterface ...$aTypes): ComplexType;

Структурный тип, требующий соответствия переданного для валидации значения всем дочерним типам.

Для данного типа имеет смысл задавать следующие дочерние типы:

function key(string $aName, TypeInterface $aType = null): PropertyType

Тип-ключ, требующее от переданного для валидации значения быть массивом, содержащим строковый ключ указанного типа.

function offset(string $aName, TypeInterface $aType = null): PropertyType

Тип-смещение, требующее от переданного для валидации значения быть массивом или объектом, реализующим интерфейс ArrayAccess, содержащим строковое смещение указанного типа.

function property(string $aName, TypeInterface $aType = null): PropertyType

Тип-свойство, требующее от переданного для валидации значения быть объектом, содержащим свойство указанного типа.

Если для указанных выше типов вызван метод optional, то соответствующая проверка допускает отсутствие такого ключа/смещения/свойства.

Для наглядной демонстрации работы библиотеки рассмотрим следующий пример:

use ArsMnemonica\Fulltype\fulltype as t;

const input_t = 'input';

t::define(input_t, t::struct(
	t::key('name', t::string()),
	t::key('authors', t::any(
		t::string(),
		t::array()->of(t::string())
	)),
	t::union(
		t::key('text', t::string()),
		t::key('content', t::struct(
			t::key('title', t::string(255)),
			t::key✱('annotation', t::string(65535)),
			t::key('text', t::string()),
			t::key✱('pages', t::cardinal?(5000))
		))
	),
	t::key('status', t::enum('WILL_READ', 'READ', 'FAVORITE_BOOK'))
));

echo "Processing input...\n";
if (PHP_SAPI === 'cli') {
	$input = [];
	parse_str(implode('&', array_slice($argv, 1)), $input);
} else {
	$input = $_GET;
}
foreach ($input as $key => $value) {
	echo "$key: " . json_encode($value) . "\n";
}
echo "Validation: " . (t::is($input, t::type(input_t)) ? 'success' : 'failed') . PHP_EOL;

Этот код проверяет корректность переданного описания элемента книжной серии:

  • Обязательный параметр name должен быть строкой произвольной длины.
  • Обязательный параметр authors должен быть строкой произвольной длины или массивом таких строк.
  • Может быть передан параметр text, являющийся строкой произвольной длины, либо составной параметр content.
  • Обязательный параметр status должен иметь одно из указанных значений.

Такой набор параметров будет валидным:

name="The Lord of the Rings"
authors[]="J. R. R. Tolkien"
content[title]="The Return of the King"
content[text]=...
status=READ

А такой не пройдёт проверку:

name="The Lord of the Rings"
authors[]="J. R. R. Tolkien"
text=...
content[title]="The Return of the King"
content[text]=...
status=READ


Вместо заключения


Хотя всё это — в некоторой степени набор велосипедов, но я надеюсь, что он может кому-то пригодиться в работе. typedef может быть удобен для проверки параметров скрипта вместе с их преобразованием с помощью json_decode. А контейнеры могут пригодиться для ограничения типов массивов в аргументах с помощью уже готовых инструментов.

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

image

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

Благодарю за ваше внимание!
Tags:
Hubs:
+25
Comments 54
Comments Comments 54

Articles