Как стать автором
Обновить

Комментарии 118

Может, не разобрался, но разве после объявления class AccountNumber, который содержит магический метод _toString, не следует добавить implements Stringable (реализовать интерфейс Stringable)?

P. S. Подписался на ваш Телеграм-канал и прокачиваю самообучение PHP, разбираю подходы и принципы (из последнего особенно понравилось про статические замыкания, которым не нужен доступ к контексту объекта $this). Спасибо ;-)

Не совсем: )

Все классы, что реализовывают указанный метод автоматически (неявно) имплементируют интерфейс Stringable.

Спасибо за фидбек =)

Насчет интерфейса лучше документации никто не объяснит.

Интерфейс Stringable обозначает класс, реализующий метод __toString(). В отличие от большинства интерфейсов, Stringable неявно присутствует в любом классе, в котором определён магический метод __toString(), хотя он может и должен быть объявлен явно.

Другими словами - документация рекомендует объявлять явно, но если вы этого не сделаете, то всё продолжит работать.

Как быть если валидация зависит от контекста? Допустим наш Order может создаваться различными путями, и для всех этих путей должна быть разная валидация?

Как быть с историческими данными? Допустим у нас есть Order, которые раньше могли создаваться с неактивными продуктами. Затем у нас появилось новое правило, о том что Order можно создавать только с активными продуктами.

Как быть в случае, когда валидность Entity завязана на большой пласт бизнес логики, которая по сути находится за пределами этой Entity? Например. мы не можем создавать Order если товаров нет в нужном количестве на складе, но при этом, есть некоторые комбинации складов и товаров, которые допускают создания Order с товарами без наличия.

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

Что касается самого Order, то в вашем бизнесе это ведь что-то конкретное, например не важно откуда придёт заявка, она должна обладать email, телефоном и именем, или только email. Тут уже условия диктует как раз ваш бизнес. А вам нужно правильно перенести это на код.

Как быть с историческими данными? 

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

К последним двум абзацам отлично подойдет вот это видео. Там есть ответы на все ваши вопросы.

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

Поэтому валидация на сущности - это не всегда выглядит корректно, это работает только там, где нет изменения требований и\или исторических данных.

Если говорит конкретно про ваш кейс, то представим, что у нас простенькая доставка суши и нам всегда хватало только телефона. а вчера пришел бизнес и сказал, что хочет обязательно еще и email. Сразу возникает вопрос - становятся ли все старые заказы невалидными? наверное нет. С ними можно продолжать работать. Сразу возникают проблемы с деплоем, т.к. появляются три этапа - добавляем необязательное невалидируемое поле, потом добавляем инпуты и только потом добавляем обязательную валидацию. Но поле в БД все равно останется нуллабельным и мы не можем гарантировать остальным получателям данных по заказам, что там заполнен email

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

Такие кейсы очень хорошо описывать CHECK CONSTRAINT'ами в БД
ALTER TABLE products
ADD CONSTRAINT marking_code_is_not_null CHECK (created_at < '2021-07-07' OR created_at >= '2021-07-07' AND marking_code IS NOT NULL)

— БД гарантирует что данные консистентны и удовлетворяют CHECK'у если миграция прошла
— прямо в коде у нас зафиксирована дата изменения схемы данных
— программист который будет, например, писать отчёт, чётко видит из структуры данных что поле marking_code когда-то было пустым и не совершит ошибку. Например, не напишет INNER JOIN вместо LEFT JOIN там где это не надо

Вот тут дискутировал на эту тему:
habr.com/ru/post/521292/#comment_22142326

— прямо в коде у нас зафиксирована дата изменения схемы данных

Вот за такое делаем ментальное внушение тем, кто такое придумывает. Все изменения в DDL должны фиксироваться миграциями (если у вас конечно есть ревью изменений, тестовые контура, автотестирование и прочее). Если фиксировать дату в миграции... Задержали релиз на день - сидим хотфиксим. Релизим не ровно в полночь - сидим хотфиксим. Решили потестировать на куа базе - сидим хотфиксим.

БД гарантирует что данные консистентны и удовлетворяют CHECK'у если миграция прошла

Ваша БД - возможно. А если у вас несколько систем? А если у вас несколько внешний интеграций? Какие констрейнты вы будете отдавать им на работу с этими сущностями?

Да и в целом, держать констрейнты в БД - это конечно максимально надежно, не спорю. Но это значит что вы в своей бизнес логике дошли до момента сохранения данных в БД с невалидными данными. Т.е. все что вы сделали до этого (а это могли быть и нетранзакционные операции, например запросы во внешние системы) - невалидно

Вот за такое делаем ментальное внушение тем, кто такое придумывает. Все изменения в DDL должны фиксироваться миграциями (если у вас конечно есть ревью изменений, тестовые контура, автотестирование и прочее). Если фиксировать дату в миграции… Задержали релиз на день — сидим хотфиксим. Релизим не ровно в полночь — сидим хотфиксим. Решили потестировать на куа базе — сидим хотфиксим.

Вам разве религия не позволяет подставлять дату динамически?
SELECT CONCAT("ALTER TABLE products
ADD CONSTRAINT marking_code_is_not_null CHECK (created_at < '", NOW()
, "' OR created_at >= '", NOW()
, "' AND marking_code IS NOT NULL)"
)
INTO @sql
;
PREPARE stmt1 FROM @sql
;
EXECUTE stmt1
;


А если у вас несколько систем?

Если мы в обе наши системы пишем транзакционно, то надо задуматься о корректности архитектуры. Транзакция на 2 системы с обеспечением ACID — практически не реализуемая инженерная задача.

А если у вас несколько внешний интеграций? Какие констрейнты вы будете отдавать им на работу с этими сущностями?

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

Интеграции в которые мы пишем данные, да, тут есть допущения консистентности. Но ни первая причина не вторая, не является поводом отказа от БДшных констрейнтов, скорее даже наоборот, за них и стоит хвататься как за последний оплот надёжности )))
запросы во внешние интеграции я бы отправлял после всех БДшных чеков, и изменений БД, но до коммита транзакции в своей системе, если внешний запрос долгий, то тогда переводим на аснхрон со сменой статусов, обработкой, проверкой очередей и пр.
Да и вообще я сторонник того что все проверки надо делать в БД, там это наиболее просто реализуется, и наиболее быстро и корректно работает + гораздо реже допускает код который выстрелит в ногу

Вам разве религия не позволяет подставлять дату динамически?

