Comments 54
- Избыточная сложность. Это вполне можно понять, потому что компонент универсальный и старается быть подходящим для всего, что связано с проверкой, предусмотреть все возможные случаи использования, в том числе некорректные. Но два класса на одно правило, один из которых инстанцируется неявно, для меня слишком)
- Неочевидность. Аннотации в doc-комментариях нужно в принципе знать и уметь — это отельный от PHP синтаксис, а все min-max, multiple, strict, format и прочие поля ограничителей нужно помнить.
Но вообще я конечно не возьмусь спорить с компонентами симфони)
2. Не хочется для каждой библиотеки, которую я использую, устанавливать плагины. И не хочется для каждой библиотеки изучать новый синтаксис, не имеющий отношения к базовому языку. А если я решу сменить библиотеку?
Что до использования кода, то массивы $options тоже не очень очевидны, надо знать набор свойств (их правда в классе правила подсмотреть всё же можно), ошибки будут видны в рантайме (и то если правило всё корректно проверяет, нативного контроля типов никакого же).
Во всяких джаво-шарпах аннотации во всю юзаются) и это круто как по мне.
В Java аннотации — это часть Java. В PHP аннотации — это часть симфони)
Проблема с библиотекам та же — это код, но не на PHP, и потенциально вариантов синтаксиса может быть столько же, сколько проектов (сравните хотя бы RFC, доктрину, Notoj, Java, C#).
Кроме того, док-блок всего один и он уже задействован под PHPDoc и описание функциональности в произвольной форме. Библиотеки накладывают на его содержание ограничения, вводя свой синтаксис. Представьте, что у вас большая и тщательно документированная кодовая база, и вам нужно задействовать в ней новую библиотеку, использующую «аннотации». Шанс, что из-за собачки не в том месте будут потрачены несколько человекочасов, не так уж мал.
Этого мало, так ещё и нельзя быть абсолютно уверенным, например, что синтаксис двух одновременно используемых библиотек не будет конфликтовать) Причём обнаружить такие конфликты будет очень сложно, потому что они вне поля зрения PHP и тем более IDE.
Кое-какая поддержка док-комментов всё же есть в самом PHP. Парсинг их, да, дело библиотек, но "изъятие" из кода именно док-комментов к конкретным классам, функциям, свойствам осуществляет именно сам PHP.
И в нормальных IDE поддержка аннотаций вполне на уровне поддержки самого языка.
Но IDE хорошо поддерживают без плагинов PHPDoc. Например property и method, совмещённые с магическими методами, позволяют в шторме делать интересные вещи) А вот аннотации без плагинов не поддерживаются ни в каком виде.
Плагины, расширения (в том числе синтаксические) дают массу дополнительных возможностей. Например, можно из PHP писать на JS) Но цена этих возможностей — дополнительные зависимости и ограничения, необходимость постоянно что-то «держать в уме» помимо задачи. Излишек всего этого отнимает много ресурсов и со временем может даже начать мешать поспевать за новыми возможностями самого языка. Например те же сторонние расширения с болью мигрировали с 5 версии на 7 (я уж не говорю про поддержку ZTS). А если всё-таки возьмут и примут очередной RFC по аннотациям? Особенно забавно будет, если их синтаксис будет основан на док-комментах)
На мой взгляд, нативные решения как правило проще, чище, их легче поддерживать и заменять при необходимости. С другой стороны, если не использовать «хаки», то потеряешь те возможности, которые они дают уже сейчас. Просто достаточно помнить, что это делается на свой страх и риск. Ну и выбирать из них те, что понадёжнее и действительно упрощают работу, а не просто клёво выглядят.
В случае с вашими типами точно также нужно держать в голове и помнить.
В данном случае IDE сможет почти всё держать в голове за разработчика — и сами типы, и их параметры, и возможность их комбинации. Во всех этих случаях будут доступны подсказки и автодополнение, а любая ошибка в описании будет подсвечена и отражена в инспекторе. Можно сказать, что с этим расчётом библиотека и создавалась)
Неочевидны могут быть названия. IDE не сможет объяснить, что именно означает StructuralType, UIntType или, тем более, Union — при разработке я опирался на понятия из Си, но это лишь допущение дизайна. Здесь могут выручить только комментарии (плюс в том, что они легко доступны в IDE). Но от подобных проблем не избавлена вообще ни одна библиотека.
С другой стороны, если отказаться от примитивов в принципе, и использовать ВО с валидацией в конструкторе, вообще никакие валидаторы не нужны.
На этот счёт я описал свою позицию чуть ниже. Если коротко — в конструкторе так или иначе всё равно нужно выполнять проверки (то есть вопрос исключительно в способе) и есть некоторые проблемы с обработкой их результатов. Но именно в расчёте на этот метод проверки я и делал «дженерики») Да, во многих случаях такого подхода достаточно — каждому инструменту своё место, и использовать валидацию там, где и без неё всё нормально, конечно не стоит. К тому же нативные возможности языка в этом направлении развиваются.
enum(false, true, 0, 1, 'yes', 'no')
Костылируем свой код на ходу :-)
Так и не понял зачем.
field('state', enum('WILL_READ', 'READ', 'FAVORITE_BOOK'));
Этот тип — аналог literal type.
Возможное реальное применение библиотеки 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;
}
Дальше эти данные можно пропускать через валидатор, где числа определять как числа, массивы — как массивы и так далее. Точно так же в целях отладки можно проконтролировать и вывод метода.
А это вопрос определения строгой сильной типизации.
В 1974 году Лисков и Зиллес назвали сильно типизированными те языки, в которых «при передаче объекта из вызывающей функции в вызываемую тип этого объекта должен быть совместим с типом, определённым в вызываемой функции»
Что достигается тайпхинтингом и strict режимом. Революция свершилась? ;)
В PHP есть свои "пользовательские типы", называются — классы. Разница между классом и структурой расплывчата и зависит от языка программирования. Т.е. не вижу никаких проблем использовать в PHP класс как структуру с воссозданием ее из массива или строки через конструктор (обычный или именованный). А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP. Пока причин хоть пытаться использовать ваш вариант typedef('input', по сравнению с Input::createFromArray($array) (боже упаси меня класс назвать Input) не вижу, что я не заметил?
Называть то, что вы придумали "пользовательскими типами" очень смело. Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.
А в отношении классов — вполне возможно, как раз для этого и сделаны «дженерики» (или что это вообще). Назначением же typedef я полагаю как раз отсутствие необходимости всякий раз описывать сложную валидацию на чистом PHP. Тем более что даже в этом случае вместо чистого PHP рано или поздно появятся некие «стандартные правила», которые порой принимают форму "(int|string)[7]?". Я просто предложил уже готовый набор правил — в отличие от метаязыков ими довольно удобно пользоваться в IDE.
Это расширение PECL не поставляется вместе с PHP.
Тем не менее, это тоже инструмент. Хотя, честно говоря, особого смысла в нём в PHP 7 я не вижу. Так-то в расширениях можно много интересного сделать, даже перегрузку операторов для объектов.
Для typedef я предполагал использование для валидации входных данных на уровне скрипта, а не отдельной функции. Для валидации сложных структур я удовлетворившего меня инструмента не нашёл, потому и решил сделать этот.
А в конструкторе можно уже придумать любую валидацию без всяких мета-языков, на чистом PHP.
В целом, чаще всего на чистом PHP реализуют валидацию в конструкторах/сеттерах в императивной манере, что часто приводит к потере читаемости. В целом декларативный стиль описания правил более удобен на сложной логике.
Это больше похоже на сериализатор, что-то аналогичное jms serializer, но нагрузили еще валидацией.
Скорее это чистой воды валидатор. В принципе ничего вроде не мешает его использованию в конструкторе или сеттерах. Главное есть ли преимущества по сравнению с более популярными валидаторами.
В целом декларативный стиль описания правил более удобен на сложной логике.
Ну, готовых решений валидаторов с декларативным описанием правил, которые можно использовать в объекте — куча, так что стиль не преграда.
Хотя мне кажется, как раз на сложной логике декларативный стиль менее удобен. Декларативное хорошо читается на простых правилах, а как появляется сложная, кастомная для этого типа логика (типа, если поле1=5, то поле2 — строка, а если поле1=6, то поле2 — инт, или что-то еще сложнее), то нам придется куда-то выносить эту логику, что бы создать декларативное правило. Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.
Скорее это чистой воды валидатор. В принципе ничего вроде не мешает его использованию в конструкторе или сеттерах. Главное есть ли преимущества по сравнению с более популярными валидаторами.
А, точно, я почему-то решил, что там потом рождаются объекты. Да, валидатор вложенных массивов ;) В общем, не просто валидатор, а описатель и валидатор схемы. Сразу находятся подобные готовые решения, причем, с более лаконичным описанием правил в виде массива. Хотя я бы просто использовал json schema и библиотеки их валидации.
Уж лучше оставить в объекте в отдельном методе, который так же неплохо читается по своему названию.
Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать. А вообще всё так и работает) Тип — это объект, который реализует метод 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-кода, да и читаемость так себе.
Извините, если неправильно понял, что вы имели в виду.
Хотя я бы просто использовал json schema и библиотеки их валидации.
Схемы — хороший инструмент. Но это отдельный язык со своей спецификацией, чисто декларативный и к PHP никакого отношения не имеющий (по мне ещё и несколько многословный, но это вопрос предпочтений).
Если это правило будет применяться впоследствии, то его в любом случае придётся выносить отдельно. Ещё какая-то переработка потребуется, когда правила нужно будет комбинировать.
Когда нужно — тогда и будет. Я о том и говорю, что декларативный подход следует комбинировать с императивным. Что-то вроде
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 спецификацию.
О том, что вам каждый раз, когда нужно проверить соответствие типу — придется проходить по всему массиву я уж молчу.
Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды. Я не могу представить, зачем может понадобиться проверять тип одних и тех же данных несколько раз.
Между классом и вашим «типом» есть одна огромная разница — создание объектов, которые могут проверять валидность создания себя, а по-этому не должны быть в «невалидном» состоянии.
Вообще говоря, я не вижу никаких противоречий между использованием классов и валидацией данных. В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области. Это в любом случае так — если тип не проверит программист, то его рано или поздно проверит сам PHP и отреагирует на ошибки по своему усмотрению (увы, до сих пор не все ошибки являются исключениями).
Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно. Кто-то рано или поздно должен будет задаться вопросом, что же там содержится. Чем глубже это будет происходить по иерархии вызовов, тем дальше от места действительной ошибки на неё возникнет какая-то реакция (ещё хуже, если это произойдёт вообще где-то дальше по коду или, тем более, останется незамеченным).
Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.
По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.
Ну, есть валидатор описания структуры, который скажет что «неизвестный тип данных strrrng», или можно писать типа 'data' => type::string. '|'. type::int.
Но этот валидатор, скорее всего, не сможет вывести подсказку в IDE, а сообщит информацию в исключении. Так же для него, вероятно, не работает автодополнение.
Не говоря уже о том, что 'data' => 'string|object' вообще быть не должно, это такой большой подводный камень.
Пусть будет 'null|object')
Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды.
Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает 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 и отловить это только в рантайме
Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает 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 или её плагин не поддерживают именно этот валидатор.
Хотя вы мне подали идею — кэшировать результат проверки с использованием какого-то дайджеста проверяемых данных) Спасибо!
Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.
Это совсем не одно и то же. Есть интерфейс (имя — это строка) и его реализация (Ивановы живут в Иванове). Через какое-то время в Иванове разрешат жить Петровым, но имя всё ещё не сможет быть числом.
Попробую еще раз. Речь о том, что как только был создан объект типа 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, ну и руки с головой для аккуратного использования исключений, и все будет замечательно
Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.
Если с данными что-то произошло, то это очевидно уже другие данные с другим дайджестом)
Речь о том, что как только был создан объект типа User, мы можем везде где нужно проверять переменные на тип User. Еще раз класс User — это тип, т.е. это описание структуры + валидация структуры.
User — это класс, объекты которого инстанцируются с использованием некоторых исходных данных, а не сами эти данные. То есть исходные данные нуждаются в проверке в любом случае, просто вы либо будете каждый раз писать кастомные проверки, либо использовать какой-то стандарт (не обязательно именно typedef — просто он должен быть). Как вы дальше будете использовать полученный объект — это уже не вопрос валидации. Вы, возможно, неправильно поняли идею — я не предлагаю валидировать данные внутри каждой функции, в этом нет смысла.
При создании объекта класса нет никакой разницы, почему провалилась проверка типа и объект был не создан — потому-что тайпхинт не пропустил аргумент в конструктор или внутри конструктора не прошла проверка.
Нет никакой разницы для программиста, но не для пользователя, который передал данные.
Ну, вы вообще в курсе, что конструктор не входит в понятие интерфейса класса? Или у вас к понятию «интерфейс» какое-то свое оригинальное определение?
Интерфейс — это несколько более широкое понятие, нежели «интерфейс класса») Интерфейс метода — это его сигнатура, а именно принимаемые аргументы, возвращаемый тип и выбрасываемые исключения. То есть всё то, что вы описываете в PHPDoc.
echo «ой ой, передали нам что-то не то»;
Важно ответить на вопрос, что именно не то и куда передали. В случае с вложенной структурой придётся оборачивать в try-catch каждый вызов конструктора на каждом уровне вложенности — и даже это не поможет понять, передали неправильно $name или $addresses, если вы не будете парсить сообщение об ошибке и сопоставлять номера аргументов. А ещё мы можем поймать не только TypeError, но и другие типы исключений, и с каждым надо как-то работать.
С телефона всё не осмыслил, два вопроса:
- есть ли возможность валидировать обычные объекты, в том числе stdClass хотя бы на уровне public свойств? Как с вложенностью объектов?
- есть ли возможность задавать кастомные валидаторы (Callable), в том числе с возможностью валидировать по сложной логике несколько свойств, типа "два свойства должны быть равны" или "одно, и только одно из списка свойств должно быть не null"?
Насчёт возможности валидировать объекты без интерфейсов вообще просто по public-свойствам я не уверен — можете привести пример, где это может быть полезно? Просто я не сторонник использования объектов в качестве словарей.
Вложенность массивов и объектов-массивов допускается любая.
2. Все типы — это объекты, реализующие интерфейс TypeInterface. Стандартные типы так же являются наследниками AbstractType (он как раз делает их callable). Поля (field, optional и union) реализуют интерфейс FieldInterface. Каждому полю для валидации передаётся целиком весь массив/объект, который передан для валидации в родительскую структуру, так что можно в рамках поля валидировать любые сочетания ключей и их значений — структурный тип по сути является объединением полей по логике «И».
То есть расширение логики вполне доступно. А функции всего лишь выполняют роль «синтаксиса» — можно определить такие же функции для кастомных типов и полей.
Для правила «одно, и только одно из списка свойств должно быть не null» уже предусмотрен стандартный тип union — он может выступать и как самостоятельный тип, и как поле структурного типа. С небольшой оговоркой — остальные поля объединения должны быть не null, а вообще отсутствовать.
Для случая «два свойства должны быть равны» стандартной логики не предусмотрено)
- Это не типизация, а валидация чистой воды
- 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.
Это не типизация, а валидация чистой воды
Да — в какой-то степени) Тип определяется допустимым множеством значений и операциями, которые над ними допустимы. Валидация же — это проверка того, что значение соответствует конкретным условиям. То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация. В первом случае мы знаем, что можем сохранить значение на участке памяти размером (допустим) два байта и не можем прибавить его к числу без дополнительных преобразований. Во втором — мы уверены, что будучи сохранённым в базу данных, значение не нарушит её целостность.
Исходя из задач, для которых я сам предполагал использовать библиотеку 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. Публичные свойства объектов, на мой взгляд, плохая практика потому что нарушают инкапсуляцию. Хотя могу согласиться со спорностью в силу его непривычности и, следовательно, неочевидности. Просто мне такая реализация кажется удобной — буду признателен за примеры других вариантов.
Да — в какой-то степени)
Проектировали как типизацию, а сделали валидацию.
Обычно, запросы от пользователя трансформируют в 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
.
Обычно, запросы от пользователя трансформируют в 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 с указанным классом. Отсутствие же класса я рассматриваю как очень частный случай.
а можно ли рассматривать в качестве 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)])
С объектами, кстати, тоже не понятно. Если вы проверяете пользовательские данные которые по определению не могут содержать объекты, зачем вам валидатор объектов?
Нет. Геттеры и сеттеры это методы доступа к данным. Для этого есть термин — анемичная модель.
Некоторые рассматривают массив как DTO, но это не правильно. Главное отличие DTO, от массива в том что объект имеет явный список свойств и он типизированный, что позволяет идентифицировать данные.
Как же тогда реализовать DTO в PHP?
JSON — объект только в js. В php же это просто строка, которую он может декодировать в массив или stdClass.
В JS JSON так же является строкой. JSON — это самостоятельный декларативный формат, такой же как XML или YAML. Во внутреннее представление в любом языке данные в этом формате преобразуются путём десериализации.
Кстати говоря, декодировать JSON можно и в данные скалярного типа.
Это пример из баз данных которые делались как универсальный сервис хранения данных. В приложении можно создавать свои типы и поэтому такие универсальные типы не нужны.
Это не универсальные типы, а базовые. Они являются фундаментом для построения типов более высокого уровня.
Например, в системе типов PostgreSQL есть ряд базовых типов, который может быть дополнен администратором СУБД (базовые типы имеют специфическую внутреннюю реализацию, в том числе основанную на расширениях), и на основе этих типов могут быть определены типы более высокого уровня (которые собственной внутренней реализации не имеют).
В TypeScript так же есть базовые типы, на основе которых новые строятся с помощью псевдонимов и интерфейсов.
Аналогичный подход вы увидите во многих языках со статической типизацией, которая не является чем-то применимым только для хранилищ данных.
Typedef основан на тех же принципах. Вот так (например) может быть определён тип country_code:
const country_code_t = 'country_code'; typedef(country_code_t, string(2));
Вы создали валидатор и пользоваться им нужно как валидатором. Как я и говорил ранее, правила можно комбинировать.
В этом и есть различие между типами и правилами — правила можно комбинировать по отношению к одному и тому же объекту, в то время как к двум типам одного уровня одновременно объект принадлежать не может.
Объект может принадлежать к типу «строка длиной два символа», который является подмножеством типа «строка», в свою очередь входящего в тип «строка или число». Тип «число» является надмножеством типа «целое число», в который включён тип «целое число без знака». И так далее. То есть тип полностью описывает объект на своём уровне.
Правила же не имеют никакой иерархии — они выглядят как тэги, характеризуя объект лишь частично.
А валидаторами справедливо называть сущности, занимающиеся проверкой как принадлежности объекта к некоторому типу, так и его соответствия некоторому правилу. Это понятие не самостоятельное — оно основано на выбранном способе описания объекта. То есть, если требуется уточнение, имеет смысл говорить о «валидаторе типа» и «валидаторе правила».
В этом случае
field('age', [int(), min(2), max(5)]);
в одно смешаны два подхода, что приводит к неочевидности использования, неопределённому поведению и даже парадоксам:
field('age', [min(2)]);
field('age', [min(2), length(5)]);
field('age', [int(), string()]);
field('age', [lot(), string(), length(5)]);
field('age', [number(), int(), max(100)]);
И даже если списать это на «граничные случаи», ни из чего не следует, что min и max нужно использовать только совместно с int, length — c string, а size — с lot. Задача помнить об этом переложена со среды разработки на программиста.
Поощряются так же такие конструкции, которые говорят об ошибках проектирования:
field('age', [instanceof(FirstInterface), instanceof(SecondInterface)]);
Зачем, кстати, проверять размер массива если нам важно только чтоб он соответствовал ожидаемой схеме если это ассоциативный массив? Если это нумерованный список то нам важен тип значения. Разве что у нас нумерованный список ограниченной длинны, но тогда его лучше описывать как структуру.
С объектами, кстати, тоже не понятно. Если вы проверяете пользовательские данные которые по определению не могут содержать объекты, зачем вам валидатор объектов?
Ассоциативный массив может быть просто картой, у которой фиксирован тип ключей и значений. Количество элементов в массиве (как ассоциативном, так и индексированном) имеет значение, например, если есть ограничение размера хранилища, пропускной способности канала или если за один запрос мы просто не хотим обрабатывать больше N элементов по соображениям оптимизации.
Объектный тип «оставлен на откуп» для случаев использования помимо валидации входных параметров скрипта. Так же как для структур будет добавлена поддержка stdClass, хотя я сам этот подход не использую.
Как же тогда реализовать DTO в PHP?
В смысле как? Вы не знаете как создать объект в php? Или для вас проблема заполнить объект пользовательскими данными?
В TypeScript так же есть базовые типы
И в php есть базовые типы. И в php можно создавать свои типы и ваша библиотека для этого совсем не нужна.
Вы просто смешали у себя в голове понятия валидации и типизации. Вы сделали валидацию:
interface TypeInterface
{
public function validate($aValue): bool;
}
Стати́ческая типиза́ция — приём, широко используемый в языках программирования, при котором переменная, параметр подпрограммы, возвращаемое значение функции связывается с типом в момент объявления и тип не может быть изменён позже (переменная или параметр будут принимать, а функция — возвращать значения только этого типа).
wiki
Ваша библиотека не реализует типизацию. Пользовательские типы в php реализуется так:
class СountryCode
{
private $code;
public function __construct(string $code)
{
if (strlen($code) != 2) {
throw new TypeExeption('Invaled country code.');
}
$this->code = $code;
}
public function code(): string
{
return $this->code;
}
}
Вот мы объявляем переменную типа СountryCode
:
$code = new СountryCode($input['code']);
// для базовых типов это выглядит так
$age = (int) $input['age'];
Вот мы проверяем переменную на соответствие типу:
if ($code instanceof СountryCode) {
// do something
}
// для базовых типов это выглядит так
if (is_int($age)) {
// do something
}
Type hinting в аргументах
function some(СountryCode $code) {
// do something
}
// для базовых типов это выглядит так
function some(int $age) {
// do something
}
Type hinting в возвращаемых значениях
function some(): СountryCode {
return $code;
}
// для базовых типов это выглядит так
function some(): int {
return $age;
}
Пока ваша библиотека не будет реализовывать хотя бы поддержку type hinting для аргументов и возвращаемых значений, она не может называться типизацией.
Геттеры и сеттеры это методы доступа к данным.
Главное отличие DTO, от массива в том что объект имеет явный список свойств и он типизированный, что позволяет идентифицировать данные.
В смысле как? Вы не знаете как создать объект в php? Или для вас проблема заполнить объект пользовательскими данными?
Учитывая, что типизированных свойств в PHP нет, я не вижу способа создать объект, который соответствовал бы паттерну DTO так, как вы его определили.
Вы просто смешали у себя в голове понятия валидации и типизации.
Видимо, не только я. То есть если бы я назвал метод check вместо validate, то это был бы чекер?)
Пользовательские типы в php реализуется так:
Так в PHP реализуются классы. Они конечно выполняют функцию типов, но этим дело не ограничивается ни для классов, ни для типов.
Вот мы объявляем переменную типа СountryCode:
$code = new СountryCode($input['code']); // для базовых типов это выглядит так $age = (int) $input['age'];
Объявление переменной в PHP выглядит так:
$a = null;
Всё остальное — это уже вызов конструктора и приведение типа, которые тип переменной никак не ограничивают. В данном случае и то, и другое — это просто операция по преобразованию одних данных ($input['code'] и $input['age']) в другие (CountryCode, int), ничем не отличающаяся от, например, вызова функции. И если первое преобразование производит валидацию значения, то второе попросту некорректно — при получении строки «шыснацать», которая тихо будет преобразована в 0 и в таком виде отправится дальше. Любое преобразование — это потенциальная возможность исказить данные, не говоря о накладных расходах в случае, если данных много.
Вот мы проверяем переменную на соответствие типу:
if ($code instanceof СountryCode) { // do something } // для базовых типов это выглядит так if (is_int($age)) { // do something }
Ничто не мешает сделать так:
if((type(country_code_t))($countryCode)) { /* SMTH */ }
или так:
if(is($countryCode, type(country_code_t))) { /* SMTH */ }
Пока ваша библиотека не будет реализовывать хотя бы поддержку type hinting для аргументов и возвращаемых значений, она не может называться типизацией.
Например, контроль типа object появится только в PHP 7.2. А для типа resource никакого контроля кроме is_resource нет и, видимо, не будет. Не так давно не существовало и контроля скалярных типов.
В JS контроля типов нет совсем. Вы возьмётесь утверждать, что в JS нет типизации?
Но если очень хочется, то всегда найдётся старый как мир трюк) В PHP 7 это, конечно, не работает.
Ну и я всё-таки не понимаю, какое отношение библиотека имеет к контролю типов аргументов и возвращаемых значений функций, типам переменных (так и до типов свойств объекта недалеко), если её основное назначение — контроль типов входных параметров скрипта) При необходимости добавить такую функциональность не составляет труда, пусть и не в «нативной» форме — но я не сделал этого сознательно как раз во избежание.
Я не понимаю, каким критериям с вашей точки зрения должен удовлетворять тип и чем он вообще для вас является.
Объекты TypeInterface являются типами в том же смысле, в каком объект класса Order является заказом, а объект класса Car — машиной. Это не более чем абстракция, позволяющая описать понятие и взаимодействие с ним в рамках языка. Говорить о том, что объект TypeInterface является валидатором — это то же самое, что говорить, что объект класса Mail является отправителем, потому что у него есть метод send. Ну да, в какой-то мере так оно и есть.
Главное отличие DTO, от массива в том что объект имеет явный список свойств и он типизированный, что позволяет идентифицировать данные.
Учитывая, что типизированных свойств в PHP нет, я не вижу способа создать объект, который соответствовал бы паттерну DTO так, как вы его определили.
Тут недопонимание, по-моему. "он" — это объект. В параметрах конструктора (или иного метода) какого-то сервиса (в широком смысле слова) Person я могу указать что-то вроде __construct (PersonDTO $dto) и на уровне языка будет контролироваться, что в теле конструктора будет использоваться именно PersonDTO с чётко ожидаемой структурой. Валидность состояния этого DTO — отдельный вопрос, обычно область ответственности самого DTO (тогда без публичные свойств, значения сервис получает по геттерам, а валидация происходит в конструкторе/сеттере DTO) или клиента, его передающего в сервис.
Валидность состояния этого DTO — отдельный вопрос, обычно область ответственности самого DTO (тогда без публичные свойств, значения сервис получает по геттерам, а валидация происходит в конструкторе/сеттере DTO) или клиента, его передающего в сервис.
Мы приняли, что геттеры и сеттеры не соответствуют DTO, превращая его в анемичную модель (статья на тему). В сочетании с отсутствием контроля типов свойств в PHP это не позволяет возложить ответственность за корректность данных на DTO (я правда не понимаю, за что он тогда вообще отвечает).
Возлагать ответственность на клиента нельзя, поскольку клиент ничего не знает о типах данных, которые должны быть помещены в DTO (в лучшем случае он знает названия полей), и, естественно, ничего не знает о реализации сервиса, принимающего этот DTO. Кроме того, у клиента вероятно уже есть зона ответственности, иначе какой в нём смысл.
Остаётся возложить ответственность на сам сервис. Но это ничем не отличается от использования массивов, полным аналогом которых являются объекты с нетипизированными публичными свойствами (разве что скорость доступа к свойству несколько выше). Но массивы мы так же отклонили, а у сервиса есть другая ответственность, помимо проверки состояния другого объекта.
Могу предположить, что нам потребуется, с одной стороны, какая-то фабрика DTO (чтобы клиент мог корректно его создать), а с другой — его валидатор (чтобы сервис мог корректно его проверить). Результат — набор классов PersonDto, PersonDtoFactory и PersonDtoValidator. Архитектура так себе.
Мы приняли, что геттеры и сеттеры не соответствуют DTO, превращая его в анемичную модель
Наоборот. Мы решили, что DTO может содержать геттеры и сеттеры. Да, такие DTO похожи на анемичную модель, но DTO это не модель. DTO — это контейнер для передачи данных между уровнями приложения. Они не должны содержать бизнес логики и могут содержать только методы для доступа к данным (геттеры и сеттеры).
В сочетании с отсутствием контроля типов свойств в PHP
DTO появились в Java и используется в других языках, не все из которых поддерживают контроль типов свойств. В PHP собираются ввести контроль типов свойств, но до тех пор мы вынуждены пользоваться геттерами и сеттерами, если хотим контролировать типы.
Ruby, например, не поддерживает type hinting, но это не значит что в нем нельзя использовать DTO.
Возлагать ответственность на клиента нельзя, поскольку клиент ничего не знает о типах данных, которые должны быть помещены в DTO (в лучшем случае он знает названия полей)
Как раз наоборот. Только клиент знает структуру данных полученных от пользователя, и только он знает как создать DTO на основе пользовательских данных, и только он знает как отреагировать на ошибки валидации пользовательских данных.
Если пользовательские данные получены из web-формы, то необходимо пользователю сообщить об ошибках заполнения формы подсветив неправильные поля. Служба предметной области ничего не знает о web-форме. Она только выполняет бизнес транзакцию. Если ей передать некорректное данные, то она может только выбросить исключение. А о web-форме знает клиент. Клиент может проваледировать данные и отобразить ошибки.
Также, нужно учитывать, что служба предметной области может быть вызвана не только в результат обработки заполненной web-формы, но и, например, в результате вызова API, консольного вызова или заполнения нескольких комбинированных форм. В этом случае мы имеем несколько клиентов/контроллеров которые обрабатывают пользовательские данные и вызывают одну службу предметной области. И вот для того что-бы не зависит от клиента, нам пригодится единый интерфейс для службы предметной области.
Разница между использованием массива
function renameArticle(array $command)
и использованием DTO
function renameArticle(RenameArticle $command)
хотя бы в том, что у службы появляется более явный интерфейс. Клиенту не нужно знать какую структуру массива ожидает служба. Клиенту вообще не нужно знать ничего о реализации службы. Клиенту достаточно знать что у службы есть есть метод renameArticle()
и на вход он ожидает DTO команду RenameArticle
. То есть для работы клиента достаточно интерфейса:
interface ArticleService
{
public function renameArticle(RenameArticle $command);
}
Могу предположить, что нам потребуется, с одной стороны, какая-то фабрика DTO, а с другой — его валидатор
Совершенно верно. В этом нет ничего страшного.
// можно использовать фабрику
$command = $factory->create($data);
// можно использовать фабричный метод
$command = RenameArticle::createFromRequest($request);
// в роли фабричного метода может выступать конструктор
$command = new RenameArticle($data);
// можно и ручками заполнить
$command = new RenameArticle();
$command->article_id = $article->id();
$command->new_name = $data['name'];
Многие предпочитают использовать последние 2 метода заполнения DTO.
На счет валидатора. Валидатор это сервис и он единый для всех. А вот правила валидации специфичны для каждого DTO. И правильней описывать правила валидации в самом DTO, так как никто кроме самого DTO не знает как лучше проваледировать его.
- В Symfony удобно использовать аннотации.
- В Yii, для валидации, используется список правил.
- Аналогично можно описать правила в Laravel.
Как результат, у нас нет ни фабрик, ни валидаторов, а есть только DTO.
Пользовательские типы в PHP