PHP 7 оснащён расширенной системой контроля типов аргументов, включающей не только классы, но и скаляры. Однако в том, что касается сложных структур данных, ничего не изменилось — для них существует единственный тип array, который в PHP может содержать всё, что угодно.
Я надеюсь, что новые версии PHP исправят ситуацию. А на данный момент я хочу поделиться с сообществом некоторыми своими наработками в этой области:
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. А «дженерики» (хотя это не совсем дженерики в привычном понимании) могут пригодиться для ограничения типов массивов в аргументах с помощью уже готовых инструментов.
Можно было бы добавить типизированные свойства объектов, оформить библиотеки в виде расширений для улучшения производительности или сделать ещё что-нибудь необдуманное, но пока я не вижу в этом острой необходимости.
Так же я буду рад выслушать конструктивную критику и что-то улучшить в этих несложных инструментах или узнать про какой-нибудь silver-bullet, просвистевший мимо меня.
Благодарю за ваше внимание!
Комментарии (45)
c4boomb
19.06.2017 10:55+1enum(false, true, 0, 1, 'yes', 'no')
Костылируем свой код на ходу :-)
Так и не понял зачем.altgamer
19.06.2017 20:52Согласен, пример получился достаточно дурной) Лучше бы подошло что-то типа:
field('state', enum('WILL_READ', 'READ', 'FAVORITE_BOOK'));
Этот тип — аналог literal type.
Sersoftin
19.06.2017 20:45+1Интерфейсы не мешало бы по PSR именновать. IType — TypeInterface, IField — FieldInterface.
altgamer
19.06.2017 21:37Согласен, для публичного кода это недоработка. Исправлю при первой возможности.
miksir
21.06.2017 22:47А PSR разве регулирует наличие/отсутствие типа класса в названии класса?
Такие требования, насколько я помню, есть только в разделе о том "как следует писать текст PSR".
kraso4niy
19.06.2017 20:45Silver bullet — ооп, паттерны. Посмотрите на symfony3+. То ято вы описали это какой то си путь в пхп.
bm13kk
19.06.2017 20:45+1То, что Вы используете венгерскую нотацию — самодостаточный аргумент запретить использовать ваш код для всего, связанного с типами.
rjhdby
19.06.2017 20:45Насколько я понимаю, область применения довольно узка и, например, операция сложения с двумя переменным типа NumericType невозможна. Было бы интересно посмотреть пример реального использования (вы же не ради любви к искусству писали, а для решения какой-то конкретной задачи?)
altgamer
19.06.2017 21:30Переменные, соответствующие типу NumericType — это же просто числа, их вполне можно сложить)
Возможное реальное применение библиотеки typdef — валидация параметров методов API. Даже если данные приходят не в JSON (то есть не читаются из php://input), их всегда можно такими сделать например так (просто набросок кода):
function parse_arg($aValue) { if (is_array($aValue)) { foreach ($aValue as &$value) { $value = parse_arg($value); } return $aValue; } if (is_string($aValue)) { $result = json_decode($aValue, true); if (json_last_error() !== JSON_ERROR_NONE) { $result = json_decode('"' . $aValue . '"'); if (json_last_error() !== JSON_ERROR_NONE) { $result = $aValue; } } return $result; } return $aValue; }
Дальше эти данные можно пропускать через валидатор, где числа определять как числа, массивы — как массивы и так далее. Точно так же в целях отладки можно проконтролировать и вывод метода.
Zeratyll
19.06.2017 20:45Надеюсь когда нибудь PHP получит строгую типизацию
miksir
21.06.2017 22:49А это вопрос определения
строгойсильной типизации.
В 1974 году Лисков и Зиллес назвали сильно типизированными те языки, в которых «при передаче объекта из вызывающей функции в вызываемую тип этого объекта должен быть совместим с типом, определённым в вызываемой функции»
Что достигается тайпхинтингом и strict режимом. Революция свершилась? ;)
miksir
19.06.2017 20:53+1В PHP есть свои "пользовательские типы", называются — классы. Разница между классом и структурой расплывчата и зависит от языка программирования. Т.е. не вижу никаких проблем использовать в PHP класс как структуру с воссозданием ее из массива или строки через конструктор (обычный или именованный). А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP. Пока причин хоть пытаться использовать ваш вариант typedef('input', по сравнению с Input::createFromArray($array) (боже упаси меня класс назвать Input) не вижу, что я не заметил?
Называть то, что вы придумали "пользовательскими типами" очень смело. Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.
altgamer
19.06.2017 21:19Насчёт сериализатора не могу согласиться просто потому что такой функциональности в библиотеке нет)
А в отношении классов — вполне возможно, как раз для этого и сделаны «дженерики» (или что это вообще). Назначением же typedef я полагаю как раз отсутствие необходимости всякий раз описывать сложную валидацию на чистом PHP. Тем более что даже в этом случае вместо чистого PHP рано или поздно появятся некие «стандартные правила», которые порой принимают форму "(int|string)[7]?". Я просто предложил уже готовый набор правил — в отличие от метаязыков ими довольно удобно пользоваться в IDE.oxidmod
19.06.2017 21:57А чем вам эти типы не угодили?
altgamer
19.06.2017 22:16Это расширение PECL не поставляется вместе с PHP.
Тем не менее, это тоже инструмент. Хотя, честно говоря, особого смысла в нём в PHP 7 я не вижу. Так-то в расширениях можно много интересного сделать, даже перегрузку операторов для объектов.
Для typedef я предполагал использование для валидации входных данных на уровне скрипта, а не отдельной функции. Для валидации сложных структур я удовлетворившего меня инструмента не нашёл, потому и решил сделать этот.
VolCh
21.06.2017 11:52А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP.
В целом, чаще всего на чистом PHP реализуют валидацию в конструкторах/сеттерах в императивной манере, что часто приводит к потере читаемости. В целом декларативный стиль описания правил более удобен на сложной логике.
Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.
Скорее это чистой воды валидатор. В принципе ничего вроде не мешает его использованию в конструкторе или сеттерах. Главное есть ли преимущества по сравнению с более популярными валидаторами.
miksir
21.06.2017 16:53В целом декларативный стиль описания правил более удобен на сложной логике.
Ну, готовых решений валидаторов с декларативным описанием правил, которые можно использовать в объекте — куча, так что стиль не преграда.
Хотя мне кажется, как раз на сложной логике декларативный стиль менее удобен. Декларативное хорошо читается на простых правилах, а как появляется сложная, кастомная для этого типа логика (типа, если поле1=5, то поле2 — строка, а если поле1=6, то поле2 — инт, или что-то еще сложнее), то нам придется куда-то выносить эту логику, что бы создать декларативное правило. Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.
Скорее это чистой воды валидатор. В принципе ничего вроде не мешает его использованию в конструкторе или сеттерах. Главное есть ли преимущества по сравнению с более популярными валидаторами.
А, точно, я почему-то решил, что там потом рождаются объекты. Да, валидатор вложенных массивов ;) В общем, не просто валидатор, а описатель и валидатор схемы. Сразу находятся подобные готовые решения, причем, с более лаконичным описанием правил в виде массива. Хотя я бы просто использовал json schema и библиотеки их валидации.
altgamer
21.06.2017 21:57Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.
Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать. А вообще всё так и работает) Тип — это объект, который реализует метод TypeInterface::validate. Наследование от AbstractType ещё и делает его callable, что позволяет передать его, допустим, в array_filter.
с более лаконичным описанием правил в виде массива
Я встречал разные реализации, но в более лаконичном описании (если я правильно вас понял), мне не нравится как раз то, что оно вводит пусть минимальный, но метаязык. Я имею в виду что-то вроде
['id' => 'int|string', 'data*' => 'string|array']
В случае с реализацией в форме обычного кода есть возможность полагаться на IDE, которая подскажет и названия типов, и их сигнатуры. Кроме того, описание чего-то вроде
['id' => 'int|string', 'data*' => "string|['head1' => 'string', 'head2' => 'string', 'image' => '[\"url\" => \"string\", \"width\" => \"int\", \"height\" => \"int\"]']"]
просто не представляется возможным без дополнительных ухищрений вроде парсинга вложенного php-кода, да и читаемость так себе.
Извините, если неправильно понял, что вы имели в виду.altgamer
21.06.2017 22:24Хотя я бы просто использовал json schema и библиотеки их валидации.
Схемы — хороший инструмент. Но это отдельный язык со своей спецификацией, чисто декларативный и к PHP никакого отношения не имеющий (по мне ещё и несколько многословный, но это вопрос предпочтений).
miksir
21.06.2017 22:45Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать.
Когда нужно — тогда и будет. Я о том и говорю, что декларативный подход следует комбинировать с императивным. Что-то вроде
class User { public function __construnct(string $name, array $addresses) { Assertion::regex($name, '/.../', sprintf('Username is not valid: %s', $name)); $this->addresses = new AddressCollection($addresses); $this->checkNameForCorrectAddress($name, $addresses); $this->name = $name; } private function checkNameForCorrectAddress(string $name, AddressCollection $addresses) { if ($addresses->hasAddress('Иваново') && $name !== 'Иванов') { throw new InvalidArgumentException("В Иваново могут проживать только Ивановы"); } } }
А вообще всё так и работает) Тип — это объект, который реализует метод TypeInterface::validate. Наследование от AbstractType ещё и делает его callable, что позволяет передать его, допустим, в array_filter.
Тип у вас — объект описывающий правила валидации иерархически вложенные друг в друга, правильно? С помощью ваших "типов" вы можете сказать — вот этот массив — он вообще совпадает с описанием структуры или нет. О том, что вам каждый раз, когда нужно проверить соответствие типу — придется проходить по всему массиву я уж молчу.
Я же говорил о том, что типы в ПХП уже есть — это классы. Между классом и вашим "типом" есть одна огромная разница — создание объектов, которые могут проверять валидность создания себя, а по-этому не должны быть в "невалидном" состоянии. Раз создали объект — значит он в валидном состоянии. А уж о тайпхинтенге и проверке instanceof я уж и молчу.
Т.е. получается, что вы создали что-то, для того, что в принципе удобнее делать иным способом. Хотя, возможно, в области валидации структуры массива без создания "типов" ваша библиотека и может быть полезна, но вот сравнивать ее с "типами" — просто всех путать.
В случае с реализацией в форме обычного кода есть возможность полагаться на IDE, которая подскажет и названия типов, и их сигнатуры. Кроме того, описание чего-то вроде
Ну, есть валидатор описания структуры, который скажет что "неизвестный тип данных strrrng", или можно писать типа 'data' => type::string. '|'. type::int. Не говоря уже о том, что 'data' => 'string|object' вообще быть не должно, это такой большой подводный камень. А так — гляньте ту же JSON Schema спецификацию.
altgamer
22.06.2017 00:12О том, что вам каждый раз, когда нужно проверить соответствие типу — придется проходить по всему массиву я уж молчу.
Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды. Я не могу представить, зачем может понадобиться проверять тип одних и тех же данных несколько раз.
Между классом и вашим «типом» есть одна огромная разница — создание объектов, которые могут проверять валидность создания себя, а по-этому не должны быть в «невалидном» состоянии.
Вообще говоря, я не вижу никаких противоречий между использованием классов и валидацией данных. В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области. Это в любом случае так — если тип не проверит программист, то его рано или поздно проверит сам PHP и отреагирует на ошибки по своему усмотрению (увы, до сих пор не все ошибки являются исключениями).
Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно. Кто-то рано или поздно должен будет задаться вопросом, что же там содержится. Чем глубже это будет происходить по иерархии вызовов, тем дальше от места действительной ошибки на неё возникнет какая-то реакция (ещё хуже, если это произойдёт вообще где-то дальше по коду или, тем более, останется незамеченным).
Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.
По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.
Ну, есть валидатор описания структуры, который скажет что «неизвестный тип данных strrrng», или можно писать типа 'data' => type::string. '|'. type::int.
Но этот валидатор, скорее всего, не сможет вывести подсказку в IDE, а сообщит информацию в исключении. Так же для него, вероятно, не работает автодополнение.
Не говоря уже о том, что 'data' => 'string|object' вообще быть не должно, это такой большой подводный камень.
Пусть будет 'null|object')miksir
22.06.2017 00:59Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды.
Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает array.
В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области
А это на самом деле одно и тоже, и в этом и есть прелесть. Ну да, проверяю. Но разница между вашим подходом и объектом в том, что у вас после проверки массива на тип — остается массив, а в случае объекта — остается переменная определенного типа (класса). Т.е. разница как между is_int($a) и $a = (int)"1";
Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно.
Нет, конструктор является методом создания объекта. Пока конструктор не отработал — объект не создан. Т.е. в моем случае тип User не просто содержит string $name, а $name в особом формате описанном регекспом. А адреса — не просто адреса, а коллекция AddressCollection состоящая из объектов типа Address, каждый из которых — так же валиден (т.е. проверил валидность адреса). А любая ошибка приводит к исключению.
Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.
Ну будет исключение, и что такое? Словим, выдадим красивую ошибочку. Если очень нужно что-то кастомное — валидаторов и десиализаторов куча. Промежуточный объект формы займется этим.
По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.
Э… а то, что в него в поле, которое должно быть строкой — пытаются засунуть массив, это не нарушение правильности его состояния? ;) К тому же, кроме объектов — сущностей существуют еще и объекты — ValueObject.
Но этот валидатор, скорее всего, не сможет вывести подсказку в IDE, а сообщит информацию в исключении. Так же для него, вероятно, не работает автодополнение.
С чего бы. type::string — свойство класса type, все будет автокомплит. В вашем случае создав "тип" input вы так же можете ошибиться в проверке is ($val, type('input')) в слове input и отловить это только в рантайме
altgamer
22.06.2017 22:30Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает array.
Если вы выполняете разные проверки на разных уровнях, то и содержимое массива (и его подмассивов любого уровня) имеет смысл выполнять там, где к нему осуществляется доступ, а на более высоких уровнях в таком случае достаточно просто знать, что это массив, чтобы не попасть на ошибку типа аргумента. Что-то вроде типизированного массива в PHP можно сделать например так (у этого способа есть свои ограничения):
function sum(int ...$aValues): int; sum(...$values);
Другой вопрос, что я бы не разделял проверки на множество уровней, да и массивы имеет смысл использовать в качестве аргументов с осторожностью, потому что их предполагаемое содержимое абсолютно не очевидно из сигнатуры метода (как минимум, оно должно быть подробно описано в PHPDoc).
Хотя вы мне подали идею — кэшировать результат проверки с использованием какого-то дайджеста проверяемых данных) Спасибо!
А это на самом деле одно и тоже, и в этом и есть прелесть.
Это совсем не одно и то же. Есть интерфейс (имя — это строка) и его реализация (Ивановы живут в Иванове). Через какое-то время в Иванове разрешат жить Петровым, но имя всё ещё не сможет быть числом. А вот если мы начнём работать так же с больными шизофренией, то имя вполне может стать массивом.
Можно сказать, что тип — это охранник на входе, который определяет, кому вообще можно зайти, а кого надо выпроводить взашей, чтобы не мешал работать и не портил обстановку. Иначе будет проходной двор и учителю танцев с тонкой душевной организацией сломает ноги случайно залетевший погреться амбал.
Т.е. разница как между is_int($a) и $a = (int)«1»;
Вернее так — is_int($a) и $a = (int)$a. Если $a и так int — зачем выполнять преобразование? А если $a содержит строку, то как преобразовать «один» в число? Мы получим 0, хотя нам его никто не передавал, и продолжим себе работать, как будто так и надо. Чтобы этого не случилось, нам нужно использовать что-то вроде ctype_digit($a), то есть выполнить другую проверку, а потом ещё и выполнить преобразование. Не лучше ли сразу отказаться от заказа, если мы просили торт, а нам принесли семечки?
А любая ошибка приводит к исключению.
Когда понадобится писать документацию, исключения придётся собирать по всей кодовой базе. Не говоря уже о том, что это «exception driven development».
Ну будет исключение, и что такое? Словим, выдадим красивую ошибочку.
Вы получите что-то типа
Argument 1 passed to FUNCTION() must be of the type string, integer given, called in FILE on line LINE
а то, что в него в поле, которое должно быть строкой — пытаются засунуть массив, это не нарушение правильности его состояния?
Это нарушение интерфейса, то есть ошибка вышестоящего кода — до самого объекта дело ещё не дошло. Тут есть две проблемы:
- Если метод принимает всё что угодно, но работает с чем-то одним, а на остальное реагирует кастомными ошибками, интерфейс метода не полон.
- Если метод выносит всю свою логику в интерфейс, то его интерфейс перегружен, и сделать другую реализацию, соответствующую ему, невозможно или по крайней мере это является бессмысленным.
Для решения этих проблем и нужны стандартизированные проверки средствами самого языка, а если их нет — хотя бы какими-то стандартизированными средствами, о которых могут знать обе стороны.
С чего бы. type::string — свойство класса type, все будет автокомплит. В вашем случае создав «тип» input вы так же можете ошибиться в проверке is ($val, type('input')) в слове input и отловить это только в рантайме
Строковые названия типов — это скорее вспомогательное средство, и конечно они должны быть константами. Но вообще кастомный тип можно создать напрямую, если лень сделать для него «синтаксическую» функцию.
В случае с type::string. '|'. type::int, во-первых, такой синтаксис совершенно не читаем, и во-вторых, легко ошибиться вот так: type::string | type::int. Сами «операторы» такого синтаксиса автокомплиту не поддаются, если только IDE или её плагин не поддерживают именно этот валидатор.miksir
22.06.2017 23:47Хотя вы мне подали идею — кэшировать результат проверки с использованием какого-то дайджеста проверяемых данных) Спасибо!
Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.
Это совсем не одно и то же. Есть интерфейс (имя — это строка) и его реализация (Ивановы живут в Иванове). Через какое-то время в Иванове разрешат жить Петровым, но имя всё ещё не сможет быть числом.
Попробую еще раз. Речь о том, что как только был создан объект типа User, мы можем везде где нужно проверять переменные на тип User. Еще раз класс User — это тип, т.е. это описание структуры + валидация структуры. Объект класса User — это уже данные типа User. При создании объекта класса нет никакой разницы, почему провалилась проверка типа и объект был не создан — потому-что тайпхинт не пропустил аргумент в конструктор или внутри конструктора не прошла проверка.
Если метод принимает всё что угодно, но работает с чем-то одним, а на остальное реагирует кастомными ошибками, интерфейс метода не полон.
Ну, вы вообще в курсе, что конструктор не входит в понятие интерфейса класса? Или у вас к понятию "интерфейс" какое-то свое оригинальное определение?
Argument 1 passed to FUNCTION() must be of the type string, integer given, called in FILE on line LINE
Нынче это прекрасно ловится.
<?php declare(strict_types=1); class A { public function __construct(string $a) {} } try { $b = new A(1); } catch (TypeError $e) { echo "ой ой, передали нам что-то не то"; }
Когда понадобится писать документацию, исключения придётся собирать по всей кодовой базе. Не говоря уже о том, что это «exception driven development».
phpdoc, IDE, ну и руки с головой для аккуратного использования исключений, и все будет замечательно
altgamer
23.06.2017 00:59Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.
Если с данными что-то произошло, то это очевидно уже другие данные с другим дайджестом)
Речь о том, что как только был создан объект типа User, мы можем везде где нужно проверять переменные на тип User. Еще раз класс User — это тип, т.е. это описание структуры + валидация структуры.
User — это класс, объекты которого инстанцируются с использованием некоторых исходных данных, а не сами эти данные. То есть исходные данные нуждаются в проверке в любом случае, просто вы либо будете каждый раз писать кастомные проверки, либо использовать какой-то стандарт (не обязательно именно typedef — просто он должен быть). Как вы дальше будете использовать полученный объект — это уже не вопрос валидации. Вы, возможно, неправильно поняли идею — я не предлагаю валидировать данные внутри каждой функции, в этом нет смысла.
При создании объекта класса нет никакой разницы, почему провалилась проверка типа и объект был не создан — потому-что тайпхинт не пропустил аргумент в конструктор или внутри конструктора не прошла проверка.
Нет никакой разницы для программиста, но не для пользователя, который передал данные.
Ну, вы вообще в курсе, что конструктор не входит в понятие интерфейса класса? Или у вас к понятию «интерфейс» какое-то свое оригинальное определение?
Интерфейс — это несколько более широкое понятие, нежели «интерфейс класса») Интерфейс метода — это его сигнатура, а именно принимаемые аргументы, возвращаемый тип и выбрасываемые исключения. То есть всё то, что вы описываете в PHPDoc.
echo «ой ой, передали нам что-то не то»;
Важно ответить на вопрос, что именно не то и куда передали. В случае с вложенной структурой придётся оборачивать в try-catch каждый вызов конструктора на каждом уровне вложенности — и даже это не поможет понять, передали неправильно $name или $addresses, если вы не будете парсить сообщение об ошибке и сопоставлять номера аргументов. А ещё мы можем поймать не только TypeError, но и другие типы исключений, и с каждым надо как-то работать.
VolCh
21.06.2017 12:00С телефона всё не осмыслил, два вопроса:
- есть ли возможность валидировать обычные объекты, в том числе stdClass хотя бы на уровне public свойств? Как с вложенностью объектов?
- есть ли возможность задавать кастомные валидаторы (Callable), в том числе с возможностью валидировать по сложной логике несколько свойств, типа "два свойства должны быть равны" или "одно, и только одно из списка свойств должно быть не null"?
altgamer
21.06.2017 21:111. Сейчас валидировать объекты можно только если они реализуют интерфейсы ArrayAccess, Countable и Traversable. По умолчанию все массивные типы (ArrayType, StructType и Union) допускают и массивы, и такие объекты, но это поведение можно ограничить. В следующем релизе (0.0.3) я планирую дать структурам возможность принимать объекты только с интерфейсом ArrayAccess — Countable и Traversable им вроде бы ни к чему.
Насчёт возможности валидировать объекты без интерфейсов вообще просто по public-свойствам я не уверен — можете привести пример, где это может быть полезно? Просто я не сторонник использования объектов в качестве словарей.
Вложенность массивов и объектов-массивов допускается любая.
2. Все типы — это объекты, реализующие интерфейс TypeInterface. Стандартные типы так же являются наследниками AbstractType (он как раз делает их callable). Поля (field, optional и union) реализуют интерфейс FieldInterface. Каждому полю для валидации передаётся целиком весь массив/объект, который передан для валидации в родительскую структуру, так что можно в рамках поля валидировать любые сочетания ключей и их значений — структурный тип по сути является объединением полей по логике «И».
То есть расширение логики вполне доступно. А функции всего лишь выполняют роль «синтаксиса» — можно определить такие же функции для кастомных типов и полей.
Для правила «одно, и только одно из списка свойств должно быть не null» уже предусмотрен стандартный тип union — он может выступать и как самостоятельный тип, и как поле структурного типа. С небольшой оговоркой — остальные поля объединения должны быть не null, а вообще отсутствовать.
Для случая «два свойства должны быть равны» стандартной логики не предусмотрено)
ghost404
27.06.2017 11:02- Это не типизация, а валидация чистой воды
- uint это число >=0. uint вполне может быть интервал [2,5).
function number(float $aMin = null, float $aMax = null): NumericType; function int(int $aMin = null, int $aMax = null): IntType; function uint(int $aMax = null): UIntType;
- Ограничение
$aMin
,$aMax
и$aLength
нужно делать отдельным валидатором. Это нарушение SRP. - Аргумент у
object()
то же самое. Нарушение SRP. struct()
спорный валидатор. По мне так лучше проверять через объект.
Вообще, готовых валидатор великое множество. Ваш интересен тем что позволяет валидировать многомерные массивы. Хотя и для этого полно других инструментов.
Мне больше нравится валидатор Symfony. Простое описание правил и удобно использовать в middleware. Для многомерных массивов строю композит из DTO+payload.
altgamer
27.06.2017 22:57Это не типизация, а валидация чистой воды
Да — в какой-то степени) Тип определяется допустимым множеством значений и операциями, которые над ними допустимы. Валидация же — это проверка того, что значение соответствует конкретным условиям. То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация. В первом случае мы знаем, что можем сохранить значение на участке памяти размером (допустим) два байта и не можем прибавить его к числу без дополнительных преобразований. Во втором — мы уверены, что будучи сохранённым в базу данных, значение не нарушит её целостность.
Исходя из задач, для которых я сам предполагал использовать библиотеку typdef, я выбрал термин «типы». Но «валидация» здесь может оказаться так же подходящим термином, зависит от использования. Это справедливо и для «дженериков».
uint это число >=0. uint вполне может быть интервал [2,5).
uint — это беззнаковое целое, это его основной признак. Если речь идёт о некоем целом диапазоне, то логичнее использовать int — ведь диапазон может сместиться, в том числе в отрицательные величины. Включающие и исключающие диапазоны я решил не разделять, поскольку это решается простым прибавлением/вычитанием единицы. А для действительных чисел операция сравнения вообще плохо применима.
Ограничение $aMin, $aMax и $aLength нужно делать отдельным валидатором. Это нарушение SRP.
SRP — это принцип, а не парадигма, то есть им нужно руководствоваться, но чётких инструкций нет.
Валидаторы Min и Max не имеют смысла вне контекста чисел и будут выполнять ровно те же проверки — нативного типа и диапазона. Но если продолжать следовать принципу единственности ответственности, то их разновидностей будет достаточно много и это будет только путать.
Валидатор Length вообще не может существовать, поскольку длину имеют строки и массивы, но они являются принципиально разными сущностями — строка это конечно тоже массив, но не в PHP) Если совместить, то это нарушение SRP в чистом виде. Если сделать интерфейс LengthyTypeInterface с методом validateLength, то это ничем не будет отличаться от текущей реализации, кроме наличия дополнительной сущности. Если сделать Length только для строк, то это будет несправедливо по отношению к массивам).
Так, как вы описываете, реализован валидатор в симфони. Но у него немного иные задачи, чем предполагал для библиотеки я.
Аргумент у object() то же самое. Нарушение SRP.
ObjectType без класса — это скорее костыль, недоработка в этом)
struct() спорный валидатор. По мне так лучше проверять через объект.
StructuralType предназначен для валидации не только массивов, но и объектов с ArrayAccess. Публичные свойства объектов, на мой взгляд, плохая практика потому что нарушают инкапсуляцию. Хотя могу согласиться со спорностью в силу его непривычности и, следовательно, неочевидности. Просто мне такая реализация кажется удобной — буду признателен за примеры других вариантов.ghost404
28.06.2017 11:30Да — в какой-то степени)
Проектировали как типизацию, а сделали валидацию.
Обычно, запросы от пользователя трансформируют в DTO, его уже валидируют и перенаправляют на доменный уровень.
То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация.
Неправильно.
- Строка — это простой тип;
- Строка длинною 2 символа — это валидация;
- Код страны — это тип предметной области (ValueObject);
- Код страны, в которой у нашей кампании есть филиал — это валидация уровня предметной области.
Если речь идёт о некоем целом диапазоне, то логичнее использовать int
То есть, если я не знаю точно может ли число ровняться 0, но точно знаю что оно не отрицательно, то я должен использовать
int
, вместоuint
. Не кажется ли вам это нелогичным?
Вообще
uint
теряет свой смысл так как эквивалентенint(0)
.
то их разновидностей будет достаточно много и это будет только путать.
Так в этом весь смысл. Много простых, логичных и легко запоминающихся валидатор которые удобно комбинировать. В вашем же случае сходу возникают проблемы:
- Какой порядок у аргументов функции?
Он конечно логичен, но все равно возникает желание перепроверить перед написанием кода. - Что, если нужно ограничить только верхнюю планку величины числа? Каждый раз передавать
null
первым аргументом?
Указывать минимальный размер числа нужно гораздо реже. Если сменить порядок аргументов, то мы возвращаемся к проблеме №1
Валидаторы Min и Max не имеют смысла вне контекста чисел и будут выполнять ровно те же проверки
А для этого у нас есть type hinting в php 7. Конечно в этом случае нельзя ограничить размер
floot
, но это и не надо на мой взгляд, так как в этом случае еще может возникнуть необходимость ограничить количество знаков после запятой, а это уже совсем другая задача.
- int — эквивалент
is_int()
; - uint — эквивалент
is_int()
+>=0
; - min — эквивалент:
function(int $int, int $min) { return $int >= $min; }
длину имеют строки и массивы
Ну так, для строки можно использовать
length()
, а для массивовsize()
, по аналогии с функциями из php.
ObjectType без класса — это скорее костыль, недоработка в этом)
Так тоже легко решается.
object()
— эквивалентis_object()
;instanceof()
— эквивалентinstanceof
. Если будет передан не объект, получимfalse
.
oxidmod
Прочел с вечера, не проникся.
Прочел повторно сутра и все равно не проникся.
Зачем???
Nahrimet
Тоже вот жду комментариев тех, кто шарит.
tmin10
Тоже прочитал статью, но не понял, чем в таком случе плохо использование обычных классов с геттерами и сеттерами, которые уже сами внутри себя смогут проверить входящие типы. Возможно стоит вынести проверки типизации в отдельный класс и использовать его…
altgamer
Я предполагал использование typedef для валидации входных данных на уровне скрипта, а не отдельных функций — на этом уровне у PHP уже есть система типов, которая расширяется как раз классами (пример — «дженерики», и я уже ненавижу это название).
oxidmod
Ну вот же
Можно кодом, можно аннотациями.
Богатая коллекция проверок из коробки
Легко добавляются кастомные в виде колбека (для разовых проверок) или собственных классов-валидаторов
В чем преимущество вашего подхода?
altgamer
Для меня минусов в данном случае два:
Но вообще я конечно не возьмусь спорить с компонентами симфони)
oxidmod
1. Их два, потому что один содержит правило, а второй проверяется соотвествие значения правилу. Это просто SRP. Избыточно ли? Не думаю. Для большинства случаев есть готовые из коробки валидаторы или просто на гитхабе написаны кемто до вас и вам писать ничео не придется. Их слишком много? So what? Экономите пару килобайт на диске?
2. С каких пор необходимость знать и уметь проблема? Тем более для ИДЕ есть плагины супортящие эти компоненты. ИДЕ все подскажет. Не хотите совсем в аннотации? Так можно же и в код)
зы. Во всяких джаво-шарпах аннотации во всю юзаются) и это круто как по мне.
altgamer
1. Дело не в количестве готовых правил — это как раз отлично — а в сложности реализации. Я не могу применить правило к данным — для этого мне нужен валидатор. Я не могу просто реализовать класс правила — для этого мне нужно реализовать ещё и класс проверятеля правила с особым названием (которое ещё и переопределить можно), который неявно инстанцирует валидатор. Причём узнать об этом из каких-либо сигнатур я тоже не могу, мне просто нужно знать что это так. Я не могу имплементировать интерфейс правила — нужно наследоваться от базового класса (причём для всего остального интерфейсы есть — поэтому все проверятели принимают на вход Constraint, а потом допроверяют его класс уже внути). Если следовать единству стиля, я ещё и должен сделать все свойства моего правила публичными и принимать их значения в виде ассоциативного массива. Всё не просто в доме, который построил Джек) И это не вдаваясь в группы, например. На мой сугубо личный взгляд, конечно.
2. Не хочется для каждой библиотеки, которую я использую, устанавливать плагины. И не хочется для каждой библиотеки изучать новый синтаксис, не имеющий отношения к базовому языку. А если я решу сменить библиотеку?
Что до использования кода, то массивы $options тоже не очень очевидны, надо знать набор свойств (их правда в классе правила подсмотреть всё же можно), ошибки будут видны в рантайме (и то если правило всё корректно проверяет, нативного контроля типов никакого же).
В Java аннотации — это часть Java. В PHP аннотации — это часть симфони)
oxidmod
И давно симфони имеет отношение к аннотациям
altgamer
Это док-комментарии, то есть просто строка комментария вида '/** My comment. */'. Аннотации — это механизм, в Java и C# он реализован на уровне языка (в том числе создание кастомных аннотаций). В PHP аннотации как часть языка были отклонены, и существуют только в форме библиотек, работающих на базе тех самых комментариев. То есть в самом PHP никаких аннотаций на данный момент нет.
Проблема с библиотекам та же — это код, но не на PHP, и потенциально вариантов синтаксиса может быть столько же, сколько проектов (сравните хотя бы RFC, доктрину, Notoj, Java, C#).
Кроме того, док-блок всего один и он уже задействован под PHPDoc и описание функциональности в произвольной форме. Библиотеки накладывают на его содержание ограничения, вводя свой синтаксис. Представьте, что у вас большая и тщательно документированная кодовая база, и вам нужно задействовать в ней новую библиотеку, использующую «аннотации». Шанс, что из-за собачки не в том месте будут потрачены несколько человекочасов, не так уж мал.
Этого мало, так ещё и нельзя быть абсолютно уверенным, например, что синтаксис двух одновременно используемых библиотек не будет конфликтовать) Причём обнаружить такие конфликты будет очень сложно, потому что они вне поля зрения PHP и тем более IDE.
oxidmod
Это всё так, но на деле ещё не встречал подобных проблем. Изначально я тоже относился к ним с подозрением, но потом распробовал.
VolCh
Кое-какая поддержка док-комментов всё же есть в самом PHP. Парсинг их, да, дело библиотек, но "изъятие" из кода именно док-комментов к конкретным классам, функциям, свойствам осуществляет именно сам PHP.
И в нормальных IDE поддержка аннотаций вполне на уровне поддержки самого языка.
altgamer
Даже не кое-какая, а вплоть до их обработки в opcache.
Но IDE хорошо поддерживают без плагинов PHPDoc. Например property и method, совмещённые с магическими методами, позволяют в шторме делать интересные вещи) А вот аннотации без плагинов не поддерживаются ни в каком виде.
Плагины, расширения (в том числе синтаксические) дают массу дополнительных возможностей. Например, можно из PHP писать на JS) Но цена этих возможностей — дополнительные зависимости и ограничения, необходимость постоянно что-то «держать в уме» помимо задачи. Излишек всего этого отнимает много ресурсов и со временем может даже начать мешать поспевать за новыми возможностями самого языка. Например те же сторонние расширения с болью мигрировали с 5 версии на 7 (я уж не говорю про поддержку ZTS). А если всё-таки возьмут и примут очередной RFC по аннотациям? Особенно забавно будет, если их синтаксис будет основан на док-комментах)
На мой взгляд, нативные решения как правило проще, чище, их легче поддерживать и заменять при необходимости. С другой стороны, если не использовать «хаки», то потеряешь те возможности, которые они дают уже сейчас. Просто достаточно помнить, что это делается на свой страх и риск. Ну и выбирать из них те, что понадёжнее и действительно упрощают работу, а не просто клёво выглядят.
oxidmod
В случае с вашими типами точно также нужно держать в голове и помнить. С другой стороны, если отказаться от примитивов в принципе, и использовать ВО с валидацией в конструкторе, вообще никакие валидаторы не нужны. Достаточно нативных возможностей пыхи.
altgamer
В данном случае IDE сможет почти всё держать в голове за разработчика — и сами типы, и их параметры, и возможность их комбинации. Во всех этих случаях будут доступны подсказки и автодополнение, а любая ошибка в описании будет подсвечена и отражена в инспекторе. Можно сказать, что с этим расчётом библиотека и создавалась)
Неочевидны могут быть названия. IDE не сможет объяснить, что именно означает StructuralType, UIntType или, тем более, Union — при разработке я опирался на понятия из Си, но это лишь допущение дизайна. Здесь могут выручить только комментарии (плюс в том, что они легко доступны в IDE). Но от подобных проблем не избавлена вообще ни одна библиотека.
На этот счёт я описал свою позицию чуть ниже. Если коротко — в конструкторе так или иначе всё равно нужно выполнять проверки (то есть вопрос исключительно в способе) и есть некоторые проблемы с обработкой их результатов. Но именно в расчёте на этот метод проверки я и делал «дженерики») Да, во многих случаях такого подхода достаточно — каждому инструменту своё место, и использовать валидацию там, где и без неё всё нормально, конечно не стоит. К тому же нативные возможности языка в этом направлении развиваются.