Религия доставляет мне сложность катить миграцию идеально в то же время, в которое происходит релиз кода, который запрещает так делать или обрабатывает такую ситуацию. Это если забыть про откаты релизов, например. Вплоть тдо того, что у вас может быть какой-нибудь роллинг релиз и два часа у вас половина пользователей создает заказы с емейлом, а половина без, так как у вас идет поэтапный деплой. с какого момента мне выставлять дату ограничения если там через один идут заказы с емейлом и без? такой констрейнт просто свалится, какую бы дату я не поставил (потому что все равно возможен откат релиза)

Транзакция на 2 системы с обеспечением ACID — практически не реализуемая инженерная задача.

C обеспечением ACID - да, безусловно. Но если вы пишите распределенные системы, есть вполне конкретные паттерны, например saga.

 я бы отправлял после всех БДшных чеков, и изменений БД, но до коммита транзакции в своей системе

А вот за такие вещи часто делают внушение уже нам. Т.к. это приводит к тому, что в БД появляются очень долгие транзакции, часто еще и с блокировками (что деградирует вашу систему). Мы так часто делаем, да (но не для валидации), т.к. это просто, но не очень правильно.

Да и вообще я сторонник того что все проверки надо делать в БД,

Я лично за то, чтобы в БД были только те констрейнты, которые бизнес готов высечь на бумаге (если мы говорим в терминах DDD). Т.е. если бизнес готов сказать, что с полуночи 07.07.2021 мы заказы без емейлов дропаем, даже те, которые в 23:59 еще оформлялись в старом интерфейсе где нет емейла - то и ладно.

Религия доставляет мне сложность катить миграцию идеально в то же время, в которое происходит релиз кода, который запрещает так делать или обрабатывает такую ситуацию. Это если забыть про откаты релизов, например. Вплоть тдо того, что у вас может быть какой-нибудь роллинг релиз и два часа у вас половина пользователей создает заказы с емейлом, а половина без, так как у вас идет поэтапный деплой. с какого момента мне выставлять дату ограничения если там через один идут заказы с емейлом и без? такой констрейнт просто свалится, какую бы дату я не поставил (потому что все равно возможен откат релиза)

А мой код и динамическая-то дата чем Вас не устраивает? Она как раз лишена всех этих недостатков которые Вы описываете. В любом случае эти недостатки гораздо меньшее зло чем БД-данные в состоянии Шрёдингера.

А вот за такие вещи часто делают внушение уже нам. Т.к. это приводит к тому, что в БД появляются очень долгие транзакции, часто еще и с блокировками (что деградирует вашу систему). Мы так часто делаем, да (но не для валидации), т.к. это просто, но не очень правильно.

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

Но вариантов ещё много, например, коммитим данные со статусом сущностей NOT_COMPLETED, делаем долгие внешние пищущие запросы, завершаем бизнес транзакцию, переводя сущности в статус COMPLETED, конечно тут жертвуем консистентностью + нужны компенсирующие транзакции… та же Сага и получается

Я лично за то, чтобы в БД были только те констрейнты, которые бизнес готов высечь на бумаге (если мы говорим в терминах DDD). Т.е. если бизнес готов сказать, что с полуночи 07.07.2021 мы заказы без емейлов дропаем, даже те, которые в 23:59 еще оформлялись в старом интерфейсе где нет емейла — то и ладно.
Все эти проблемы лечатся динамической датой в констрейтах (см. выше)
Извиняюсь, я не правильно прочитал насчёт наката миграций и наката приложения. Но указанная Вами проблема лишь опосредованно относится к констрентам с датой. В общем случае, новый код не может работать без новой миграции или может работать не корректно, что ещё хуже, т.к. может повредить данные. И не встречал проектов где над этим бы заморачивались, т.е. релизили сначала код который поддерживает и старую и новую версию схемы БД, потом накатывали миграции, потом накатывали версию приложения которая работает только с новой схемой. Проще было пожертвовать несколькими десятками секунд простоя и накатить всё одновременно. Ну и я не могу придумать кейс когда бы новое правило консистентности, выкаченное на десяток секунд раньше, ломало бы систему.

На самом деле этой проблемы вообще нет, динамическая дата решает проблему того чтобы миргация накатилась, констрейнт начал действовать и под него не попали старые данные. Так что проблемы, которые Вы описываете, надуманные.

И не встречал проектов где над этим бы заморачивались, т.е. релизили сначала код который поддерживает и старую и новую версию схемы БД, потом накатывали миграции, потом накатывали версию приложения которая работает только с новой схемой. Проще было пожертвовать несколькими десятками секунд простоя и накатить всё одновременно. Ну и я не могу придумать кейс когда бы новое правило консистентности, выкаченное на десяток секунд раньше, ломало бы систему.

У нас почти так, только наоборот.

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

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

Проще было пожертвовать несколькими десятками секунд простоя и накатить всё одновременно. Ну и я не могу придумать кейс когда бы новое правило консистентности, выкаченное на десяток секунд раньше, ломало бы систему.

При поэтапном релизе (когда вы раскатываете новый релиз сначала на 10% инстансов например, потом на 50%, потом на 100%) несколько десятков секунд могут занимать пару часов наблюдения за раскаткой. И все это время у нас будут создаваться заказы и с емейлом и без. И все должно работать. Поэтому я не вижу большого смысла в констрейнте, который мы через несколько часов наложим.

проблемы нет, со статикой есть проблема:
1. если поставить завтрашнюю дату, а завтра не релизнуть, то послезавтра, миграция, с некоторой вероятностью, не накатиться в момент релиза и будет пожар
2. если поставить +неделю, то завтра у нас может заболеть девопс и понимая сложность текущего релиза мы можем совместно с клиентом принять решение перенести релиз на +2 недели и столкнуться с проблемой №1
3. мы можем поставить +месяц, но тогда послезавтра после завтрашнего обновления к нам прибежит клиент и спросит, а Вы же говорили что это реализовали, а оно не работает

констрейнт с динамическим временем лишён всех вышеперечисленных недостатов, на апп он не завязан, он накатывается всегда и начинает действовать с момента наката, идеален по всем составляющим

обновления к нам прибежит клиент и спросит, а Вы же говорили что это реализовали, а оно не работает

Ну это неправда, оно уже релизнуто и может находится в этом же a\b тестиорвании месяц, и пока мы ждем этот месяц мы все равно не можем раскатать эту миграцию. Поэтому она все равно так или иначе будет раскатана руками. Да, может с доп удобствами вида now(), но это не сильно принципиально здесь уже (лично для меня из этого треда, не говорю что вообще не принципиально)

