А почему не создавать логгер отдельным провайдером?..
Я подразумевал, что он так и создаётся. Но ProvideAll связывает сущности, которые в общем-то ничем не связаны, кроме похожих зависимостей. Кроме того, вот в такой ситуации:
— Logger
— MetricCollector
— System (depends on: MetricsCollector)
— DBClient (depends on: Logger)
— MessageConsumer (depends on: Logger, MetricCollector)
— HTTPServer (depends on: MetricCollector)
— Application (depends on: System, DBClient, MessageConsumer, HTTPServer)
нельзя сделать какой-то один ProvideAll — для получения Application провайдеры для Logger и MetricCollector нужно указать явно, даже если эти зависимости для него транзитивны. Грубо говоря, создавая объект, нужно знать в точке его создания обо всех зависимостях его зависимостей, зависимостях их зависимостей и так далее. ProviderSet решает эту проблему, но лишь отчасти.
Однако, паниковать в Go не принято.
Я ни в коем случае не призываю использовать панику в качестве механизма обработки ошибок! :) Однако паники иногда всё же случаются, даже если нигде явно не писать panic. И, например, вот эта локальная база сломается, если её корректно не закрыть.
Насколько я понимаю, в целом тут речь идёт об обычном конструкторе — просто способ «возврата» у него нестандартный. Соответственно можно воспользоваться тем же Reaper, собрав деструкторы через Assume и передав их в функцию горутины в форме единого деструктора через Release.
Для случая, если функция принимает именно io.Closer и её сигнатура не может быть изменена (случай с библиотекой), действительно придётся сделать обёртку типа
type Closable struct { kdone.Destructor }
func (c Closable) Close() error { return c.Destroy() }
// При передаче:
go Cleanup(Closable{reaper.MustRelease()})
Чего-то типа io.CloserFunc в Go к сожалению нет :)
Таким образом мы просто делегируем освобождение ресурсов (в данном случае закрытие обработчиков) не вышестоящему коду, а вызываемой горутине.
Если же закрывающая функция имеет какое-то более специфическое поведение (например, применяет стратегию MRU для определения порядка), то Close — это уже часть более широкого интерфейса. Его можно скомпоновать (см. пример с DestructibleResource), но серебряной пули всё же не существует — возможно, освобождение ресурсов обработчика со временем станет лишь деталью реализации, а не публичным интерфейсом, и правильнее будет сформулировать зависимость конкретнее чем io.Closer или kdone.Destructor.
Program execution begins by initializing the main package and then invoking the function main. When that function invocation returns, the program exits. It does not wait for other (non-main) goroutines to complete.
То есть, если вся остальная программа не знает про горутину, которая рассчитывает статистику, то при досрочном выходе из main до освобождения ресурсов дело просто не дойдёт (даже у GC не будет шанса вызвать финализаторы). Конечно, в тривиальных случаях Go сам позаботится об освобождении системных ресурсов (хотя это fallback и необходимости корректной финализации с записями в лог и т.д. он не отменяет). Но если финализация ресурса предполагает сложную логику, то есть риски — например, тут локальная база, не будучи корректно закрытой, окажется повреждена. Даже банальная синхронизация файла может не состояться, а СУБД со своей стороны может записать в лог ошибку неожиданного закрытия соединения.
Говоря про совместимость драйверов БД с database/sql, я имел в виду скорее то, что нарушится связность экосистемы — различные утилиты вроде golang-migrate полагаются на стандартный интерфейс и при всём желании не могут предусмотреть все возможные частные случаи. В таких ситуациях сохранение идиоматичности кода необходимо по вполне практическим соображениям. Однако многие драйверы расширяют стандартную функциональность, предлагая при этом частные подходы к написанию кода — и тут я полностью согласен, часто расширения намного удобнее стандартной реализации (т. к. максимально используют возможности протокола конкретной СУБД) и используются вместо неё. Это хороший пример того, что расширение возможностей языка не противоречит совместимости и оказывается полезным :)
Согласен, для простых случаев предложенный подход избыточен. Хотя он даёт гарантию времени компиляции, но всё же в трёх строчках кода это можно трактовать как ненужную сложность.
Но подход предназначен в первую очередь для сведения кода сложных конструкторов к привычному шаблону new — error — defer. Кроме того, деструкторы легко поддаются расширению и компоновке, в то время как io.Closer требует оборачивания для расширения и опять же финализации в другой функции (имплементации метода Close), что усложняет код.
Тут, как мне кажется, лучше привести пример кода.
// Без использования выделенных деструкторов.
type Resource struct {
closed bool
r1 *Resource1
r2 *Resource2
}
func NewResource() (*Resource, error) {
r1, err := NewResource1() // Опускаю обработку ошибок.
r2, err := NewResource2(r1)
if err != nil { // А тут для примера нужно написать бойлерпрейт.
r1.Close() // Тут обработка ошибки не пропущена, так нередко и пишут.
return err
}
return &Resource{r1, r2}, nil
}
func (r *Resource) Close() error {
if r.closed { /* ошибка */ }
defer r.r1.Close() // Либо использовать multierror (гарантии для паники опциональны).
defer r.r2.Close()
return nil
}
type ResourceWithLog struct {
closed bool
*Resource
log *Logger
}
func NewResourceWithLog(log *Logger) (*ResourceWithLog, error) {
r, err := NewResource() // Опускаю обработку ошибок.
return ResourceWithLog{r, log}, nil
}
func (r *ResourceWithLog) Close() error {
if r.closed { /* ошибка */ }
defer r.log.Print("Конец освобождения ресурса")
defer r.r1.Close() // Либо использовать multierror, но нет гарантий для случая паники.
defer r.r2.Close()
defer r.log.Print("Начало освобождения ресурса")
return nil
}
Во втором сниппете меньше кода, меньше сущностей и больше гарантий успешной финализации. При этом можно без труда обернуть ресурс в io.Closer там, где требуется его закрытие (что-то вроде type DestructibleResource struct { *Resource; kdone.Destructor }) — а обратное с сохранением возможности отделить интерфейс финализации весьма трудоёмко. При этом само обозначение DestructibleResource (или ClosableResource) даёт понять, что ответственность за освобождение этого ресурса ложится на принимающую сторону.
Относительно освобождающей горутины я должен задать те же вопросы, что и в ответе на комментарий выше. Это всё же не совсем стандартный случай — или, по крайней мере, я неправильно понимаю, что вы имеете в виду.
В качестве примеров ресурсов можно привести соединение с БД, брокером, логгер, или хотя бы тот же файл. Память в Go обычно в качестве явного ресурса не выступает, но всё же могут быть отдельные случаи.
Свою реализацию я использую в другой своей библиотеке KInit, но это частный случай. А более общим примером сценария использования может послужить тот же Wire, чья задача состоит в генерации бойлерплейта инициализации — таким способом можно решить проблему паники в провайдерах и функциях очистки (которые, помимо прочего, можно научить возвращать ошибки).
Относительно вашего примера мне нужно больше контекста. Без конкретики могу сказать только, что если исходить из определения ресурса как зависимости, то здесь освобождение ресурса является частью его интерфейса, от которого зависит горутина, и допускаю, что типовой шаблон new — error — defer в данном случае не применим.
Но кто в данном случае владелец ресурса, а кто его просто использует (обычно зависимость трактуется как использование, а не владение)? Почему ресурсы создаются и освобождаются в разных горутинах (в некоторых случаях это может создать проблемы)? Есть ли другие горутины, зависящие от ресурса? Как осуществляется передача зависимости (вызов или канал)? Кто управляет самой горутиной (в частности, дожидается её окончания)? Как горутина декларирует то, что она будет освобождать переданный ресурс? Подход заставляет как минимум задуматься, где должен быть вызван деструктор по аналогии с тем, как явный возврат ошибки заставляет задуматься об её обработке в отличие от исключений :)
Что касается идиоматики, то это сложная тема)
Согласен, беспричинно нарушать идиомы языка — это плохая практика, поскольку код сразу же становится сложнее для понимания и плохо совместимым с уже существующей кодовой базой. Но суть любого фреймворка (не только для Go) как раз и состоит во введении правил организации кодовой базы. Кроме того, фреймворки обычно реализуют IoC. Так что без создания новых идиом и замены существующих им часто не обойтись — если бы эти идиомы уже существовали, то фреймворк был бы не нужен. В конце концов, даже сами языки меняют сложившиеся практики, объявляют их устаревшими вплоть до реальных ошибок и предупреждений при компиляции/интерпретации. Мне бы, например, очень хотелось, чтобы в Go появился более удобный механизм обработки ошибок, нежели существует сейчас. Если изменения оправданы — то почему они плохи?
У чисто утилитарных пакетов (таких, как драйвера баз данных, парсеры форматов, математические движки и т.д.) просто другая задача. Если, например, драйвер БД не будет совместим с database/sql/driver, то неминуемо потеряет в функциональности вплоть до того, что его применимость в реальных проектах окажется под вопросом.
Валидность состояния этого DTO — отдельный вопрос, обычно область ответственности самого DTO (тогда без публичные свойств, значения сервис получает по геттерам, а валидация происходит в конструкторе/сеттере DTO) или клиента, его передающего в сервис.
Мы приняли, что геттеры и сеттеры не соответствуют DTO, превращая его в анемичную модель (статья на тему). В сочетании с отсутствием контроля типов свойств в PHP это не позволяет возложить ответственность за корректность данных на DTO (я правда не понимаю, за что он тогда вообще отвечает).
Возлагать ответственность на клиента нельзя, поскольку клиент ничего не знает о типах данных, которые должны быть помещены в DTO (в лучшем случае он знает названия полей), и, естественно, ничего не знает о реализации сервиса, принимающего этот DTO. Кроме того, у клиента вероятно уже есть зона ответственности, иначе какой в нём смысл.
Остаётся возложить ответственность на сам сервис. Но это ничем не отличается от использования массивов, полным аналогом которых являются объекты с нетипизированными публичными свойствами (разве что скорость доступа к свойству несколько выше). Но массивы мы так же отклонили, а у сервиса есть другая ответственность, помимо проверки состояния другого объекта.
Могу предположить, что нам потребуется, с одной стороны, какая-то фабрика DTO (чтобы клиент мог корректно его создать), а с другой — его валидатор (чтобы сервис мог корректно его проверить). Результат — набор классов PersonDto, PersonDtoFactory и PersonDtoValidator. Архитектура так себе.
Главное отличие 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
}
Пока ваша библиотека не будет реализовывать хотя бы поддержку type hinting для аргументов и возвращаемых значений, она не может называться типизацией.
Например, контроль типа object появится только в PHP 7.2. А для типа resource никакого контроля кроме is_resource нет и, видимо, не будет. Не так давно не существовало и контроля скалярных типов.
В JS контроля типов нет совсем. Вы возьмётесь утверждать, что в JS нет типизации?
Но если очень хочется, то всегда найдётся старый как мир трюк) В PHP 7 это, конечно, не работает.
Ну и я всё-таки не понимаю, какое отношение библиотека имеет к контролю типов аргументов и возвращаемых значений функций, типам переменных (так и до типов свойств объекта недалеко), если её основное назначение — контроль типов входных параметров скрипта) При необходимости добавить такую функциональность не составляет труда, пусть и не в «нативной» форме — но я не сделал этого сознательно как раз во избежание.
Я не понимаю, каким критериям с вашей точки зрения должен удовлетворять тип и чем он вообще для вас является.
Объекты TypeInterface являются типами в том же смысле, в каком объект класса Order является заказом, а объект класса Car — машиной. Это не более чем абстракция, позволяющая описать понятие и взаимодействие с ним в рамках языка. Говорить о том, что объект TypeInterface является валидатором — это то же самое, что говорить, что объект класса Mail является отправителем, потому что у него есть метод send. Ну да, в какой-то мере так оно и есть.
Нет. Геттеры и сеттеры это методы доступа к данным. Для этого есть термин — анемичная модель.
Некоторые рассматривают массив как DTO, но это не правильно. Главное отличие DTO, от массива в том что объект имеет явный список свойств и он типизированный, что позволяет идентифицировать данные.
Как же тогда реализовать DTO в PHP?
JSON — объект только в js. В php же это просто строка, которую он может декодировать в массив или stdClass.
В JS JSON так же является строкой. JSON — это самостоятельный декларативный формат, такой же как XML или YAML. Во внутреннее представление в любом языке данные в этом формате преобразуются путём десериализации.
Это пример из баз данных которые делались как универсальный сервис хранения данных. В приложении можно создавать свои типы и поэтому такие универсальные типы не нужны.
Это не универсальные типы, а базовые. Они являются фундаментом для построения типов более высокого уровня.
Например, в системе типов PostgreSQL есть ряд базовых типов, который может быть дополнен администратором СУБД (базовые типы имеют специфическую внутреннюю реализацию, в том числе основанную на расширениях), и на основе этих типов могут быть определены типы более высокого уровня (которые собственной внутренней реализации не имеют).
Вы создали валидатор и пользоваться им нужно как валидатором. Как я и говорил ранее, правила можно комбинировать.
В этом и есть различие между типами и правилами — правила можно комбинировать по отношению к одному и тому же объекту, в то время как к двум типам одного уровня одновременно объект принадлежать не может.
Объект может принадлежать к типу «строка длиной два символа», который является подмножеством типа «строка», в свою очередь входящего в тип «строка или число». Тип «число» является надмножеством типа «целое число», в который включён тип «целое число без знака». И так далее. То есть тип полностью описывает объект на своём уровне.
Правила же не имеют никакой иерархии — они выглядят как тэги, характеризуя объект лишь частично.
А валидаторами справедливо называть сущности, занимающиеся проверкой как принадлежности объекта к некоторому типу, так и его соответствия некоторому правилу. Это понятие не самостоятельное — оно основано на выбранном способе описания объекта. То есть, если требуется уточнение, имеет смысл говорить о «валидаторе типа» и «валидаторе правила».
В этом случае
field('age', [int(), min(2), max(5)]);
в одно смешаны два подхода, что приводит к неочевидности использования, неопределённому поведению и даже парадоксам:
И даже если списать это на «граничные случаи», ни из чего не следует, что min и max нужно использовать только совместно с int, length — c string, а size — с lot. Задача помнить об этом переложена со среды разработки на программиста.
Поощряются так же такие конструкции, которые говорят об ошибках проектирования:
Зачем, кстати, проверять размер массива если нам важно только чтоб он соответствовал ожидаемой схеме если это ассоциативный массив? Если это нумерованный список то нам важен тип значения. Разве что у нас нумерованный список ограниченной длинны, но тогда его лучше описывать как структуру.
С объектами, кстати, тоже не понятно. Если вы проверяете пользовательские данные которые по определению не могут содержать объекты, зачем вам валидатор объектов?
Ассоциативный массив может быть просто картой, у которой фиксирован тип ключей и значений. Количество элементов в массиве (как ассоциативном, так и индексированном) имеет значение, например, если есть ограничение размера хранилища, пропускной способности канала или если за один запрос мы просто не хотим обрабатывать больше N элементов по соображениям оптимизации.
Объектный тип «оставлен на откуп» для случаев использования помимо валидации входных параметров скрипта. Так же как для структур будет добавлена поддержка stdClass, хотя я сам этот подход не использую.
Обычно, запросы от пользователя трансформируют в 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 с указанным классом. Отсутствие же класса я рассматриваю как очень частный случай.
Да — в какой-то степени) Тип определяется допустимым множеством значений и операциями, которые над ними допустимы. Валидация же — это проверка того, что значение соответствует конкретным условиям. То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация. В первом случае мы знаем, что можем сохранить значение на участке памяти размером (допустим) два байта и не можем прибавить его к числу без дополнительных преобразований. Во втором — мы уверены, что будучи сохранённым в базу данных, значение не нарушит её целостность.
Исходя из задач, для которых я сам предполагал использовать библиотеку 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. Публичные свойства объектов, на мой взгляд, плохая практика потому что нарушают инкапсуляцию. Хотя могу согласиться со спорностью в силу его непривычности и, следовательно, неочевидности. Просто мне такая реализация кажется удобной — буду признателен за примеры других вариантов.
В случае с вашими типами точно также нужно держать в голове и помнить.
В данном случае IDE сможет почти всё держать в голове за разработчика — и сами типы, и их параметры, и возможность их комбинации. Во всех этих случаях будут доступны подсказки и автодополнение, а любая ошибка в описании будет подсвечена и отражена в инспекторе. Можно сказать, что с этим расчётом библиотека и создавалась)
Неочевидны могут быть названия. IDE не сможет объяснить, что именно означает StructuralType, UIntType или, тем более, Union — при разработке я опирался на понятия из Си, но это лишь допущение дизайна. Здесь могут выручить только комментарии (плюс в том, что они легко доступны в IDE). Но от подобных проблем не избавлена вообще ни одна библиотека.
С другой стороны, если отказаться от примитивов в принципе, и использовать ВО с валидацией в конструкторе, вообще никакие валидаторы не нужны.
На этот счёт я описал свою позицию чуть ниже. Если коротко — в конструкторе так или иначе всё равно нужно выполнять проверки (то есть вопрос исключительно в способе) и есть некоторые проблемы с обработкой их результатов. Но именно в расчёте на этот метод проверки я и делал «дженерики») Да, во многих случаях такого подхода достаточно — каждому инструменту своё место, и использовать валидацию там, где и без неё всё нормально, конечно не стоит. К тому же нативные возможности языка в этом направлении развиваются.
Даже не кое-какая, а вплоть до их обработки в opcache.
Но IDE хорошо поддерживают без плагинов PHPDoc. Например property и method, совмещённые с магическими методами, позволяют в шторме делать интересные вещи) А вот аннотации без плагинов не поддерживаются ни в каком виде.
Плагины, расширения (в том числе синтаксические) дают массу дополнительных возможностей. Например, можно из PHP писать на JS) Но цена этих возможностей — дополнительные зависимости и ограничения, необходимость постоянно что-то «держать в уме» помимо задачи. Излишек всего этого отнимает много ресурсов и со временем может даже начать мешать поспевать за новыми возможностями самого языка. Например те же сторонние расширения с болью мигрировали с 5 версии на 7 (я уж не говорю про поддержку ZTS). А если всё-таки возьмут и примут очередной RFC по аннотациям? Особенно забавно будет, если их синтаксис будет основан на док-комментах)
На мой взгляд, нативные решения как правило проще, чище, их легче поддерживать и заменять при необходимости. С другой стороны, если не использовать «хаки», то потеряешь те возможности, которые они дают уже сейчас. Просто достаточно помнить, что это делается на свой страх и риск. Ну и выбирать из них те, что понадёжнее и действительно упрощают работу, а не просто клёво выглядят.
Women Who Code, Django Girls, Black Girls Code, Girl Develop It, She Codes, Ladies Code
Зачем вообще противопоставлять женщин и мужчин в данном случае? IT — это же не клуб по интересам, а профессия. Для профессионального сообщества ценной является возможность обмена опытом, которая с полом никак не связана. Даже если вы по какой-то причине считаете, что для женщин профессия IT является чем-то новым и им требуется дополнительная поддержка — тем более для них, как для любого новичка, важно вливаться в уже существующие сообщества, а не вариться в собственном соку.
Это док-комментарии, то есть просто строка комментария вида '/** My comment. */'. Аннотации — это механизм, в Java и C# он реализован на уровне языка (в том числе создание кастомных аннотаций). В PHP аннотации как часть языка были отклонены, и существуют только в форме библиотек, работающих на базе тех самых комментариев. То есть в самом PHP никаких аннотаций на данный момент нет.
Проблема с библиотекам та же — это код, но не на PHP, и потенциально вариантов синтаксиса может быть столько же, сколько проектов (сравните хотя бы RFC, доктрину, Notoj, Java, C#).
Кроме того, док-блок всего один и он уже задействован под PHPDoc и описание функциональности в произвольной форме. Библиотеки накладывают на его содержание ограничения, вводя свой синтаксис. Представьте, что у вас большая и тщательно документированная кодовая база, и вам нужно задействовать в ней новую библиотеку, использующую «аннотации». Шанс, что из-за собачки не в том месте будут потрачены несколько человекочасов, не так уж мал.
Этого мало, так ещё и нельзя быть абсолютно уверенным, например, что синтаксис двух одновременно используемых библиотек не будет конфликтовать) Причём обнаружить такие конфликты будет очень сложно, потому что они вне поля зрения PHP и тем более IDE.
Вы не сможете это сделать, ибо вы не знаете — что произошло с вашей переменной между проверками и та ли это переменная, что передается в проверку.
Если с данными что-то произошло, то это очевидно уже другие данные с другим дайджестом)
Речь о том, что как только был создан объект типа User, мы можем везде где нужно проверять переменные на тип User. Еще раз класс User — это тип, т.е. это описание структуры + валидация структуры.
User — это класс, объекты которого инстанцируются с использованием некоторых исходных данных, а не сами эти данные. То есть исходные данные нуждаются в проверке в любом случае, просто вы либо будете каждый раз писать кастомные проверки, либо использовать какой-то стандарт (не обязательно именно typedef — просто он должен быть). Как вы дальше будете использовать полученный объект — это уже не вопрос валидации. Вы, возможно, неправильно поняли идею — я не предлагаю валидировать данные внутри каждой функции, в этом нет смысла.
При создании объекта класса нет никакой разницы, почему провалилась проверка типа и объект был не создан — потому-что тайпхинт не пропустил аргумент в конструктор или внутри конструктора не прошла проверка.
Нет никакой разницы для программиста, но не для пользователя, который передал данные.
Ну, вы вообще в курсе, что конструктор не входит в понятие интерфейса класса? Или у вас к понятию «интерфейс» какое-то свое оригинальное определение?
Интерфейс — это несколько более широкое понятие, нежели «интерфейс класса») Интерфейс метода — это его сигнатура, а именно принимаемые аргументы, возвращаемый тип и выбрасываемые исключения. То есть всё то, что вы описываете в PHPDoc.
echo «ой ой, передали нам что-то не то»;
Важно ответить на вопрос, что именно не то и куда передали. В случае с вложенной структурой придётся оборачивать в try-catch каждый вызов конструктора на каждом уровне вложенности — и даже это не поможет понять, передали неправильно $name или $addresses, если вы не будете парсить сообщение об ошибке и сопоставлять номера аргументов. А ещё мы можем поймать не только TypeError, но и другие типы исключений, и с каждым надо как-то работать.
1. Дело не в количестве готовых правил — это как раз отлично — а в сложности реализации. Я не могу применить правило к данным — для этого мне нужен валидатор. Я не могу просто реализовать класс правила — для этого мне нужно реализовать ещё и класс проверятеля правила с особым названием (которое ещё и переопределить можно), который неявно инстанцирует валидатор. Причём узнать об этом из каких-либо сигнатур я тоже не могу, мне просто нужно знать что это так. Я не могу имплементировать интерфейс правила — нужно наследоваться от базового класса (причём для всего остального интерфейсы есть — поэтому все проверятели принимают на вход Constraint, а потом допроверяют его класс уже внути). Если следовать единству стиля, я ещё и должен сделать все свойства моего правила публичными и принимать их значения в виде ассоциативного массива. Всё не просто в доме, который построил Джек) И это не вдаваясь в группы, например. На мой сугубо личный взгляд, конечно.
2. Не хочется для каждой библиотеки, которую я использую, устанавливать плагины. И не хочется для каждой библиотеки изучать новый синтаксис, не имеющий отношения к базовому языку. А если я решу сменить библиотеку?
Что до использования кода, то массивы $options тоже не очень очевидны, надо знать набор свойств (их правда в классе правила подсмотреть всё же можно), ошибки будут видны в рантайме (и то если правило всё корректно проверяет, нативного контроля типов никакого же).
Во всяких джаво-шарпах аннотации во всю юзаются) и это круто как по мне.
В Java аннотации — это часть Java. В PHP аннотации — это часть симфони)
Тип проверять нужно в любом методе или функции, который ждет определенный тип аргумента и хочет с ним работать, а по факту получает 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 или её плагин не поддерживают именно этот валидатор.
Избыточная сложность. Это вполне можно понять, потому что компонент универсальный и старается быть подходящим для всего, что связано с проверкой, предусмотреть все возможные случаи использования, в том числе некорректные. Но два класса на одно правило, один из которых инстанцируется неявно, для меня слишком)
Неочевидность. Аннотации в doc-комментариях нужно в принципе знать и уметь — это отельный от PHP синтаксис, а все min-max, multiple, strict, format и прочие поля ограничителей нужно помнить.
Но вообще я конечно не возьмусь спорить с компонентами симфони)
О том, что вам каждый раз, когда нужно проверить соответствие типу — придется проходить по всему массиву я уж молчу.
Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды. Я не могу представить, зачем может понадобиться проверять тип одних и тех же данных несколько раз.
Между классом и вашим «типом» есть одна огромная разница — создание объектов, которые могут проверять валидность создания себя, а по-этому не должны быть в «невалидном» состоянии.
Вообще говоря, я не вижу никаких противоречий между использованием классов и валидацией данных. В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области. Это в любом случае так — если тип не проверит программист, то его рано или поздно проверит сам PHP и отреагирует на ошибки по своему усмотрению (увы, до сих пор не все ошибки являются исключениями).
Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно. Кто-то рано или поздно должен будет задаться вопросом, что же там содержится. Чем глубже это будет происходить по иерархии вызовов, тем дальше от места действительной ошибки на неё возникнет какая-то реакция (ещё хуже, если это произойдёт вообще где-то дальше по коду или, тем более, останется незамеченным).
Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.
По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.
Ну, есть валидатор описания структуры, который скажет что «неизвестный тип данных strrrng», или можно писать типа 'data' => type::string. '|'. type::int.
Но этот валидатор, скорее всего, не сможет вывести подсказку в IDE, а сообщит информацию в исключении. Так же для него, вероятно, не работает автодополнение.
Не говоря уже о том, что 'data' => 'string|object' вообще быть не должно, это такой большой подводный камень.
Я подразумевал, что он так и создаётся. Но ProvideAll связывает сущности, которые в общем-то ничем не связаны, кроме похожих зависимостей. Кроме того, вот в такой ситуации:
— Logger
— MetricCollector
— System (depends on: MetricsCollector)
— DBClient (depends on: Logger)
— MessageConsumer (depends on: Logger, MetricCollector)
— HTTPServer (depends on: MetricCollector)
— Application (depends on: System, DBClient, MessageConsumer, HTTPServer)
нельзя сделать какой-то один ProvideAll — для получения Application провайдеры для Logger и MetricCollector нужно указать явно, даже если эти зависимости для него транзитивны. Грубо говоря, создавая объект, нужно знать в точке его создания обо всех зависимостях его зависимостей, зависимостях их зависимостей и так далее. ProviderSet решает эту проблему, но лишь отчасти.
Я ни в коем случае не призываю использовать панику в качестве механизма обработки ошибок! :) Однако паники иногда всё же случаются, даже если нигде явно не писать panic. И, например, вот эта локальная база сломается, если её корректно не закрыть.
Для случая, если функция принимает именно io.Closer и её сигнатура не может быть изменена (случай с библиотекой), действительно придётся сделать обёртку типа
Чего-то типа io.CloserFunc в Go к сожалению нет :)
Таким образом мы просто делегируем освобождение ресурсов (в данном случае закрытие обработчиков) не вышестоящему коду, а вызываемой горутине.
Если же закрывающая функция имеет какое-то более специфическое поведение (например, применяет стратегию MRU для определения порядка), то Close — это уже часть более широкого интерфейса. Его можно скомпоновать (см. пример с DestructibleResource), но серебряной пули всё же не существует — возможно, освобождение ресурсов обработчика со временем станет лишь деталью реализации, а не публичным интерфейсом, и правильнее будет сформулировать зависимость конкретнее чем io.Closer или kdone.Destructor.
Из документации:
То есть, если вся остальная программа не знает про горутину, которая рассчитывает статистику, то при досрочном выходе из main до освобождения ресурсов дело просто не дойдёт (даже у GC не будет шанса вызвать финализаторы). Конечно, в тривиальных случаях Go сам позаботится об освобождении системных ресурсов (хотя это fallback и необходимости корректной финализации с записями в лог и т.д. он не отменяет). Но если финализация ресурса предполагает сложную логику, то есть риски — например, тут локальная база, не будучи корректно закрытой, окажется повреждена. Даже банальная синхронизация файла может не состояться, а СУБД со своей стороны может записать в лог ошибку неожиданного закрытия соединения.
Говоря про совместимость драйверов БД с database/sql, я имел в виду скорее то, что нарушится связность экосистемы — различные утилиты вроде golang-migrate полагаются на стандартный интерфейс и при всём желании не могут предусмотреть все возможные частные случаи. В таких ситуациях сохранение идиоматичности кода необходимо по вполне практическим соображениям. Однако многие драйверы расширяют стандартную функциональность, предлагая при этом частные подходы к написанию кода — и тут я полностью согласен, часто расширения намного удобнее стандартной реализации (т. к. максимально используют возможности протокола конкретной СУБД) и используются вместо неё. Это хороший пример того, что расширение возможностей языка не противоречит совместимости и оказывается полезным :)
Согласен, для простых случаев предложенный подход избыточен. Хотя он даёт гарантию времени компиляции, но всё же в трёх строчках кода это можно трактовать как ненужную сложность.
Но подход предназначен в первую очередь для сведения кода сложных конструкторов к привычному шаблону new — error — defer. Кроме того, деструкторы легко поддаются расширению и компоновке, в то время как io.Closer требует оборачивания для расширения и опять же финализации в другой функции (имплементации метода Close), что усложняет код.
Тут, как мне кажется, лучше привести пример кода.
Во втором сниппете меньше кода, меньше сущностей и больше гарантий успешной финализации. При этом можно без труда обернуть ресурс в io.Closer там, где требуется его закрытие (что-то вроде type DestructibleResource struct { *Resource; kdone.Destructor }) — а обратное с сохранением возможности отделить интерфейс финализации весьма трудоёмко. При этом само обозначение DestructibleResource (или ClosableResource) даёт понять, что ответственность за освобождение этого ресурса ложится на принимающую сторону.
Относительно освобождающей горутины я должен задать те же вопросы, что и в ответе на комментарий выше. Это всё же не совсем стандартный случай — или, по крайней мере, я неправильно понимаю, что вы имеете в виду.
В качестве примеров ресурсов можно привести соединение с БД, брокером, логгер, или хотя бы тот же файл. Память в Go обычно в качестве явного ресурса не выступает, но всё же могут быть отдельные случаи.
Свою реализацию я использую в другой своей библиотеке KInit, но это частный случай. А более общим примером сценария использования может послужить тот же Wire, чья задача состоит в генерации бойлерплейта инициализации — таким способом можно решить проблему паники в провайдерах и функциях очистки (которые, помимо прочего, можно научить возвращать ошибки).
Относительно вашего примера мне нужно больше контекста. Без конкретики могу сказать только, что если исходить из определения ресурса как зависимости, то здесь освобождение ресурса является частью его интерфейса, от которого зависит горутина, и допускаю, что типовой шаблон new — error — defer в данном случае не применим.
Но кто в данном случае владелец ресурса, а кто его просто использует (обычно зависимость трактуется как использование, а не владение)? Почему ресурсы создаются и освобождаются в разных горутинах (в некоторых случаях это может создать проблемы)? Есть ли другие горутины, зависящие от ресурса? Как осуществляется передача зависимости (вызов или канал)? Кто управляет самой горутиной (в частности, дожидается её окончания)? Как горутина декларирует то, что она будет освобождать переданный ресурс? Подход заставляет как минимум задуматься, где должен быть вызван деструктор по аналогии с тем, как явный возврат ошибки заставляет задуматься об её обработке в отличие от исключений :)
Что касается идиоматики, то это сложная тема)
Согласен, беспричинно нарушать идиомы языка — это плохая практика, поскольку код сразу же становится сложнее для понимания и плохо совместимым с уже существующей кодовой базой. Но суть любого фреймворка (не только для Go) как раз и состоит во введении правил организации кодовой базы. Кроме того, фреймворки обычно реализуют IoC. Так что без создания новых идиом и замены существующих им часто не обойтись — если бы эти идиомы уже существовали, то фреймворк был бы не нужен. В конце концов, даже сами языки меняют сложившиеся практики, объявляют их устаревшими вплоть до реальных ошибок и предупреждений при компиляции/интерпретации. Мне бы, например, очень хотелось, чтобы в Go появился более удобный механизм обработки ошибок, нежели существует сейчас. Если изменения оправданы — то почему они плохи?
У чисто утилитарных пакетов (таких, как драйвера баз данных, парсеры форматов, математические движки и т.д.) просто другая задача. Если, например, драйвер БД не будет совместим с database/sql/driver, то неминуемо потеряет в функциональности вплоть до того, что его применимость в реальных проектах окажется под вопросом.
Мы приняли, что геттеры и сеттеры не соответствуют DTO, превращая его в анемичную модель (статья на тему). В сочетании с отсутствием контроля типов свойств в PHP это не позволяет возложить ответственность за корректность данных на DTO (я правда не понимаю, за что он тогда вообще отвечает).
Возлагать ответственность на клиента нельзя, поскольку клиент ничего не знает о типах данных, которые должны быть помещены в DTO (в лучшем случае он знает названия полей), и, естественно, ничего не знает о реализации сервиса, принимающего этот DTO. Кроме того, у клиента вероятно уже есть зона ответственности, иначе какой в нём смысл.
Остаётся возложить ответственность на сам сервис. Но это ничем не отличается от использования массивов, полным аналогом которых являются объекты с нетипизированными публичными свойствами (разве что скорость доступа к свойству несколько выше). Но массивы мы так же отклонили, а у сервиса есть другая ответственность, помимо проверки состояния другого объекта.
Могу предположить, что нам потребуется, с одной стороны, какая-то фабрика DTO (чтобы клиент мог корректно его создать), а с другой — его валидатор (чтобы сервис мог корректно его проверить). Результат — набор классов PersonDto, PersonDtoFactory и PersonDtoValidator. Архитектура так себе.
Учитывая, что типизированных свойств в PHP нет, я не вижу способа создать объект, который соответствовал бы паттерну DTO так, как вы его определили.
Видимо, не только я. То есть если бы я назвал метод check вместо validate, то это был бы чекер?)
Так в PHP реализуются классы. Они конечно выполняют функцию типов, но этим дело не ограничивается ни для классов, ни для типов.
Объявление переменной в PHP выглядит так:
Всё остальное — это уже вызов конструктора и приведение типа, которые тип переменной никак не ограничивают. В данном случае и то, и другое — это просто операция по преобразованию одних данных ($input['code'] и $input['age']) в другие (CountryCode, int), ничем не отличающаяся от, например, вызова функции. И если первое преобразование производит валидацию значения, то второе попросту некорректно — при получении строки «шыснацать», которая тихо будет преобразована в 0 и в таком виде отправится дальше. Любое преобразование — это потенциальная возможность исказить данные, не говоря о накладных расходах в случае, если данных много.
Ничто не мешает сделать так:
или так:
Например, контроль типа object появится только в PHP 7.2. А для типа resource никакого контроля кроме is_resource нет и, видимо, не будет. Не так давно не существовало и контроля скалярных типов.
В JS контроля типов нет совсем. Вы возьмётесь утверждать, что в JS нет типизации?
Но если очень хочется, то всегда найдётся старый как мир трюк) В PHP 7 это, конечно, не работает.
Ну и я всё-таки не понимаю, какое отношение библиотека имеет к контролю типов аргументов и возвращаемых значений функций, типам переменных (так и до типов свойств объекта недалеко), если её основное назначение — контроль типов входных параметров скрипта) При необходимости добавить такую функциональность не составляет труда, пусть и не в «нативной» форме — но я не сделал этого сознательно как раз во избежание.
Я не понимаю, каким критериям с вашей точки зрения должен удовлетворять тип и чем он вообще для вас является.
Объекты TypeInterface являются типами в том же смысле, в каком объект класса Order является заказом, а объект класса Car — машиной. Это не более чем абстракция, позволяющая описать понятие и взаимодействие с ним в рамках языка. Говорить о том, что объект TypeInterface является валидатором — это то же самое, что говорить, что объект класса Mail является отправителем, потому что у него есть метод send. Ну да, в какой-то мере так оно и есть.
Как же тогда реализовать DTO в PHP?
В JS JSON так же является строкой. JSON — это самостоятельный декларативный формат, такой же как XML или YAML. Во внутреннее представление в любом языке данные в этом формате преобразуются путём десериализации.
Кстати говоря, декодировать JSON можно и в данные скалярного типа.
Это не универсальные типы, а базовые. Они являются фундаментом для построения типов более высокого уровня.
Например, в системе типов PostgreSQL есть ряд базовых типов, который может быть дополнен администратором СУБД (базовые типы имеют специфическую внутреннюю реализацию, в том числе основанную на расширениях), и на основе этих типов могут быть определены типы более высокого уровня (которые собственной внутренней реализации не имеют).
В TypeScript так же есть базовые типы, на основе которых новые строятся с помощью псевдонимов и интерфейсов.
Аналогичный подход вы увидите во многих языках со статической типизацией, которая не является чем-то применимым только для хранилищ данных.
Typedef основан на тех же принципах. Вот так (например) может быть определён тип country_code:
В этом и есть различие между типами и правилами — правила можно комбинировать по отношению к одному и тому же объекту, в то время как к двум типам одного уровня одновременно объект принадлежать не может.
Объект может принадлежать к типу «строка длиной два символа», который является подмножеством типа «строка», в свою очередь входящего в тип «строка или число». Тип «число» является надмножеством типа «целое число», в который включён тип «целое число без знака». И так далее. То есть тип полностью описывает объект на своём уровне.
Правила же не имеют никакой иерархии — они выглядят как тэги, характеризуя объект лишь частично.
А валидаторами справедливо называть сущности, занимающиеся проверкой как принадлежности объекта к некоторому типу, так и его соответствия некоторому правилу. Это понятие не самостоятельное — оно основано на выбранном способе описания объекта. То есть, если требуется уточнение, имеет смысл говорить о «валидаторе типа» и «валидаторе правила».
В этом случае
в одно смешаны два подхода, что приводит к неочевидности использования, неопределённому поведению и даже парадоксам:
И даже если списать это на «граничные случаи», ни из чего не следует, что min и max нужно использовать только совместно с int, length — c string, а size — с lot. Задача помнить об этом переложена со среды разработки на программиста.
Поощряются так же такие конструкции, которые говорят об ошибках проектирования:
Ассоциативный массив может быть просто картой, у которой фиксирован тип ключей и значений. Количество элементов в массиве (как ассоциативном, так и индексированном) имеет значение, например, если есть ограничение размера хранилища, пропускной способности канала или если за один запрос мы просто не хотим обрабатывать больше N элементов по соображениям оптимизации.
Объектный тип «оставлен на откуп» для случаев использования помимо валидации входных параметров скрипта. Так же как для структур будет добавлена поддержка stdClass, хотя я сам этот подход не использую.
Этап валидации присутствует в любом случае — так или иначе, в какой-то момент мы должны решить, соответствуют ли данные нашим ожиданиям, и можем ли мы с ними работать. Чем ближе это произойдёт к месту, где данные были приняты, тем меньше возможностей исказить данные, тем меньше точек отказа и тем меньше вызывающая сторона зависит от реализации стороны принимающей.
Например, в роли DTO можно рассматривать сам пришедший объект данных без всяких трансформаций — JSON-объект совершенно точно не имеет поведения) В этом случае нужно только проверить, что этот объект — правильный. Скажем в TypeScript можно было бы использовать для этого интерфейс, но в PHP интерфейсы имеют несколько иной смысл.
Кстати, интересный вопрос — а можно ли рассматривать в качестве DTO объект с геттерами и сеттерами, необходимыми при отсутствии типизированных свойств в PHP? Это же фактически поведение)
Вы весьма категоричны) Но я попробую привести пример — INT(11). 11 — это длина строкового представления числа, которое занимает 4 байта. Считать этот случай типом или валидацией?
Тип — это описательная сущность. Валидация — это действие, совершаемое над данными, и она может происходить в том числе в отношении их типа. В PHP в отношении типов присутствует полная свобода, но вообще данные некоторого типа имеют некоторое внутреннее представление, от которого зависит объём необходимой для этого представления памяти. И за пределами PHP это важно. Например, строка длиной в два символа может быть сохранена в колонке таблицы, соответствующей типу CHAR(2) (я не беру в расчёт кодировку), а целое число из диапазона 0..65535 может быть записано в переменную Си типа uint16_t.
IDE выдаёт подсказку, включающую имена аргументов.
Для целого числа задаётся диапазон, а не отдельные границы. Если требуется тип, открытый от нуля «вниз», то он явно специфичен. В остальных случаях границы диапазона известны.
Что касается uint — тип нелогичен в смысле названия. В данном случае имелся в виду количественный тип (cardinal), который ограничивается сверху и этот случай достаточно распространён. В остальных случаях речь идёт просто о целом числе в некотором диапазоне.
Проверка скалярных типов не работает на уровне входных данных скрипта. Если же просто передать неправильные значения в функцию с ограниченными типами аргументов, то результатом будет исключение TypeError, выдающее отладочную информацию о номере аргумента и позиции в коде. Кроме того массивы типизируются только через rest-аргументы и только линейно.
Это явно добавляет задачу помнить про ещё две функции, которые к тому же не имеют смысла вне контекста сущностей. Не может быть length без string, то есть мы нагружаем «длину» проверкой того, что значение является строкой — это крайне неочевидное поведение. В лучшем случае это могут быть методы тех же типов StringType и ArrayType.
В какой ситуации может возникнуть «просто объект», о классе которого нам ничего неизвестно? И как работать с объектом, интерфейс которого нам неизвестен? Для множества классов есть полиморфизм или в крайнем случае тип AnyType.
Так и ведёт себя тип object с указанным классом. Отсутствие же класса я рассматриваю как очень частный случай.
Да — в какой-то степени) Тип определяется допустимым множеством значений и операциями, которые над ними допустимы. Валидация же — это проверка того, что значение соответствует конкретным условиям. То есть «строка длиной два символа» — это тип, а «код страны, в которой у нашей кампании есть филиал, отражённый в базе данных» — это уже валидация. В первом случае мы знаем, что можем сохранить значение на участке памяти размером (допустим) два байта и не можем прибавить его к числу без дополнительных преобразований. Во втором — мы уверены, что будучи сохранённым в базу данных, значение не нарушит её целостность.
Исходя из задач, для которых я сам предполагал использовать библиотеку typdef, я выбрал термин «типы». Но «валидация» здесь может оказаться так же подходящим термином, зависит от использования. Это справедливо и для «дженериков».
uint — это беззнаковое целое, это его основной признак. Если речь идёт о некоем целом диапазоне, то логичнее использовать int — ведь диапазон может сместиться, в том числе в отрицательные величины. Включающие и исключающие диапазоны я решил не разделять, поскольку это решается простым прибавлением/вычитанием единицы. А для действительных чисел операция сравнения вообще плохо применима.
SRP — это принцип, а не парадигма, то есть им нужно руководствоваться, но чётких инструкций нет.
Валидаторы Min и Max не имеют смысла вне контекста чисел и будут выполнять ровно те же проверки — нативного типа и диапазона. Но если продолжать следовать принципу единственности ответственности, то их разновидностей будет достаточно много и это будет только путать.
Валидатор Length вообще не может существовать, поскольку длину имеют строки и массивы, но они являются принципиально разными сущностями — строка это конечно тоже массив, но не в PHP) Если совместить, то это нарушение SRP в чистом виде. Если сделать интерфейс LengthyTypeInterface с методом validateLength, то это ничем не будет отличаться от текущей реализации, кроме наличия дополнительной сущности. Если сделать Length только для строк, то это будет несправедливо по отношению к массивам).
Так, как вы описываете, реализован валидатор в симфони. Но у него немного иные задачи, чем предполагал для библиотеки я.
ObjectType без класса — это скорее костыль, недоработка в этом)
StructuralType предназначен для валидации не только массивов, но и объектов с ArrayAccess. Публичные свойства объектов, на мой взгляд, плохая практика потому что нарушают инкапсуляцию. Хотя могу согласиться со спорностью в силу его непривычности и, следовательно, неочевидности. Просто мне такая реализация кажется удобной — буду признателен за примеры других вариантов.
В данном случае IDE сможет почти всё держать в голове за разработчика — и сами типы, и их параметры, и возможность их комбинации. Во всех этих случаях будут доступны подсказки и автодополнение, а любая ошибка в описании будет подсвечена и отражена в инспекторе. Можно сказать, что с этим расчётом библиотека и создавалась)
Неочевидны могут быть названия. IDE не сможет объяснить, что именно означает StructuralType, UIntType или, тем более, Union — при разработке я опирался на понятия из Си, но это лишь допущение дизайна. Здесь могут выручить только комментарии (плюс в том, что они легко доступны в IDE). Но от подобных проблем не избавлена вообще ни одна библиотека.
На этот счёт я описал свою позицию чуть ниже. Если коротко — в конструкторе так или иначе всё равно нужно выполнять проверки (то есть вопрос исключительно в способе) и есть некоторые проблемы с обработкой их результатов. Но именно в расчёте на этот метод проверки я и делал «дженерики») Да, во многих случаях такого подхода достаточно — каждому инструменту своё место, и использовать валидацию там, где и без неё всё нормально, конечно не стоит. К тому же нативные возможности языка в этом направлении развиваются.
Но IDE хорошо поддерживают без плагинов PHPDoc. Например property и method, совмещённые с магическими методами, позволяют в шторме делать интересные вещи) А вот аннотации без плагинов не поддерживаются ни в каком виде.
Плагины, расширения (в том числе синтаксические) дают массу дополнительных возможностей. Например, можно из PHP писать на JS) Но цена этих возможностей — дополнительные зависимости и ограничения, необходимость постоянно что-то «держать в уме» помимо задачи. Излишек всего этого отнимает много ресурсов и со временем может даже начать мешать поспевать за новыми возможностями самого языка. Например те же сторонние расширения с болью мигрировали с 5 версии на 7 (я уж не говорю про поддержку ZTS). А если всё-таки возьмут и примут очередной RFC по аннотациям? Особенно забавно будет, если их синтаксис будет основан на док-комментах)
На мой взгляд, нативные решения как правило проще, чище, их легче поддерживать и заменять при необходимости. С другой стороны, если не использовать «хаки», то потеряешь те возможности, которые они дают уже сейчас. Просто достаточно помнить, что это делается на свой страх и риск. Ну и выбирать из них те, что понадёжнее и действительно упрощают работу, а не просто клёво выглядят.
Зачем вообще противопоставлять женщин и мужчин в данном случае? IT — это же не клуб по интересам, а профессия. Для профессионального сообщества ценной является возможность обмена опытом, которая с полом никак не связана. Даже если вы по какой-то причине считаете, что для женщин профессия IT является чем-то новым и им требуется дополнительная поддержка — тем более для них, как для любого новичка, важно вливаться в уже существующие сообщества, а не вариться в собственном соку.
Проблема с библиотекам та же — это код, но не на PHP, и потенциально вариантов синтаксиса может быть столько же, сколько проектов (сравните хотя бы RFC, доктрину, Notoj, Java, C#).
Кроме того, док-блок всего один и он уже задействован под PHPDoc и описание функциональности в произвольной форме. Библиотеки накладывают на его содержание ограничения, вводя свой синтаксис. Представьте, что у вас большая и тщательно документированная кодовая база, и вам нужно задействовать в ней новую библиотеку, использующую «аннотации». Шанс, что из-за собачки не в том месте будут потрачены несколько человекочасов, не так уж мал.
Этого мало, так ещё и нельзя быть абсолютно уверенным, например, что синтаксис двух одновременно используемых библиотек не будет конфликтовать) Причём обнаружить такие конфликты будет очень сложно, потому что они вне поля зрения PHP и тем более IDE.
Если с данными что-то произошло, то это очевидно уже другие данные с другим дайджестом)
User — это класс, объекты которого инстанцируются с использованием некоторых исходных данных, а не сами эти данные. То есть исходные данные нуждаются в проверке в любом случае, просто вы либо будете каждый раз писать кастомные проверки, либо использовать какой-то стандарт (не обязательно именно typedef — просто он должен быть). Как вы дальше будете использовать полученный объект — это уже не вопрос валидации. Вы, возможно, неправильно поняли идею — я не предлагаю валидировать данные внутри каждой функции, в этом нет смысла.
Нет никакой разницы для программиста, но не для пользователя, который передал данные.
Интерфейс — это несколько более широкое понятие, нежели «интерфейс класса») Интерфейс метода — это его сигнатура, а именно принимаемые аргументы, возвращаемый тип и выбрасываемые исключения. То есть всё то, что вы описываете в PHPDoc.
Важно ответить на вопрос, что именно не то и куда передали. В случае с вложенной структурой придётся оборачивать в try-catch каждый вызов конструктора на каждом уровне вложенности — и даже это не поможет понять, передали неправильно $name или $addresses, если вы не будете парсить сообщение об ошибке и сопоставлять номера аргументов. А ещё мы можем поймать не только TypeError, но и другие типы исключений, и с каждым надо как-то работать.
2. Не хочется для каждой библиотеки, которую я использую, устанавливать плагины. И не хочется для каждой библиотеки изучать новый синтаксис, не имеющий отношения к базовому языку. А если я решу сменить библиотеку?
Что до использования кода, то массивы $options тоже не очень очевидны, надо знать набор свойств (их правда в классе правила подсмотреть всё же можно), ошибки будут видны в рантайме (и то если правило всё корректно проверяет, нативного контроля типов никакого же).
В Java аннотации — это часть Java. В PHP аннотации — это часть симфони)
Если вы выполняете разные проверки на разных уровнях, то и содержимое массива (и его подмассивов любого уровня) имеет смысл выполнять там, где к нему осуществляется доступ, а на более высоких уровнях в таком случае достаточно просто знать, что это массив, чтобы не попасть на ошибку типа аргумента. Что-то вроде типизированного массива в PHP можно сделать например так (у этого способа есть свои ограничения):
Другой вопрос, что я бы не разделял проверки на множество уровней, да и массивы имеет смысл использовать в качестве аргументов с осторожностью, потому что их предполагаемое содержимое абсолютно не очевидно из сигнатуры метода (как минимум, оно должно быть подробно описано в PHPDoc).
Хотя вы мне подали идею — кэшировать результат проверки с использованием какого-то дайджеста проверяемых данных) Спасибо!
Это совсем не одно и то же. Есть интерфейс (имя — это строка) и его реализация (Ивановы живут в Иванове). Через какое-то время в Иванове разрешат жить Петровым, но имя всё ещё не сможет быть числом. А вот если мы начнём работать так же с больными шизофренией, то имя вполне может стать массивом.
Можно сказать, что тип — это охранник на входе, который определяет, кому вообще можно зайти, а кого надо выпроводить взашей, чтобы не мешал работать и не портил обстановку. Иначе будет проходной двор и учителю танцев с тонкой душевной организацией сломает ноги случайно залетевший погреться амбал.
Вернее так — 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::int, во-первых, такой синтаксис совершенно не читаем, и во-вторых, легко ошибиться вот так: type::string | type::int. Сами «операторы» такого синтаксиса автокомплиту не поддаются, если только IDE или её плагин не поддерживают именно этот валидатор.
Но вообще я конечно не возьмусь спорить с компонентами симфони)
Проверка данных на соответствие типу выполняется тогда, когда эти данные поступают на вход, и все проходы при этом выполняются единожды. Я не могу представить, зачем может понадобиться проверять тип одних и тех же данных несколько раз.
Вообще говоря, я не вижу никаких противоречий между использованием классов и валидацией данных. В том же конструкторе сначала проверяется «тип» данных (то есть их соответствие некоторой схеме), а затем уже их соответствие логике предметной области. Это в любом случае так — если тип не проверит программист, то его рано или поздно проверит сам PHP и отреагирует на ошибки по своему усмотрению (увы, до сих пор не все ошибки являются исключениями).
Пример. Вы указали, что $name должен быть строкой (это его тип), до проведения проверки, что это корректное имя (это его логика). А в случае с адресами вы можете получить на вход всё что угодно. Кто-то рано или поздно должен будет задаться вопросом, что же там содержится. Чем глубже это будет происходить по иерархии вызовов, тем дальше от места действительной ошибки на неё возникнет какая-то реакция (ещё хуже, если это произойдёт вообще где-то дальше по коду или, тем более, останется незамеченным).
Другой пример — нужно создать объект класса User, используя параметры, переданные на вход скрипта. Если просто передать в функцию $myJson['name'] (строка) и $myJson['addresses'] (тоже оказалось строкой), то до проверок дело вообще не дойдёт — будет выброшено исключение TypeError. И это если $myJson — это вообще массив.
По идее, объект отвечает за свою предметную область и проверяет правильность своего состояния и входных данных с этой точки зрения, и ни с какой другой. Соответственно самостоятельно заниматься проверкой того, что ему передали именно массив строк, а не массив неизвестно чего, он должен только в том случае, если это и является его прямой обязанностью.
Но этот валидатор, скорее всего, не сможет вывести подсказку в IDE, а сообщит информацию в исключении. Так же для него, вероятно, не работает автодополнение.
Пусть будет 'null|object')