18 июня в 18:11

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

PHP*
В отношении данных, которые программа получает извне, принято следовать правилу 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, просвистевший мимо меня.

Благодарю за ваше внимание!
Алексей Максимов @altgamer
карма
–1,0
рейтинг 20,0
Программист
Похожие публикации
Самое читаемое Разработка

Комментарии (47)

  • +4
    Прочел с вечера, не проникся.
    Прочел повторно сутра и все равно не проникся.
    Зачем???
    • 0
      Тоже вот жду комментариев тех, кто шарит.
      • 0
        Тоже прочитал статью, но не понял, чем в таком случе плохо использование обычных классов с геттерами и сеттерами, которые уже сами внутри себя смогут проверить входящие типы. Возможно стоит вынести проверки типизации в отдельный класс и использовать его…
        • 0
          Я предполагал использование typedef для валидации входных данных на уровне скрипта, а не отдельных функций — на этом уровне у PHP уже есть система типов, которая расширяется как раз классами (пример — «дженерики», и я уже ненавижу это название).
          • 0
            Ну вот же

            Можно кодом, можно аннотациями.
            Богатая коллекция проверок из коробки
            Легко добавляются кастомные в виде колбека (для разовых проверок) или собственных классов-валидаторов

            В чем преимущество вашего подхода?
            • 0
              Для меня минусов в данном случае два:

              1. Избыточная сложность. Это вполне можно понять, потому что компонент универсальный и старается быть подходящим для всего, что связано с проверкой, предусмотреть все возможные случаи использования, в том числе некорректные. Но два класса на одно правило, один из которых инстанцируется неявно, для меня слишком)
              2. Неочевидность. Аннотации в doc-комментариях нужно в принципе знать и уметь — это отельный от PHP синтаксис, а все min-max, multiple, strict, format и прочие поля ограничителей нужно помнить.

              Но вообще я конечно не возьмусь спорить с компонентами симфони)
              • 0
                1. Их два, потому что один содержит правило, а второй проверяется соотвествие значения правилу. Это просто SRP. Избыточно ли? Не думаю. Для большинства случаев есть готовые из коробки валидаторы или просто на гитхабе написаны кемто до вас и вам писать ничео не придется. Их слишком много? So what? Экономите пару килобайт на диске?
                2. С каких пор необходимость знать и уметь проблема? Тем более для ИДЕ есть плагины супортящие эти компоненты. ИДЕ все подскажет. Не хотите совсем в аннотации? Так можно же и в код)

                зы. Во всяких джаво-шарпах аннотации во всю юзаются) и это круто как по мне.
                • 0
                  1. Дело не в количестве готовых правил — это как раз отлично — а в сложности реализации. Я не могу применить правило к данным — для этого мне нужен валидатор. Я не могу просто реализовать класс правила — для этого мне нужно реализовать ещё и класс проверятеля правила с особым названием (которое ещё и переопределить можно), который неявно инстанцирует валидатор. Причём узнать об этом из каких-либо сигнатур я тоже не могу, мне просто нужно знать что это так. Я не могу имплементировать интерфейс правила — нужно наследоваться от базового класса (причём для всего остального интерфейсы есть — поэтому все проверятели принимают на вход Constraint, а потом допроверяют его класс уже внути). Если следовать единству стиля, я ещё и должен сделать все свойства моего правила публичными и принимать их значения в виде ассоциативного массива. Всё не просто в доме, который построил Джек) И это не вдаваясь в группы, например. На мой сугубо личный взгляд, конечно.

                  2. Не хочется для каждой библиотеки, которую я использую, устанавливать плагины. И не хочется для каждой библиотеки изучать новый синтаксис, не имеющий отношения к базовому языку. А если я решу сменить библиотеку?

                  Что до использования кода, то массивы $options тоже не очень очевидны, надо знать набор свойств (их правда в классе правила подсмотреть всё же можно), ошибки будут видны в рантайме (и то если правило всё корректно проверяет, нативного контроля типов никакого же).

                  Во всяких джаво-шарпах аннотации во всю юзаются) и это круто как по мне.

                  В Java аннотации — это часть Java. В PHP аннотации — это часть симфони)
                  • 0
                    И давно симфони имеет отношение к аннотациям
                    • 0
                      Это док-комментарии, то есть просто строка комментария вида '/** My comment. */'. Аннотации — это механизм, в Java и C# он реализован на уровне языка (в том числе создание кастомных аннотаций). В PHP аннотации как часть языка были отклонены, и существуют только в форме библиотек, работающих на базе тех самых комментариев. То есть в самом PHP никаких аннотаций на данный момент нет.

                      Проблема с библиотекам та же — это код, но не на PHP, и потенциально вариантов синтаксиса может быть столько же, сколько проектов (сравните хотя бы RFC, доктрину, Notoj, Java, C#).

                      Кроме того, док-блок всего один и он уже задействован под PHPDoc и описание функциональности в произвольной форме. Библиотеки накладывают на его содержание ограничения, вводя свой синтаксис. Представьте, что у вас большая и тщательно документированная кодовая база, и вам нужно задействовать в ней новую библиотеку, использующую «аннотации». Шанс, что из-за собачки не в том месте будут потрачены несколько человекочасов, не так уж мал.

                      Этого мало, так ещё и нельзя быть абсолютно уверенным, например, что синтаксис двух одновременно используемых библиотек не будет конфликтовать) Причём обнаружить такие конфликты будет очень сложно, потому что они вне поля зрения PHP и тем более IDE.
                      • 0
                        Это всё так, но на деле ещё не встречал подобных проблем. Изначально я тоже относился к ним с подозрением, но потом распробовал.
                      • 0

                        Кое-какая поддержка док-комментов всё же есть в самом PHP. Парсинг их, да, дело библиотек, но "изъятие" из кода именно док-комментов к конкретным классам, функциям, свойствам осуществляет именно сам PHP.


                        И в нормальных IDE поддержка аннотаций вполне на уровне поддержки самого языка.

                        • 0
                          Даже не кое-какая, а вплоть до их обработки в opcache.

                          Но IDE хорошо поддерживают без плагинов PHPDoc. Например property и method, совмещённые с магическими методами, позволяют в шторме делать интересные вещи) А вот аннотации без плагинов не поддерживаются ни в каком виде.

                          Плагины, расширения (в том числе синтаксические) дают массу дополнительных возможностей. Например, можно из PHP писать на JS) Но цена этих возможностей — дополнительные зависимости и ограничения, необходимость постоянно что-то «держать в уме» помимо задачи. Излишек всего этого отнимает много ресурсов и со временем может даже начать мешать поспевать за новыми возможностями самого языка. Например те же сторонние расширения с болью мигрировали с 5 версии на 7 (я уж не говорю про поддержку ZTS). А если всё-таки возьмут и примут очередной RFC по аннотациям? Особенно забавно будет, если их синтаксис будет основан на док-комментах)

                          На мой взгляд, нативные решения как правило проще, чище, их легче поддерживать и заменять при необходимости. С другой стороны, если не использовать «хаки», то потеряешь те возможности, которые они дают уже сейчас. Просто достаточно помнить, что это делается на свой страх и риск. Ну и выбирать из них те, что понадёжнее и действительно упрощают работу, а не просто клёво выглядят.
                          • +1
                            В случае с вашими типами точно также нужно держать в голове и помнить. С другой стороны, если отказаться от примитивов в принципе, и использовать ВО с валидацией в конструкторе, вообще никакие валидаторы не нужны. Достаточно нативных возможностей пыхи.
                            • 0
                              В случае с вашими типами точно также нужно держать в голове и помнить.

                              В данном случае IDE сможет почти всё держать в голове за разработчика — и сами типы, и их параметры, и возможность их комбинации. Во всех этих случаях будут доступны подсказки и автодополнение, а любая ошибка в описании будет подсвечена и отражена в инспекторе. Можно сказать, что с этим расчётом библиотека и создавалась)

                              Неочевидны могут быть названия. IDE не сможет объяснить, что именно означает StructuralType, UIntType или, тем более, Union — при разработке я опирался на понятия из Си, но это лишь допущение дизайна. Здесь могут выручить только комментарии (плюс в том, что они легко доступны в IDE). Но от подобных проблем не избавлена вообще ни одна библиотека.

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

                              На этот счёт я описал свою позицию чуть ниже. Если коротко — в конструкторе так или иначе всё равно нужно выполнять проверки (то есть вопрос исключительно в способе) и есть некоторые проблемы с обработкой их результатов. Но именно в расчёте на этот метод проверки я и делал «дженерики») Да, во многих случаях такого подхода достаточно — каждому инструменту своё место, и использовать валидацию там, где и без неё всё нормально, конечно не стоит. К тому же нативные возможности языка в этом направлении развиваются.
  • +1
    enum(false, true, 0, 1, 'yes', 'no')

    Костылируем свой код на ходу :-)
    Так и не понял зачем.

    • 0
      Согласен, пример получился достаточно дурной) Лучше бы подошло что-то типа:

      field('state', enum('WILL_READ', 'READ', 'FAVORITE_BOOK'));
      

      Этот тип — аналог literal type.
  • +1
    Интерфейсы не мешало бы по PSR именновать. IType — TypeInterface, IField — FieldInterface.
    • 0
      Согласен, для публичного кода это недоработка. Исправлю при первой возможности.
    • 0

      А PSR разве регулирует наличие/отсутствие типа класса в названии класса?
      Такие требования, насколько я помню, есть только в разделе о том "как следует писать текст PSR".

  • 0
    Silver bullet — ооп, паттерны. Посмотрите на symfony3+. То ято вы описали это какой то си путь в пхп.
  • +1

    То, что Вы используете венгерскую нотацию — самодостаточный аргумент запретить использовать ваш код для всего, связанного с типами.

  • 0
    Насколько я понимаю, область применения довольно узка и, например, операция сложения с двумя переменным типа NumericType невозможна. Было бы интересно посмотреть пример реального использования (вы же не ради любви к искусству писали, а для решения какой-то конкретной задачи?)
    • 0
      Переменные, соответствующие типу 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;
      }

      Дальше эти данные можно пропускать через валидатор, где числа определять как числа, массивы — как массивы и так далее. Точно так же в целях отладки можно проконтролировать и вывод метода.
  • 0
    Надеюсь когда нибудь PHP получит строгую типизацию
    • 0

      А это вопрос определения строгой сильной типизации.


      В 1974 году Лисков и Зиллес назвали сильно типизированными те языки, в которых «при передаче объекта из вызывающей функции в вызываемую тип этого объекта должен быть совместим с типом, определённым в вызываемой функции»

      Что достигается тайпхинтингом и strict режимом. Революция свершилась? ;)

  • +1

    В PHP есть свои "пользовательские типы", называются — классы. Разница между классом и структурой расплывчата и зависит от языка программирования. Т.е. не вижу никаких проблем использовать в PHP класс как структуру с воссозданием ее из массива или строки через конструктор (обычный или именованный). А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP. Пока причин хоть пытаться использовать ваш вариант typedef('input', по сравнению с Input::createFromArray($array) (боже упаси меня класс назвать Input) не вижу, что я не заметил?


    Называть то, что вы придумали "пользовательскими типами" очень смело. Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.

    • 0
      Насчёт сериализатора не могу согласиться просто потому что такой функциональности в библиотеке нет)

      А в отношении классов — вполне возможно, как раз для этого и сделаны «дженерики» (или что это вообще). Назначением же typedef я полагаю как раз отсутствие необходимости всякий раз описывать сложную валидацию на чистом PHP. Тем более что даже в этом случае вместо чистого PHP рано или поздно появятся некие «стандартные правила», которые порой принимают форму "(int|string)[7]?". Я просто предложил уже готовый набор правил — в отличие от метаязыков ими довольно удобно пользоваться в IDE.
      • 0
        А чем вам эти типы не угодили?
        • 0
          Это расширение PECL не поставляется вместе с PHP.

          Тем не менее, это тоже инструмент. Хотя, честно говоря, особого смысла в нём в PHP 7 я не вижу. Так-то в расширениях можно много интересного сделать, даже перегрузку операторов для объектов.

          Для typedef я предполагал использование для валидации входных данных на уровне скрипта, а не отдельной функции. Для валидации сложных структур я удовлетворившего меня инструмента не нашёл, потому и решил сделать этот.
    • 0
      А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP.

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


      Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.

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

      • 0
        В целом декларативный стиль описания правил более удобен на сложной логике.

        Ну, готовых решений валидаторов с декларативным описанием правил, которые можно использовать в объекте — куча, так что стиль не преграда.


        Хотя мне кажется, как раз на сложной логике декларативный стиль менее удобен. Декларативное хорошо читается на простых правилах, а как появляется сложная, кастомная для этого типа логика (типа, если поле1=5, то поле2 — строка, а если поле1=6, то поле2 — инт, или что-то еще сложнее), то нам придется куда-то выносить эту логику, что бы создать декларативное правило. Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.


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

        А, точно, я почему-то решил, что там потом рождаются объекты. Да, валидатор вложенных массивов ;) В общем, не просто валидатор, а описатель и валидатор схемы. Сразу находятся подобные готовые решения, причем, с более лаконичным описанием правил в виде массива. Хотя я бы просто использовал json schema и библиотеки их валидации.

        • 0
          Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.

          Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать. А вообще всё так и работает) Тип — это объект, который реализует метод 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-кода, да и читаемость так себе.

          Извините, если неправильно понял, что вы имели в виду.
          • 0
            Хотя я бы просто использовал json schema и библиотеки их валидации.

            Схемы — хороший инструмент. Но это отдельный язык со своей спецификацией, чисто декларативный и к PHP никакого отношения не имеющий (по мне ещё и несколько многословный, но это вопрос предпочтений).
          • 0
            Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать.

            Когда нужно — тогда и будет. Я о том и говорю, что декларативный подход следует комбинировать с императивным. Что-то вроде


            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 спецификацию.

            • 0
              О том, что вам каждый раз, когда нужно проверить соответствие типу — придется проходить по всему массиву я уж молчу.

              Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды. Я не могу представить, зачем может понадобиться проверять тип одних и тех же данных несколько раз.

              Между классом и вашим «типом» есть одна огромная разница — создание объектов, которые могут проверять валидность создания себя, а по-этому не должны быть в «невалидном» состоянии.

              Вообще говоря, я не вижу никаких противоречий между использованием классов и валидацией данных. В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области. Это в любом случае так — если тип не проверит программист, то его рано или поздно проверит сам PHP и отреагирует на ошибки по своему усмотрению (увы, до сих пор не все ошибки являются исключениями).

              Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно. Кто-то рано или поздно должен будет задаться вопросом, что же там содержится. Чем глубже это будет происходить по иерархии вызовов, тем дальше от места действительной ошибки на неё возникнет какая-то реакция (ещё хуже, если это произойдёт вообще где-то дальше по коду или, тем более, останется незамеченным).

              Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.

              По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.

              Ну, есть валидатор описания структуры, который скажет что «неизвестный тип данных strrrng», или можно писать типа 'data' => type::string. '|'. type::int.

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

              Не говоря уже о том, что 'data' => 'string|object' вообще быть не должно, это такой большой подводный камень.

              Пусть будет 'null|object')
              • 0
                Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды.

                Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает 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 и отловить это только в рантайме

                • 0
                  Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает 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 или её плагин не поддерживают именно этот валидатор.
                  • 0
                    Хотя вы мне подали идею — кэшировать результат проверки с использованием какого-то дайджеста проверяемых данных) Спасибо!

                    Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.


                    Это совсем не одно и то же. Есть интерфейс (имя — это строка) и его реализация (Ивановы живут в Иванове). Через какое-то время в Иванове разрешат жить Петровым, но имя всё ещё не сможет быть числом.

                    Попробую еще раз. Речь о том, что как только был создан объект типа 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, ну и руки с головой для аккуратного использования исключений, и все будет замечательно

                    • 0
                      Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.

                      Если с данными что-то произошло, то это очевидно уже другие данные с другим дайджестом)

                      Речь о том, что как только был создан объект типа User, мы можем везде где нужно проверять переменные на тип User. Еще раз класс User — это тип, т.е. это описание структуры + валидация структуры.

                      User — это класс, объекты которого инстанцируются с использованием некоторых исходных данных, а не сами эти данные. То есть исходные данные нуждаются в проверке в любом случае, просто вы либо будете каждый раз писать кастомные проверки, либо использовать какой-то стандарт (не обязательно именно typedef — просто он должен быть). Как вы дальше будете использовать полученный объект — это уже не вопрос валидации. Вы, возможно, неправильно поняли идею — я не предлагаю валидировать данные внутри каждой функции, в этом нет смысла.

                      При создании объекта класса нет никакой разницы, почему провалилась проверка типа и объект был не создан — потому-что тайпхинт не пропустил аргумент в конструктор или внутри конструктора не прошла проверка.

                      Нет никакой разницы для программиста, но не для пользователя, который передал данные.

                      Ну, вы вообще в курсе, что конструктор не входит в понятие интерфейса класса? Или у вас к понятию «интерфейс» какое-то свое оригинальное определение?

                      Интерфейс — это несколько более широкое понятие, нежели «интерфейс класса») Интерфейс метода — это его сигнатура, а именно принимаемые аргументы, возвращаемый тип и выбрасываемые исключения. То есть всё то, что вы описываете в PHPDoc.

                      echo «ой ой, передали нам что-то не то»;

                      Важно ответить на вопрос, что именно не то и куда передали. В случае с вложенной структурой придётся оборачивать в try-catch каждый вызов конструктора на каждом уровне вложенности — и даже это не поможет понять, передали неправильно $name или $addresses, если вы не будете парсить сообщение об ошибке и сопоставлять номера аргументов. А ещё мы можем поймать не только TypeError, но и другие типы исключений, и с каждым надо как-то работать.
  • 0

    С телефона всё не осмыслил, два вопроса:


    • есть ли возможность валидировать обычные объекты, в том числе stdClass хотя бы на уровне public свойств? Как с вложенностью объектов?
    • есть ли возможность задавать кастомные валидаторы (Callable), в том числе с возможностью валидировать по сложной логике несколько свойств, типа "два свойства должны быть равны" или "одно, и только одно из списка свойств должно быть не null"?
    • 0
      1. Сейчас валидировать объекты можно только если они реализуют интерфейсы ArrayAccess, Countable и Traversable. По умолчанию все массивные типы (ArrayType, StructType и Union) допускают и массивы, и такие объекты, но это поведение можно ограничить. В следующем релизе (0.0.3) я планирую дать структурам возможность принимать объекты только с интерфейсом ArrayAccess — Countable и Traversable им вроде бы ни к чему.

      Насчёт возможности валидировать объекты без интерфейсов вообще просто по public-свойствам я не уверен — можете привести пример, где это может быть полезно? Просто я не сторонник использования объектов в качестве словарей.

      Вложенность массивов и объектов-массивов допускается любая.

      2. Все типы — это объекты, реализующие интерфейс TypeInterface. Стандартные типы так же являются наследниками AbstractType (он как раз делает их callable). Поля (field, optional и union) реализуют интерфейс FieldInterface. Каждому полю для валидации передаётся целиком весь массив/объект, который передан для валидации в родительскую структуру, так что можно в рамках поля валидировать любые сочетания ключей и их значений — структурный тип по сути является объединением полей по логике «И».

      То есть расширение логики вполне доступно. А функции всего лишь выполняют роль «синтаксиса» — можно определить такие же функции для кастомных типов и полей.

      Для правила «одно, и только одно из списка свойств должно быть не null» уже предусмотрен стандартный тип union — он может выступать и как самостоятельный тип, и как поле структурного типа. С небольшой оговоркой — остальные поля объединения должны быть не null, а вообще отсутствовать.

      Для случая «два свойства должны быть равны» стандартной логики не предусмотрено)
  • 0
    1. Это не типизация, а валидация чистой воды
    2. 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;
    3. Ограничение $aMin, $aMax и $aLength нужно делать отдельным валидатором. Это нарушение SRP.
    4. Аргумент у object() то же самое. Нарушение SRP.
    5. struct() спорный валидатор. По мне так лучше проверять через объект.

    Вообще, готовых валидатор великое множество. Ваш интересен тем что позволяет валидировать многомерные массивы. Хотя и для этого полно других инструментов.


    Мне больше нравится валидатор Symfony. Простое описание правил и удобно использовать в middleware. Для многомерных массивов строю композит из DTO+payload.

    • 0
      Это не типизация, а валидация чистой воды

      Да — в какой-то степени) Тип определяется допустимым множеством значений и операциями, которые над ними допустимы. Валидация же — это проверка того, что значение соответствует конкретным условиям. То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация. В первом случае мы знаем, что можем сохранить значение на участке памяти размером (допустим) два байта и не можем прибавить его к числу без дополнительных преобразований. Во втором — мы уверены, что будучи сохранённым в базу данных, значение не нарушит её целостность.

      Исходя из задач, для которых я сам предполагал использовать библиотеку 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. Публичные свойства объектов, на мой взгляд, плохая практика потому что нарушают инкапсуляцию. Хотя могу согласиться со спорностью в силу его непривычности и, следовательно, неочевидности. Просто мне такая реализация кажется удобной — буду признателен за примеры других вариантов.
      • 0
        Да — в какой-то степени)

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


        То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация.

        Неправильно.


        • Строка — это простой тип;
        • Строка длинною 2 символа — это валидация;
        • Код страны — это тип предметной области (ValueObject);
        • Код страны, в которой у нашей кампании есть филиал — это валидация уровня предметной области.

        Если речь идёт о некоем целом диапазоне, то логичнее использовать int

        То есть, если я не знаю точно может ли число ровняться 0, но точно знаю что оно не отрицательно, то я должен использовать int, вместо uint. Не кажется ли вам это нелогичным?


        Вообще uint теряет свой смысл так как эквивалентен int(0).


        то их разновидностей будет достаточно много и это будет только путать.

        Так в этом весь смысл. Много простых, логичных и легко запоминающихся валидатор которые удобно комбинировать. В вашем же случае сходу возникают проблемы:


        1. Какой порядок у аргументов функции?
          Он конечно логичен, но все равно возникает желание перепроверить перед написанием кода.
        2. Что, если нужно ограничить только верхнюю планку величины числа? Каждый раз передавать 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.
        • 0
          Обычно, запросы от пользователя трансформируют в DTO, его уже валидируют и перенаправляют на доменный уровень.

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

          Например, в роли DTO можно рассматривать сам пришедший объект данных без всяких трансформаций — JSON-объект совершенно точно не имеет поведения) В этом случае нужно только проверить, что этот объект — правильный. Скажем в TypeScript можно было бы использовать для этого интерфейс, но в PHP интерфейсы имеют несколько иной смысл.

          Кстати, интересный вопрос — а можно ли рассматривать в качестве DTO объект с геттерами и сеттерами, необходимыми при отсутствии типизированных свойств в PHP? Это же фактически поведение)

          Неправильно.

          Вы весьма категоричны) Но я попробую привести пример — INT(11). 11 — это длина строкового представления числа, которое занимает 4 байта. Считать этот случай типом или валидацией?

          Тип — это описательная сущность. Валидация — это действие, совершаемое над данными, и она может происходить в том числе в отношении их типа. В PHP в отношении типов присутствует полная свобода, но вообще данные некоторого типа имеют некоторое внутреннее представление, от которого зависит объём необходимой для этого представления памяти. И за пределами PHP это важно. Например, строка длиной в два символа может быть сохранена в колонке таблицы, соответствующей типу CHAR(2) (я не беру в расчёт кодировку), а целое число из диапазона 0..65535 может быть записано в переменную Си типа uint16_t.

          Какой порядок у аргументов функции?

          IDE выдаёт подсказку, включающую имена аргументов.

          Что, если нужно ограничить только верхнюю планку величины числа? Каждый раз передавать null первым аргументом?

          То есть, если я не знаю точно может ли число ровняться 0, но точно знаю что оно не отрицательно, то я должен использовать int, вместо uint. Не кажется ли вам это нелогичным?

          Для целого числа задаётся диапазон, а не отдельные границы. Если требуется тип, открытый от нуля «вниз», то он явно специфичен. В остальных случаях границы диапазона известны.

          Что касается uint — тип нелогичен в смысле названия. В данном случае имелся в виду количественный тип (cardinal), который ограничивается сверху и этот случай достаточно распространён. В остальных случаях речь идёт просто о целом числе в некотором диапазоне.

          А для этого у нас есть type hinting в php 7.

          Проверка скалярных типов не работает на уровне входных данных скрипта. Если же просто передать неправильные значения в функцию с ограниченными типами аргументов, то результатом будет исключение TypeError, выдающее отладочную информацию о номере аргумента и позиции в коде. Кроме того массивы типизируются только через rest-аргументы и только линейно.

          Ну так, для строки можно использовать length(), а для массивов size(), по аналогии с функциями из php.

          Это явно добавляет задачу помнить про ещё две функции, которые к тому же не имеют смысла вне контекста сущностей. Не может быть length без string, то есть мы нагружаем «длину» проверкой того, что значение является строкой — это крайне неочевидное поведение. В лучшем случае это могут быть методы тех же типов StringType и ArrayType.

          object() — эквивалент is_object()

          В какой ситуации может возникнуть «просто объект», о классе которого нам ничего неизвестно? И как работать с объектом, интерфейс которого нам неизвестен? Для множества классов есть полиморфизм или в крайнем случае тип AnyType.

          instanceof() — эквивалент instanceof. Если будет передан не объект, получим false

          Так и ведёт себя тип object с указанным классом. Отсутствие же класса я рассматриваю как очень частный случай.
          • 0
            а можно ли рассматривать в качестве DTO объект с геттерами и сеттерами, необходимыми при отсутствии типизированных свойств в PHP? Это же фактически поведение)

            Нет. Геттеры и сеттеры это методы доступа к данным. Для этого есть термин — анемичная модель.


            Например, в роли DTO можно рассматривать сам пришедший объект данных без всяких трансформаций — JSON-объект совершенно точно не имеет поведения)

            JSON — объект только в js. В php же это просто строка, которую он может декодировать в массив или stdClass. Некоторые рассматривают массив как DTO, но это не правильно. Главное отличие DTO, от массива в том что объект имеет явный список свойств и он типизированныей, что позволяет идентифицировать данные.


            Валидация — это действие, совершаемое над данными, и она может происходить в том числе в отношении их типа.

            Я разве говорил что не надо проверять данные на соответсвие типа? Я говорил что правила:


            • Значение является строкой
            • Строка длинною 2 симвоал

            Это два отдельных правила, а не одно. А вот правило:


            • Значение является кодом страны

            Это одно правило. И для удовлетворения этого правила значение должно быть строкой длинной 2 символа, но это описано внутри типа код страны. Говоря вашими понятиями это будет выглядеть так:


            field('code', country_code())

            Но я попробую привести пример — INT(11). 11 — это длина строкового представления числа, которое занимает 4 байта.

            Это пример из баз данных которые делались как универсальный сервис хранения данных. В приложении можно создавать свои типы и поэтому такие универсальные типы не нужны. Например:


            • Phone — в БД будет INT(11) для Российского региона.
              (int не самый лучший способ хранения телефона)
            • СountryCode — в БД будет CHAR(2)
            • ArticleId — в БД будет INT или CAR(36) если это UUID

            Если же просто передать неправильные значения в функцию с ограниченными типами аргументов, то результатом будет исключение TypeError

            Всё верно. Вы создали валидатор и пользоваться им нужно как валидатором. Как я и говорил ранее, правила можно комбинировать. Условно:


            field('age', [int(), min(2), max(5)])

            field('name', [string(), length(128)])

            field('titles', [lot(), size(5)])

            Зачем, кстати, проверять размер массива если нам важно только чтоб он соответствовал ожидаемой схеме если это ассоциативный массив? Если это нумерованный список то нам важен тип значения. Разве что у нас нумерованный список ограниченной длинны, но тогда его лучше описывать как структуру.


            field('id', [object(), instanceof(ArticleId::class)])

            С объектами, кстати, тоже не понятно. Если вы проверяете пользовательские данные которые по определению не могут содержать объекты, зачем вам валидатор объектов?

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.