Мне гораздо интересен другая часть мессаджа, а именно зачем нам вообще нужен такой констрейнт. И я как раз доформулировал кейс. Представьте себе банальное a\b тестирование. Оно может вообще месяцами идти. И при этом у нас параллельно работают две версии. Одна - с емейлами, вторая без. Т.е. весь код должен успешно работать длительное время без этого констрейнта. Зачем мы его будем накладывать?

Мне гораздо интересен другая часть мессаджа, а именно зачем нам вообще нужен такой констрейнт. И я как раз доформулировал кейс.

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

О, если вы покажете такой констнейнт на БД, я бы посмотрел. Необходимость маркировки у нас задается для конкретных категорий товаров с конкретной даты. Категории товаров - это дерево, в котором есть наследование (т.е. если маркировка обязательна для промежуточного узла, то и для всех дочерних тоже). А еще обязательность маркировки зависит от страны, в которой мы торгуем.

Я не говорю что валидация не нужна. Я конкретно про констрейнты на БД.

у Вас простой кейс. Уверен, проверку в БД реализовать просто, пришлите сокращённую схему, я смогу Вам помочь написать констрейнт
Есть классическая задача-решение от Тома Кайта, как реализовать бизнес-правило в БД, вроде, чтобы в отделе работало от 1 одного до 8 сотрудников, т.е. не менее одного и не более 8
Прочитайте, тогда Вы сами легко сможете решить свою проблему.

Ещё меня, в своё время, поразила, по уровню мышления, статья, как можно материализованными представлениями в Oracle поддерживать консистентность данных.
habr.com/ru/post/132727

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

Ну и конечно, шелуха проверок уровня апп пропадёт

На самом деле это был мой следующий вопрос, насколько удобно потом из БД эти ошибки выцарапывать, чтобы, скажем, скоммуницировать партнеру, который делает нам апи запрос (или того хуже, загружает файл), что вот, мол, ты нам загрузил неправильные данные вот в этом поле в этой строчке

не сложнее чем в апп

Ну вот здесь категорически не соглашусь. в апп валидации можно выполнять прямо на том DTO, что вы получаете при вызове API, на конкретных его полях. Отдавать ошибки на таких полях - 0 проблем, какое поле\объект валидировали - то в ошибке и указали. Когда дело доходит до сохранения в БД - это значит что у нас уже давно нет DTO, мы работаем с некоторой entity и валидацию выполняем на ней (пусть и в БД). И даже тут повезет и мы можем смапить обратно поля ентити на поля дто, то нам все равно нужно будет для каждого такого констрейнта индивидуально писать дополнительно обработчик ошибки в коде

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

проблема апп в том что
а) валидациии в нём могут работают не корректно, например, проверка на уникальность
б) в несколько раз больше нагружают базу
в) работают существенно медленнее

а) какое-то голословное утверджение. а могут работать и корректно, отвечу я вам. как напишете

б) валидации, которые максимально не используют БД нагружают ее больше, чем те, которые на каждый запрос пытаются выполнить блокирующую запись, я правильно вас понял?

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

а) приведите, пожалуйста, пример проверки на уникальность

б) как они могут не использовать БД? БД — единственная достоверная точка данных

в) да, а потом вы это поле пишите в БД и там тоже идёт проверка NOT NULL, 2 логические операции вместо одной
да, а потом вы это поле пишите в БД и там тоже идёт проверка NOT NULL, 2 логические операции вместо одной

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

В этом контексте бекенд даст существенную фору БД.

Поэтому вынос логики валидации из БД — это одна из форм разделения ответственности, которая облегчает масштабировани (да, путем большего объема затрат железа и человеческого труда)
Ещё раз повторю, БД — единственный достоверный источник данных. Поэтому отмасштабированное горизонтально приложение всё равно будет ходить в БД для проверок чуть сложнее чем NOT NULL, которые требуют согласованных по чтению и консистентных данных и ещё сильнее нагружать базу, т.к. в приложении валидацию писать это сильно не оптимально.
Мы с Михаилом, который написал комментарий ниже, проводили исследования на простых проверках, ну и БД была сильно впереди
habr.com/ru/post/521292/#comment_22144890

почитайте, если интересно, там в гите все исходники есть, попробуйте сами
Мы с Михаилом, который написал комментарий ниже, проводили исследования на простых проверках, ну и БД была сильно впереди

Не БД, а NodeJS, не вводите людей в заблуждение. Я же там в комментариях вам это объяснял.

Объяснили что?

Что "была сильно впереди" не БД, а NodeJS.


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

habr.com/ru/post/521292/#comment_22159512
Заменил пулл на коннект
fastify.addHook('preHandler', async (req) => {
//req.db = await db_pool.getConnection();
req.db = await mysql.createConnection(db_config);
и соответственно
req.db.close();
вместо релиза на закрытие, т.е. пул не использую вообще, полный аналог php получился, кол-во реквестов упало до ~600 RPS.

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


Вы же дальше сами отмечаете зависимость от языка "Но даже при Ваших 400 на ноде и 60 на php, с php явно что-то не то".


Проверять надо на одном железе и на одном языке, вот здесь я так и сделал.


То же самое, но с предварительной имитацией открытия страницы документа. Различие совсем небольшое, база всего в 1.2 раза быстрее приложения.
здесь Вы непонятно что с чем сравнивали, сами домыслили код и ПО и сами с собой сравнили

Пишите свой, кто вам мешает. Такой, чтобы были оба варианта, и с проверками в базе, и с проверками в приложении.

мы же уже подобное делали, писали БЛ, просадка была в разы
habr.com/ru/post/312134/#comment_9863962

Да, не в пользу БД.


Список из DB: 0.34 — 0.36
После добавления кэширования:
Список из app: 0.04 — 0.08

Из приложения в 4-8 раз быстрее.

кеширование это костыли костыльные

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

Ещё раз повторю, БД — единственный достоверный источник данных.

Я не совсем понимаю почему вы решили, что ваши фантазии имеют хоть какое-то отношение к реальности.

да, а потом вы это поле пишите в БД и там тоже идёт проверка NOT NULL, 2 логические операции вместо одной

И во сколько вы оцениваете улучшение производительности, если мы сэкономим одну проверку?

Михаил, Вы можете сами провести тесты, мы же с Вами писали приложения, все исходники есть

Это не является ответом на мой вопрос.


Таких проверок (машинная команда cmp по сути) на пути запроса из приложения в базу по протоколу TCP, через драйвера сети ОС, парсер SQL, драйвера дисковой системы, происходит хорошо если миллион, а скорее всего больше. Вы хотите сэкономить одну проверку из миллиона, усложнив код приложения специальной ловлей эксепшенов, парсингом текста и маппингом полей БД на поля интерфейса. К тому же повысив нагрузку на базу, так как раньше некорректные запросы в базу не шли, и она могла заниматься другими запросами, а теперь идут.


С учетом оценки в миллион команд cmp на пути выполнения одного запроса даже один лишний запрос в базу с невалидными данными убирает эффект от миллиона запросов с валидными.

нагрузка наоборот снижается, т.к. для проверки уникальности, Вы, например, делайте SELECT из БД, а я сразу пишу в БД
— да, а потом вы это поле пишите в БД и там тоже идёт проверка NOT NULL, 2 логические операции вместо одной
— И во сколько вы оцениваете улучшение производительности, если мы сэкономим одну проверку? К тому же повысив нагрузку на базу, так как раньше некорректные запросы в базу не шли, а теперь идут.

При чем тут проверка уникальности, если разговор был про проверку NOT NULL?)

разговор про валидацию вообще

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


Если хотите поговорить про валидацию вообще, тогда надо оценивать, сколько у нас проверок NOT NULL и сколько проверок на уникальность поля, с какой частотой они выполняются на продакшене.


Если NOT NULL у нас 90% полей с 1000 запросов на запись в секунду, а уникальность только на email при регистрации пользователя, которая происходит 10 раз в день, то ваш подход даст больше нагрузку на базу.

1000 запросов Ваше ПО не потянет. Т.е. Вы предлагаете не объявлять поля NOT NULL… ну ну… ну тогда уж и всё в текстовом виде храните и FOREIGN KEY не создавайте, зачем лишняя нагрузка на базу данных и уникальность можно в апп проверить и норм.
Т.е. Вы предлагаете не объявлять поля NOT NULL

Почему? Я предлагаю объявлять поля NOT NULL, только когда с фронта приходит NULL, такой запрос в базу не пойдет.


и FOREIGN KEY не создавайте

Как ни странно, иногда так и делают. А еще есть такая штука "микросервисы", и у них свои базы, и FOREIGN KEY в системе, которая из них данные берет, в принципе нельзя проконтролировать.

проверка на NULL то всё равно идёт, т.е. Вы в пустую расходуйте ресурсы приложения, да ещё код пишите лишний.
Иногда да, убирают FK, но это скорее исключение и проблем с контролем целостности прибавляется в разы. Это исключение которое подтверждает правило, что консистентность данных проще всего поддерживать в одном месте, в БД
проверка на NULL то всё равно идёт

Как она идет, если запрос не идет в базу?


что консистентность данных проще всего поддерживать в одном месте, в БД

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

но потом-то она идёт в БД и там опять проверяться
можно много других сложных путей выбирать…
но потом-то она идёт в БД и там опять проверяться

Нет, если с фронтенда пришло пустое поле, запрос в БД не идет. Поэтому каждый такой случай экономит миллион сравнений.

ну не миллион, а исключительную ситуацию, конгда фронт это пропустил, т.е. 1 на миллион
и опять же проверяться она же в любом случае, т.е. у Вас оверинжиниринг приветствуется

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


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

Кстати, посмотрел я те исходники, которые вы писали якобы для тестов.

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

Надо ли сказать, что все это можно переписать в гораздо меньшее количество запросов? И получить совсем другие цифры?

Не понял, какое место не нравится?

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

можно строчку кода и как должно быть, по Вашему мнению

get_app_list.php

$data = [];
foreach ($materials as $material) {
	$price = get_client_price($material, $doc);
	$data[] = ['name' => $material['name'].' - '.$price.' - '.$material['kol']];
}

3 строка - это вызов нескольких запросов внутри цикла.

И, да, это можно и нужно свести к join и предвычислениям. Даже если засунуть в один запрос будет слишком сложно, то уж ограничиться до десятка запросов - можно.

У вас же тот код выполняет 1000+ запросов к БД.

Вы не поняли, я как раз категорически против такого кода. Михаил, с которым мы дискутировали в той ветке, считает что подобным кодом и надо программировать, отгородившись от БД всякими ОРМ'ами. А в этом коде я на примере показал, что бывает если на прикладном языке начинать решать не свойственные для него задачи

Михаил, с которым мы дискутировали в той ветке, считает что подобным кодом и надо программировать, отгородившись от БД всякими ОРМ'ами.

Ничего подобного. Я не знаю, с чего вы решили, что использование ОРМ означает запросы в цикле.


Вот пример, ваял на скорую руку.

Каким образом то, что вы ваяли на скорую руку, говорит что-то о том, что считаю я? Вы предоставили код, я использовал его как есть без всяких доработок.


А в этом коде я на примере показал, что бывает если на прикладном языке начинать решать не свойственные для него задачи

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

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


Безотносительно позиции вашего визави и его категоричности относительно БД — данный вопрос решается версией схемы данных.

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

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

Звучит уже слишком сложно на уровне валидации констнейнтов в БД. это получается надо где-то на уровне сессии задавать "фиче тоглы", которые должны учитываться при работе констрейнта? Я не слишком скиловый БД разработчик, поэтому я сходу даже не скажу, заработает ли это на %databasename%. Не говоря уже о том, что это начинает походить на мем про буханку.

Формально различные флаги тоже должны войти в версии схемы данных и да, учитываться.

Но вообще-то это справедливо и для уровня приложения-то :)
При общении с БД код должен будет учитывать отличие схем (например, путем реализации собственных абстракций доступа и маппинга данных в рамках фич)

P.S. если что — я не топлю за использование constraints в рамках БД если они сложнее чем foreign key. Просто вышеуказанные проблемы имеют решение даже для идеи вашего визави.

Не, тут же наоборот, не код должен знать поо схему, это как раз нормально, миграции версионируются и код знает о них, какие применены в какой версии приложения. Наоборот, с одной схемой БД работают разные версии приложения и БД при обработке запросов должна эту версию получить (через сессию или запрос) и учесть в работе констрейнта

Динамическая дата не спасает при роллинг релизах. В БД уже есть констрейт, а половина инстансов приложения еще со старым кодом и шлет запросы, которые будут фейлится на этом констрейнте
А что спасает-то? Пусть шлют, пусть фейлятся, для этого констрейнт и добавляют, чтобы новые данные соответствовали новым бизнес правилам
Я боюсь, что ваши клиенты будут не слишком рады такому повороту событий и уйдут к тем конкурентам, у которых заказы не фейлятся

Спасут проверки в приложении. Новая версия будет отшивать кривые заказы и показывать адекватную ошибку. Человеку понятно, что нужно дозаполнить что-то и он видит это что-то на форме, потому что это новая версия приложения.
Старая версия сохранит без мыла и его даже можно будет потом вручную дозаполнить при контакте с клиентом
Все заказы приняты, все довольны
Я нигде не писал, что запрещаю обрабатывать ошибки в приложении. Просто если бизнесу нужно новое правило, то самое место его прописать в БД.
А то у вас не правило получается, а какая-то рекомендация, типа новые версии уже с мылом, старые без мыла, которое можно дозаполнить… т.е. мы никогда не будем уверены, что у нас данные с мылом. Ну если такая нужна ситуация, то не надо констрейнта в БД
Всё по ситуации и зависит от требований
Ок, жду с нетерпением ваших следующих статей.

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

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

Когда я говорил про бизнес логику которая не зависит от Entity я имел ввиду всякие методы по типу canOrderChangeStatus() или про методы isValid(). То есть не про простые кейсы что email это действительно email, и что у единорога есть рог, а про что то сложное, что требует каких то сложных зависимостей.

Видео действительно классное, спасибо за него.

По идее, заказ можно создавать с неактивными товарами, а вот добавить неактивный товар в корзину - нельзя. Тогда для получения старых заказов (где есть вероятность попадания неактивных товаров) вы собираете заказ напрямую по списку, а для оформления пользователем нового заказа - создаёте его из корзины.

Получается в статье описан 1 контекст. А у вас — 2. Тогда получается будут 2 Entity.
Вы правы, я хотел указать именно на то что в статье мало написано про контексты. В случае с историческими данными чаще всего делают два разных Entity, но когда правила меняются часто, то это приводит к огромному количеству классов, с которыми тяжелее работать чем с одним. Тут надо искать компромиссы, а не слепо следовать лучшим практикам.
Например физический (реальный) самолет сделан так, что он имеет неровности, шероховатости и неоптимальную форму. Теоретический самолет сделан так, что он не имеет недостатков. Я могу «представить» себе идеальный самолет. Или идеальный стол или стул. Они существуют, но лишь в моем уме. Теоретический самолет (или машину например или еще что-то) можно лишь вообразить в уме. Так и эти архитектурные шаблоны. Глупо/неправильно представлять в уме программу, в которой существует entity и она в невалидном состоянии. Это как представлять некрасивый стол или стул. Фантазии всегда идеальны. Если бы мы программировали «мозгом», то наверное мы смогли бы написать такой код. Но поскольку как и физический самолет мы ограничены технологиями, то реализовать «чистый код» — трудно (например можно реализовать в очень маленьких программах).

Вам бы чуть-чуть дополнить статью, упомянуть что Rich Domain не единственный вариант, бывают ещё и другие. А то получается двояко - вроде и по делу, например, про Entity написано, и одновременно нет - вполне может быть вариант, например, где ответственность за правильность создания сущности (сообразность укладываемых в нее данных) лежит на пораждающем классе, а сама сущность вообще immutable.

Могу я вас попросить привести пример кодом? Потому как даже если сущность immutable, то всё равно она создаётся через конструктор и она может и должна сама себя валидировать.

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

Entity не должна существовать в вашем приложении если внутри неполные или невалидные данные.

Но если не передать id в конструктор (auto increment ведь ?), то она в любом случае может быть не полной. Или все таки "иногда можно" ? )

Все красиво на бумаге...

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

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

Что касается auto increment, для этого тоже придумали механизм, в postgresql он называется sequences, копать туда.

Ну и не могу не привести короткий пост из канала :)

Entity не должна существовать в вашем приложении если внутри неполные или невалидные данные.

То есть вы предлагаете, например, исключительно во всех случаях при выборке из базы данных вытаскивать все поля таблицы (SELECT *)? Надеюсь, что нет, особенно если у вас есть поля с JSON. А если не все поля, то Entity, полученные в результате такого запроса, будут не полными.

Противоречия тут похоже что нет: ленивая загрузка полей класса достаточно распространена среди ORM

Но еще тут не говорится что Entity - это сущность ORM (доктрины :D)

Но все всё поняли )

А имеется ввиду что в любой момент времени сущность должна быть "валидна" (в целом согласен). Но все ломается об автоинкремент )

И получается "в любой момент времени кроме ..."

Нет нет, Entity тут упоминается в контексте DDD. Ни о какой доктрине нет речи.

Про автоинкремент ответил выше, ничего не ломается и можно без "кроме" ;)

Я думаю entity и инварианты имеют смысл при операциях изменения данных. В этом случае да, лучше достать все необходимое

Если же Вы просто читаете данные, то доставайте только то, что действительно нужно

Перестаньте думать о базе данных. Речь идёт именно о проектировании структуры и самой сущности. Конечно, есть много случаев, когда нам нужно считаться с нашей базой (высокая конкуренция запросов, deadlocks и т.д.), но идея в том, чтобы перестроить мышление и думать об объектах, как о вещах в реальном мире, а не как о данных в таблице. Маппинг данных в это отдельная задача.

Entity нам нужны для бизнес операций, то есть когда в нашей системе происходят какие-то изменения. Если нам нужно просто вывести какие-то данные - сущности скорее всего вам не нужны, тут читать в сторону Read Model.

Полноту данных в вашей сущности определяет не таблица в БД, а условия бизнеса. Если у вас в JSON хранится какая-то информация, которую надо проверить перед изменением Entity - придётся её достать. Тут вы уже должны сами проанализировать насколько правильно спроектирована БД под ваши бизнес операции, а не наоборот.

Entity нам нужны для бизнес операций

Ну вот простая бизнес-операция, отправить пользователю email при создании заказа. Вы его из сущности будете отправлять?

При сохранении сущности можно бросить событие, а уж кто его перехватит и что сделает- на совести обработчика

Ну а event bus вы будете в сущность пробрасывать, или он все-таки находится в вызывающем коде, где находится и зависимость от конкретного класса OrderCreatedEvent, связанного с этим бизнес-действием?

Сущность ни от чего не зависит, в неё ничего не пробрасывается, она просто может в себе список событий содержать (события- просто классы)

Сервис, ответственный за сохранение сущности (ну, непосредственно за перекладывание её в БД) уже имеет доступ к event bus'у, и он бросает события

Ну я об этом и говорю, есть код, который относится к конкретному действию бизнес-логики, но находится не в сущности.

Почему из сущности?

Сущность сможет вам гарантировать, что был указан корректный email (с точки зрения ввода), присвоить заказу номер в системе, а отправкой будет заниматься другой сервис, который уже будет использовать заведомо проверенные данные если мы ему передадим валидный order.

а отправкой будет заниматься другой сервис

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


Тут вот как получается. Сущность сама себя создавать не может. Значит есть другой код, который это делает. Это он вызывает конструктор сущности. После создания нужно сделать дополнительные действия, связанные с данным бизнес-действием — отправить письмо или в общем случае кинуть событие, сделать persist. На примере Доктрины это будет для создания persist+flush, для удаления remove+flush, для просмотра ничего. Принципиальной разницы нет, обращаемся мы для выполнения бизнес-действия к базе или к почтовому серверу.


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

появляется термин "создание заказа", это ее реализация относится к инфраструктуре и находится в другом сервисе.

Находится он не в инфрастурктуре, а в application layer, это же бизнесовое действие. Да это юзкейс/интеррактор/комманд хендлер, не принципиально. На вход получает команду (например DTO, который составлен из пользовательского реквеста), и дальше передает его на создание, сохранение, отправку почты. Вот пример видоса, где можно посмотреть реализацию на похожем примере.

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

Не важно, поднимаем мы сущность из команды или из стейта, она прекрасно с этим может справиться с помощью именованного конструктора. Смотрите паттерн репозиторий и fromState сущности Post

После создания нужно сделать дополнительные действия, связанные с данным бизнес-действием

А что вас смущает? всё находится в рамках одного юзкейса, одного сценария.

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

Приведите пример с кодом пожалуйста, я не совсем уловил мысль, как это получается :(

Дальше по комментарию я к сожалению не уловил мысль, но мне кажется мы говорим о разных подходах, которые уже никак не относятся к валидации.

Если речь о построении бизнес процесса, то выше я как раз привёл в пример видео, чтобы было понятно о чем речь.

Вот пример видоса, где можно посмотреть реализацию на похожем примере.

Там говорится примерно про то же самое, только называется use case, а не сервис.


А еще у него там всякие static-вызовы для валидации в конструкторе типа Assert::notEmpty(). Зависимость, скрытая в static-вызове, это все равно зависимость. Почему бы тогда и другие зависимости так же не использовать, написали static-вызов для отправки email из конструктора и всё. Или для подгрузки полей из базы как в ActiveRecord.


она прекрасно с этим может справиться с помощью именованного конструктора

Какая разница, вызываете вы new Entity() или Entity::newEntity(), этот код все равно должен где-то вызываться, и этот вызывающий код связан конкретно с бизнес-действием создания.
Код в статической функции все равно находится вне жизненного цикла сущности. Если вы там будете делать проверки и бросать исключения, соответствующие конкретно этому методу, это то же самое, что и делать их в вызывающем коде, только без дополнительной обертки try-catch.


А что вас смущает? всё находится в рамках одного юзкейса, одного сценария.

То, что бизнес-логика все равно находится не в сущности, которая сохраняется в базу.


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

В этом видео есть на 14:17.


namespace Model\User\UseCase\Signup\Confirm;

class Handler
{
    function handle()
    {
        ...
        $user->confirmSignUp();
    }
}

Только тут каждое бизнес-действие в отдельном классе, поэтому дублируются название класса и название метода: Signup\Confirm и confirmSignUp. Если бы для действия требовались дополнительные параметры, они бы брались из входа метода handle и передавались в метод сущности. То, что они для handle объединены в Command, принципиально ничего не меняет.


Только Command уже провалидирована, и мы можем на это полагаться, а для метода сущности надо раскладывать ее на отдельные параметры, и потом внутри делать те же проверки.


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

Как раз к валидации, потому что есть много случаев, когда валидность данных сущности зависит от дополнительного состояния, которого нет в самой сущности. Поле делать пустым нельзя, но если заполняет главный мененджер, то можно.
Или например что значение поля должно существовать в базе в некоторой таблице (ссылка на другую сущность). Вы можете передать в конструктор другую сущность и даже проверять у нее непустой id, но проверка, что он существует в базе будет находиться вне сущности.

Данное видео я привел в пример просто чтобы мы засинхронились и говорили на одном языке.

Зависимость, скрытая в static-вызове, это все равно зависимость. 

Не всё что в вендоре является кодом инфраструктуры. Вот отличная статья Нобака на эту тему.

Какая разница, вызываете вы new Entity() или Entity::newEntity()

Разница в том, что код маппинга находится в именованном конструкторе, а валидация в основном. У каждого метода есть своя причина для изменений и одна ответсвенность.

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

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

То, что бизнес-логика все равно находится не в сущности, которая сохраняется в базу.

Так, кажется я начал понимать в чем дело. Фамилия Бугаенко вам о чём-то говорит?)

Только Command уже провалидирована, и мы можем на это полагаться, а для метода сущности надо раскладывать ее на отдельные параметры, и потом внутри делать те же проверки.

А вы можете гарантировать, что command валидна? Она не содержит проверок в конструкторе. Это просто структура данных. Другой разработчик может легко создать такую команду в другом месте и впихнуть её вам забыв провалидировать.

Да, проверки для данных пользователя это одна задача. Там задача помочь пользователю правильно ввести данные. Помочь пользователю. Помочь. А проверки внутри бизнес логики другая задача - защищать свои бизнес правила. Не смотря на то, что они будут пересекаться - это разная валидация, на разных уровнях и решает разные задачи.

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

Получается, что при изменении вашего поля, помимо текста вам нужно передать автора. Прямо в метод сущности.

class SomeOrder
{
    //..
    public function changeSomeField(Author $author, Field $field)
    {
        if (! $author->isModerator()) {
            // exception
        }
        
        $this->field = $field;
    }
}

проверка, что он существует в базе будет находиться вне сущности.

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

Не всё что в вендоре является кодом инфраструктуры.

Я и не говорил, что это код инфрастурктуры, это код компонента валидации. Если static-вызовов не будет, вам надо будет пробрасывать в конструктор сущности дополнительную зависимость (от сервиса в терминах Symfony DI).


— Какая разница, вызываете вы new Entity() или Entity::newEntity(), этот код все равно должен где-то вызываться, и этот вызывающий код связан конкретно с бизнес-действием создания.
— Разница в том, что код маппинга находится в именованном конструкторе, а валидация в основном.

Каким образом местонахождение кода маппинга меняет факт, что любой из этих конструкторов должен где-то вызываться, и этот вызывающий код связан конкретно с бизнес-действием создания? Если не меняет, значит в этом отношении разницы между new Entity() или Entity::newEntity() нет.


Фамилия Бугаенко вам о чём-то говорит?

Нет.


А вы можете гарантировать, что command валидна? Она не содержит проверок в конструкторе.

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


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


Передача параметров в виде структуры, проверяющей свою валидность, это что-то вроде контрактов на аргументы в C#. То есть например в функции divide(DivideArgs args) можно быть уверенным, что args.b всегда не равен 0, и не делать соответствующие проверки с исключениями, а делать только бизнес-логику действия return args.a / args.b;. Все функции так писать нет смысла, а вот функции с бизнес-логикой вполне.


Получается, что при изменении вашего поля, помимо текста вам нужно передать автора. Прямо в метод сущности.

Как бы да, только автор у нас уже есть в вызывающем методе/классе, который тоже соответствует действию changeSomeField и тоже содержит часть бизнес-логики (а не является например веб-контроллером).
Я согласен, что так можно делать, просто как я и сказал, методы сущности дублируют методы вызывающего кода. Не по реализации конечно, а по назначению, параметрам и проверкам.


А если разделять функциональность на отдельные пакеты/модули, которыми занимаются разные команды, тогда тем более нельзя разрешать создавать сущности некоторого пакета в коде других пакетов в обход интерфейсов, которые предоставляет этот пакет. А интерфейсы будут вести на сервисы/юз-кейсы, а не напрямую на сущности.


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

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

Хотя казалось бы, сервис валидации, получающий на вход только набор сущностей необходимых для валидации и решающих только одну задачу — валидацию, куда лучше в плане decoupling.

Вам придется делать бесконечные и ненужные геттеры внутри Entity (ведь для валидатора данные нужно будет как-то извлечь),


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

следить за тем что нужно обновить сервис в случае изменения самой сущности


И вы тут же создаете Value class для валидации, в котором вам надо будет делать изменения. А еще могут меняться правила валидации без изменения модели.

и не забывать его вызвать каждый раз при её создании.


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

Дайте пожалуйста констуктива, мне нравится, что есть другое мнение, но пока нет конкретики говорить не о чем.

В чем оверинжениринг, если правила валидации будут одни и те же, что в случае сервиса, что в случае валидации в сущности.

Когда у вас есть сервис валидации отдельно, а сущность отдельно, то есть возможность создать сущность в обход валидации, просто не вызвав сервис (не знали, забыли и т.д.)

Я не говорю, что геттеры сложно создать, нет, но если вы их генерируете только для валидации, то вам не кажется, что что-то вы делаете не так?

Насчёт VO - они используются внутри сущности и не надо будет делать изменения в 2х местах, достаточно только в VO. В случае с сервисом - вам придется затронуть и сущность и сервис.

Приведите пример пожалуйста, когда в одной сущности у вас 2 набора правил валидации. Не для Request объекта, а для сущности.

В чем оверинжениринг, если правила валидации будут одни и те же, что в случае сервиса, что в случае валидации в сущности.


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

Когда у вас есть сервис валидации отдельно, а сущность отдельно, то есть возможность создать сущность в обход валидации, просто не вызвав сервис (не знали, забыли и т.д.)


И это хорошо. Валидация — это не всегда легкая операция. В ней могут быть в том числе запросы к внешним API. Типичный пример — что добавляемый клиент/точка находится в сфере обслуживания вашей компании, для чего используется Google maps api. Обычно эти сущности добавляются операторами и валидация нужна. А потом вам присылают список сущностей про которые вы точно знаете, что они соответсвуют критерию дальности и дергать API лишний раз не надо.

Я не говорю, что геттеры сложно создать, нет, но если вы их генерируете только для валидации, то вам не кажется, что что-то вы делаете не так?


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

Насчёт VO — они используются внутри сущности и не надо будет делать изменения в 2х местах, достаточно только в VO. В случае с сервисом — вам придется затронуть и сущность и сервис.


В случае сложной валидации вам придется пихать в VO все что по хорошему там не нужно, доступ к API например, вплоть до разруливания асинхронности доступа к этому API. Не уверен, что после такого VO все еще сохранит свой смысл. Во-вторых ситуация когда нужно менять валидацию а не модель — не такая уж и редкость.

Приведите пример пожалуйста, когда в одной сущности у вас 2 набора правил валидации. Не для Request объекта, а для сущности.


Когда сущность создается из доверенного и недоверенного источника. Непроверенные клиенты не могут заказать доставку больше скольких-то единиц товара. Или когда один пользователь может вставлять внешние ссылки в публикуемыей материал, другой — нет, потому что у первого премиум аккаунт. Или когда заказ по Москве может быть доставлен на одних условиях, а по СНГ — на других.

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

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

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

Когда сущность создается из доверенного и недоверенного источника

Опять же, можно сделать разные сущности, либо разные конструкторы

У Вас ведь получается, что заказ проверенного клиента - это не то же самое, что непроверенного клиента, там наверняка ещё какая то разница есть, кроме количества товара

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

Это уже какой-то новый уровень валидации....

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

Не новый, это да. Получается, ваше Entity должен уметь хотеть родиться потому что… и эти "потому что" вы предлагаете наваливать в виде "именованных конструкторов". Ухипоевший подход в статье про "давайте Я вам покажу как делать сервисы валидации".

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

При чем тут понятие "Оверинжениринг" к "не прижилось"? Если не можете сами объяснить в чем минус - скиньте статейку или книгу, но пока я не увидел ни одного примера или строчки кода =(

что добавляемый клиент/точка находится в сфере обслуживания вашей компании, для чего используется Google maps api.

"Добавляемый", то есть есть какой-то менеджер, который выбирает координаты, которые нам нужно проверить, верно? Значит он отправляет нам Request. То есть тут мы валидируем пользовательские данные, и используем внешний апи, чтобы определить например входит ли адрес в зону доставки. Сущности как таковой еще нет, это пользовательская валидация.

Или генератор геттеров.

Звучит как что-то зловещее :)) я не являюсь категоричным в этом вопросе, что геттеров нужно избегать совсем, но правда в том что часто их лепят везде где ни попадя, тем самым раскрывая внутреннее состояние объекта, предоставляя возможность другому коду решать за него (см. пример с cancel())

пихать в VO все что по хорошему там не нужно, доступ к API например

Ни в коем случае. Эти вещи находятся на разных уровнях. Мне кажется вы путаете валидацию и правила бизнеса. Давайте на вашем примере, если я его правильно понял.

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

В идеале, конечно, еще на этапе создания заказа не дать пользователю выбрать точку вне этой зоны, но об этом планирую рассказать в след статье. Но допустим он может или вообще у нас какой-то хитрец, который в обход фронтовой валидации напрямую отправил нам POST данные.

Нам приходит широта и долгота. VO проверит, что точки заданного формата, что пришли обе, что вам не подсунули туда вообще что-нибудь левое. И заказ вполне может быть создан, ведь он прошел фронтовую валидацию и данные вам пришли. Значит на них надо как-то реагировать. Как варианты:

  1. Проверить на беке удаленность точки (не в VO) и сразу выдать ошибку, не создавая сущность.

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

другой — нет, потому что у первого премиум аккаунт

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

При чем тут понятие «Оверинжениринг» к «не прижилось»? Если не можете сами объяснить в чем минус — скиньте статейку или книгу, но пока я не увидел ни одного примера или строчки кода =(


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

Звучит как что-то зловещее :))

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

«Добавляемый», то есть есть какой-то менеджер, который выбирает координаты, которые нам нужно проверить, верно? Значит он отправляет нам Request. То есть тут мы валидируем пользовательские данные, и используем внешний апи, чтобы определить например входит ли адрес в зону доставки. Сущности как таковой еще нет, это пользовательская валидация.


Ок, сделайте вместо «добавляемый» — «сохраняемый». Когда у вас есть partial entity, часть которого заполняется при создании, а детали дозаполняются потом, но редактирование уже имеющихся полей также возможно.

Ни в коем случае. Эти вещи находятся на разных уровнях. Мне кажется вы путаете валидацию и правила бизнеса.


Мне кажется, что четкое разделение валидации и бизнес логики не всегда возможно. Еще мне кажется, что их можно безболезненно смешивать в сервисах валидации и все правила для сохранения сущности у вас будут лежать в одном месте, а не распиханные по слоям. Да, это может привести к проблемам в будущем. Но даже тогда можно будет разбить сервис валидации на куски отвечающие за валидацию и бизнес логику. При условии что вы вообще дорастете до этой необходимости в своем проекте.

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


И когда возможности пользователя по редактированию сущностей будут определяться оплатил ли он подписку на текущий момент или нет, сделаем абстрактную фабрику, которая будет в зависимости от значения в таблице создавать RegularUser и PremiumUser? Вместо одного блока if в сервисе валидации.

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

Но я считаю, что вообще не стоит нагромождать

Определите метод cancel(), который будет выполнять проверки внутри сущности и если всё согласовано — менять её состояние.

Метод cancel() Entity управлять своими статусами. Всё это нужно инкапсулировать, ведь для этого есть паттерн "Состояние".

И да и нет, всё таки всё зависит от задачи.

Стейт действительно хорош когда есть несколько сценариев, и например в зависимости от текущего статуса у нас будет построен разный процесс. Именно процесс. А вот за согласованность своих данных хорошо когда сущность отвечает сама. Одно другому не противоречит.

Валидировали-валидировали да невыдовалидировали. Проверка $amount <= 0 - частая ошибка что в PHP, что в JS. Встречайте - NAN

$a = new Account('1234567890123456');
$a->putMoney(NAN, 'USD');

Интересно, никогда не сталкивался на практике с NAN и с INF. Спасибо.

Цель статьи, конечно не в этом, но замечание интересное. Как правите ошибку то? :)

Лучше проверить if ($amount >=0 && $amount <= $someMaxValue) { ... } else { throw ... }

someMaxValue может уберечь от миллионных потерь, в случае если где-то в коде случится баг (например при конвертации курса из-за деления на ноль). Живой пример - клиент webmoney для android

А что касается валидации в целом - я придерживаюсь другой идеологии: сущность не должна заниматься валидацией. Например, откуда сущность может знать какие валюты поддерживает система? Что если есть разные лимиты на пополнение аккаунта в зависимости от валюты и от страны пользователя? Валидацией должны заниматься валидаторы, но не сущность)

Сущность должна валидировать свое состояние, а не состояние системы

Ок. представь себе ситуацию - вот некая сущность UserBalance, у которой есть два метода - debit (пополнить средства) и credit (списать средства). Сущность валидирует себя, чтобы credit не позволил сделать баланс меньше нуля.

class UserBalance
{
    protected $currency;
    protected float $balance = 0;

    public function credit(float $amount)
    {
        if ($amount >= 0 && $amount < $this->balance) {
            $this->balance -= $amount;
        } else {
            throw new \Exception();
        }
    }

    public function debit(float $amount)
    {
        if ($amount >= 0) {
            $this->balance += $amount;
        } else {
            throw new \Exception();
        }
    }
}

class User {
     protected UserBalance $balance;
}

Потом приходит заказчик и говорит - нам нужно дать возможность для некоторых пользователей уходить в минус. мы их проссто пометим как privileged

class User {
    protected UserBalance $balance;
    protected bool $privileged;
}

Но вот незадача - класс UserBalance и знать не знает к какому пользователю он принадлежит. Как он теперь будет валидировать себя, чтобы позволить привилегированным пользователям уходить в минус?

У меня есть решение и кстати оно описано в комментах где-то выше. Но мне интересно посмотреть ваше предложение и узнать в чем его преимущество.

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

class UserBalance
{
    protected $currency;
    protected float $balance = 0.0;
    protected float $allowedOverdraft = 0.0;

    public function credit(float $amount)
    {
        if ($amount >= 0 && $amount < ($this->balance + $this->allowedOverdraft)) {
            $this->balance -= $amount;
        } else {
            throw new \Exception();
        }
    }

    public function debit(float $amount)
    {
        if ($amount >= 0) {
            $this->balance += $amount;
        } else {
            throw new \Exception();
        }
    }
}

class User {
    protected UserBalance $balance;
}


Но если прямо хочется с флажком в юзере, то флажок можно использовать для того, чтоб смапить состояние БД на необходимый класс UserBalance \ UserBalanceWithOverdraft \ etc

зы. не храните деньги в float =)

Статья интересная, хотел бы предложить на ваше рассмотрение два своих подхода:

Проверка данных в методе

Предлагаю не проверять внутри метода возможность его совершения. Рассмотрим предложенный вами пример с заказом. Выдачей сущностей у нас заведует репозиторий и я предлагаю в зависимости от статуса выдавать разные сущности унаследованные от одной основной, с той разницей что у DeliveredOrder не будет иметь метода cancel. То есть мы уже на уровне типизации защитимся от этого невозможного вызова.

Проверка данных в конструкторе

Я бы все же предложил переложить валидацию данных на билдер для конкретного класса. Например Собака не содержит в себе информацию о том как проверить набор веществ для своего создания, это в её "создателе" (да это тоже Собака, но ссылка на другой объект) произошла проверка что вещества подходят ли и в нужном ли количестве для создания Собака. А она уже содержит набор правил по взаимодействую с миром к тому что она ходит, а не летает например (ну это уже относится к первому пункту).

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