Pull to refresh

Comments 127

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

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

https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0_%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B9

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

Внешние данные (от пользователя) сделали дальнейшие действия по базовому алгоритму (авторизацию) бессмысленными. Это — исключение.
Ok, let's talk 'bout philosophy.

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

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

Где-то между ними болтается класс ситуаций, которые невозможно распознать напрямую, но в то же время их возможно изолировать без нарушения внутреннего состояния приложения. Например, если вы внезапно получили через web api вмеcто json какой-то странный html с 200 кодом возврата. Понятно, что сделать с ним вы ничего не можете, но совершенно необязательно это разрушает ваше приложение.

Я утверждаю, что обрабатывать штатные исключительные ситуации необходимо с помощью кодов возврата. Это может быть что угодно, принятое для вашего языка — от getlasterror до монады error. Нештатные исключительные ситуации должны выбрасывать терминальные исключения и завершать работу приложения (с 500 ошибкой для веба). То что болтается посередине обрабатывается исходя из возможности восстановить работоспособность приложения.

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

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

Уложите пожалуйста мне в код возврата информацию ['Неверный email', 'Слишком короткий пароль', 'И про капчу ты, уважаемый юзер, тоже забыл']
На это очень тяжело отвечать серьезно, потому что в plain old C именно так и делали, и выглядело это вот так:
https://www.openssl.org/docs/manmaster/crypto/ERR_error_string.html

Конечно, подход не самый красивый, но он связан в первую очередь с ограниченим языка. Вы в PHP с объектной моделью можете возвращать некий ValidationResult, который содержит всю необходимую информацию.
Вы не поверите, но я ровно это и делаю в подходе, который описан в статье. Возвращаю (бросаю) некий ValidationErrors, который является коллекцией ValidationColumnError.

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

Это удобное средство инкапсуляции информации об ошибках и управления потоком программы. Я не вижу причин, почему исключения в приложении должны быть исключительно терминальными (фактически — аналогом фатальных ошибок).
Тут вопрос не в том что возвращать, а как. Практически во всех языках программирования разбрасываться исключенями — не самое дешевое занятие. "Don't use Exceptions for flow control" — основной гайдлайн почти везде.

Исключение указывает на "исключительную" ситуацию.
Ситуация "Юзер ввел неправильные данные" — не является исключительной, это один из юз-кейсов программы.
Стремление пробросить исключение, чтобы оно всплыло наверх через несколько слоев логики, — признак плохо продуманной архитектуры
«Don't use Exceptions for flow control» — основной гайдлайн почти везде.

В PHP нет такого гайдлайна. И не в PHP тоже — находится только одна статья в MSDN десятилетней давности. Весьма спорная статья в которой речь лишь о том, что в древнем .Net throw на 3 порядка медленнее чем return. Какое это имеет отношение к моей статье?

И кстати. Я уверен, что в PHP нет ситуации, когда стоимость исключения в сравнении с не-исключением стала бы критично важной.

Ситуация «Юзер ввел неправильные данные» — не является исключительной

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

Пользователь облажался в одной букве, а вы в ответ АЛЯРМ ААА ПАНИКА НЕПРАВИЛЬНЫЙ ПАРОЛЬ ВСЕ В ИСКЛЮЧЕНИЕ.

Ну несерьезно это.
не очень понимаю, почему при вводе неверных креденшалов у вас «дальнейшее выполнение программы невозможно»

Странно. Почему же вы не понимаете? Пользователь ввел неверные данные для входа. Следовательно дальнейшее ШТАТНОЕ выполнение программы (а именно — процедура аутентификации и авторизации) разумеется невозможно. Или вы предлагаете пускать любого с любым паролем? Конечно же нет.

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

вы в ответ АЛЯРМ ААА ПАНИКА НЕПРАВИЛЬНЫЙ ПАРОЛЬ ВСЕ В ИСКЛЮЧЕНИЕ

Я этого нигде не говорил.
Странно, что и вы не хотите услышать собеседников, хоть сами к этому призываете. Правильно говорят выше – исключение это когда вам ломают ноги и вы становитесь нетрудоспособным (коннекта к БД, допустим, нет – это исключение). Ошибка валидации – это когда вместо кофе приносят чай, на трудоспособность не сказывается, просим заменить чай на кофе -> profit.

Почитайте language-agnostic дискуссию на stackoverflow: http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control.
Странно, что и вы не хотите услышать собеседников, хоть сами к этому призываете

Я вас слышу. Но разве обязан быть с вами согласен? Нет.

Правильно говорят выше – исключение это когда вам ломают ноги и вы становитесь нетрудоспособным (коннекта к БД, допустим, нет – это исключение).

Это ваше мнение. Мое другое — нет, не только когда ломают ноги.

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

Вот вам пара цитат:

Throwing exceptions is one of the most expensive operations in .NET.

Снова тот же избитый аргумент про старый .Net. Давно уже опровергнутый.
И тут же:

However, some languages (notably Python) use exceptions as flow-control constructs. For example, iterators raise a StopIteration exception if there are no further items.

Ну и под конец дискуссии:

I don't think there is anything wrong with using Exceptions for flow-control. Exceptions are somewhat similar to continuations and in statically typed languages, Exceptions are more powerful than continuations, so, if you need continuations but your language doesn't have them, you can use Exceptions to implement them.

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

$user = new User;
$user->load($_POST);

if ($user->save()) {
  redirect('hello.php');
} else {
  $this->view->assign('errors', $user->getErrors());
}

Можете привести какой-нибудь пример, где такое использование исключений дает что-нибудь полезное?

Еще такой вопрос. Допустим, перед повторным показом формы мне надо установить для поля пустое значение, которое пользователь должен заполнить. Например, сбросить пароль при ошибке авторизации. Получается, я делаю $model->password = '' и получаю исключение RequiredException?
Код отличается, и значительно.

Начнем с того, что вы ловите возможные ошибки на этапе синхронизации модели с БД. Я же — на этапе присвоения. Хотя это и не главное.

Мой код отличается от вашего тем, что исключение, в отличие от того, что вы вернули из метода getErrors() является объектом и, в силу этого факта, имеет тип. У меня могут быть разные типы исключений, например UserShortPassword, UserSimplePassword, которые а) наследуются от общего предка UserPasswordException и б) упакованы в коллекцию-итератор, которая сама тоже объект и Throwable. Это позволяет делать мне что-то вроде такого:

catch (FormMultiException $e) {
  if ($passwordErrors = $e->extractByColumn('password') {
    // что-то сделали с ошибками про пароль, в лог записали о том, что была попытка подбора, например
    // и все остальные ошибки бросили дальше
    throw $e;
  }
}

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

Вопрос же ваш до конца не понял. Зачем вы присваиваете модели данных пустое значения поля ради того, чтобы не показывать во view это значение? Не надо так делать.
Допустим, завернём мы наши ошибки в коллекцию типизированных объектов, наследованных от Error. И будем её возвращать, а не выкидывать. Чем это будет хуже?
Тем, что вместо прямого и понятно try...catch...throw наш код превратится в if-hell.
if ($result instanceof Errors) {

// и вот тут бы всё, поработали, отработали 5 ошибок из 7, надо бы оставшиеся 2 дальше отправить, а throw нет — что делать?
}

Вам это нравится?

И потом. Возвращать вы говорите. Секундочку, а как вы можете вернуть что-то из, скажем, __set()? Выбросить — можно. А вот вернуть из сеттера или конструктора — нельзя.
М… и чем try-hell лучше if-hell?

Вот try-catch:

$post = new Post();
$e = null;
try {  
  $post->load($_POST);
  $post->save();
} catch (FormMultiException $e) {
  $errors = $e;
} finally {
  return $this->render('view', ['post' => $post, 'errors' => $e]);
}

Вот if:

$post = new Post();
$post->load($_POST);
$post->save();
return $this->render('view', ['post' => $post, 'errors' => $post->getErrors()]);
Это типичная задачка. Показать ошибки под полями формы и дать исправить.
Не надо игнорировать тот факт, что вызов save() невозможен, если не завершился успехом вызов load().
В моем случае исключения не дадут вам проигнорировать. В вашем — вы можете забыть написать очередной if.
Ну да, обычно это выглядит так:

if ($post->load($_POST) && $post->save()) {
    // success!
}

$this->render...
Глупая конструкция в контексте Yii2 :)
Есть у меня форма, и в форму еще прокидываю UploadedFile. В случае если $_POST == [] (допустим, обновление только аватара, форма изменения профиля) функция load возвращает false. Даже не знаю насколько это поведение корректно
Глупый Yii2 )))

Почему "процедура" объекта вообще возвращает что-то, кроме самого объекта?
Вы про метод load? Он возвращает результат присвоения. http://take.ms/KIO3u
Результат присвоения? А зачем?

  • если присваивание прошло ожидаемо, мне не нужен true
  • если что-то случилось — я хочу знать, что именно, а не просто false
    — и для этого есть… пам-пам… механизм под названием "Исключения"!
    — а если "случилось" несколько ошибок или проблем? не беда, у нас же исключение может быть коллекцией других исключений! (см. статью)
Почему бы и нет? method chaining не всегда лучшее решение.
Потому что это не нужно. "Результат присваивания" вообще не нужен, понимаете? Если присваивание прошло штатно, мне не нужен true. Если возникли ошибки — мне нужны ошибки, а не false.

Ваш load() — типичная "процедура", то есть метод, лишь меняющий внутреннее состояние объекта, но не возвращающий результат. В этом случае принято возвращать сам объект в его новом состоянии.
В контексте статьи и организации валидации через исключения, естественно, понимаю. В контексте решения без исключений нет.
А зачем делать решения без исключений, если гораздо проще и логичнее это сделать с исключениями? Исключение — штатное средство языка. Метод getErrors() — велосипед, изобретенный от нежелания или неумения пользоваться штатными средствами.

Имхо.
Потому что они не особо хуже? Я не вижу прям сильного профита в решении из статьи, как и не вижу фатальных недостатков подхода.
При чём тут вообще Yii?
Ваш код полностью выглядит так:

$post = new Post();
$errors = [];

$res = $post->load($_POST);
if (false === $res) {
  $errors = array_merge($errors, $post->getErrors());
}
if (empty($errors) {
  $res = $post->save();
  if (false === $res) {
    $errors = array_merge($errors, $post->getErrors());
  }
}

$this->view->post = $post;
$this->view->errors = $e;

а мой, на самом деле, так:

try {
  $this->view->post = (new Post())->fill($_POST)->save();
} catch (MultiException $e) {
  $this->view->errors = $e;
}
  1. Ваш код выше не будет работать потому что $this->view->post = $post; у вас никогда не выполнится. Исключение прерывает исполнение блока try. Без finally вы работать не сможете.

  2. В коде с if валидация не делается при load(). Вы же написали так, как буд-то делается. Два раза делать валидацию я смысла не вижу. Инициализировать массив ошибок и делать merge поэтому нет необходимости.
  1. Да, не выполнится. Но это не аргумент против подхода. Я упрощаю ровно также, как и вы ))

  2. А хреново, что не делается. Это в Yii, может, и принято валидацию данных в модели проводить только при попытке save(), и молча этот save() отменять, ничего не говоря. В реальных приложениях требование "валидация при присваивании" и "результат валидации сразу" встречается очень даже нередко.
ОК. Давайте не упрощать, но и не сравнивать специфичный для фреймворка код с абстрактным описанием подхода.

Задача:

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

  1. Валидация производится в момент присваивания данных модели.
  2. Метод load() только загружает данные. Метод save() только сохраняет модель. Метод validate() только валидирует.
  3. validate() либо кидает исключение-контейнер ошибок, либо возвращает этот контейнер ошибок. Получить ошибки из модели после нельзя.
  4. Модель и ошибки надо передать в view (и то и то нужно чтобы показать форму с ошибками). Допустим, делаем мы это через $this->render(название view, массив данных).
  5. View у нас получается идентичный. Разница в непосредственно валидации.

Для данной задачи вариант с if выглядит так:

$post = new Post();
$post->load($_POST);
$errors = $post->validate();
if (count($errors) === 0) {
    $post->save();
}
$this->render('post', ['post' => $post, 'errors' => $errors]);

Вариант с try-catch:

$post = new Post();
$post->load($_POST);

$errors = new ValidationErrors(); // или null, в зависимости от того, с чем может работать view
try {
    $post->validate();
    $post->save();
} catch (ValidationErrors $errors) {
    // do nothing
} finally {
    $this->render('post', ['post' => $post, 'errors' => $errors])
}
Вы неправы ровно в тот момент, когда говорите "Есть форма, это модель". Это опять какой-то очень странный, антиархитектурный подход.

Разрешите я более точно переформулирую задачу.

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

Задача — создать пост в блоге. Или отобразить форму, в которой будет четко и внятно указано, почему это нельзя сделать.

Мой вариант:

// условный контроллер
$data = (условно)$_POST;
try {
  $post = $data->id ? Post::findById($id) : new Post;
  $post->fill($data)->save();
  redirect();
} catch (MultiException $e) {
  $this->view->errors = $e;
}
$this->view->data = $data;

// условно шаблон
{% for error in errors %}
  <div class="alert">{{ error }}</div>
{% endfor %}
<form>...</form>

Что я делаю не так?
Ошибки валидации ловятся и в fill() и в save(). В первом случае это валидаторы отдельных полей модели, во втором случае — общий валидатор модели.

Можно и под каждым полем. Это легко. Ведь $view->errors у нас объект!

{% for error in errors.getByColumn('password') %}
«Есть форма, это модель»

ОК. Да, у меня привычка называть сущности моделями. Суть от этого не меняется. В нашем случае Post — это класс, хранящий данные и умеющий их сохранять и валидировать. Как ни назови.

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

Потому что может быть валидация конкретного значения в контексте поля модели ("Пароль не может быть короче 6 символов"), валидация комплекса данных в контексте модели ("Номер телефона и регион должны соответствовать друг другу") и даже валидация с использованием внешних зависимостей ("Имя пользователя должно быть уникальным")

Соответственно и валидаторы бывают разные. Вы, как разработчик, должны иметь выбор — какие использовать. А фреймворк — давать вам такую возможность.
Валидацию в контексте поля вы производите при каждом присваивании?
Я — кто?

Я — "разработчик класса абстрактной модели"? Нет, не произвожу при каждом присваивании. Но даю возможность произвести.

Я — "разработчик модели конкретного класса"? Нет, не произвожу при каждом присваивании. Лишь тогда, когда мне это надо. Захочу для данного поля в данном сценарии и контексте — буду. Не захочу — не буду.
И да, самое главное — при моем подходе во view отправляется не "модель" (а ее быть не может в случае ошибки валидации, модель не имеет права существовать в "разобранном" состоянии), а те данные, что ввел пользователь. Пусть он их видит, корректирует, улучшает, отправляет снова. Но. Это не модель.
Это не повлияет на код примера никак.
Чуть усложним: регистрация пользователя. Нужно ввести логин, пароль и подтверждение. Облом может случиться в двух местах:

  1. При проверке пароля и подтверждения.
  2. При попытке создать пользователя с уже зарегистрированным логином.

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

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

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

А валидация в сеттерах — это один их возможных подходов. Статья-то не про это )))
Компонент регистрации будет бросать MultiException?
Ну то есть компонент регистрации фактически не отличается от модели пользователя. По своей сути это тоже модель. Только она save() делегирует пользователю.
Нет, конечно же )))
Ахаха, перестаньте так шутить )))

Компонент регистрации — это вообще не MVC. Это компонент. То есть некая часть бизнес-логики. Он юзает M, иногда даже V или C в своих грязных целях, но гораздо умнее при этом. И выполняет именно бизнес-процессы, а не beforeSave — save — afterSave, как любят разработчики на одном всем нам известном фреймворке )))
M в MVC — это модель в смысле "доменная логика", не в смысле "штука, которая сохраняет данные в базу".
Компонент регистрации управляет множеством объектов доменной области, сводя их поведение к бизнес-сценариями. Например объектами "Пользователь", "Группа", "Письмо о регистрации", "Отчет о количестве зарегистрированных", "Партнер, который привел реферала". Все они управляются в рамках сценария "Регистрация нового пользователя".

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

А метод мёржа с массивом — довольно полезная штука безотносительно инвариантов.
Не в зависимостях дело, а в сложности в процессе работы с объектом всегда поддерживать его в консистентном состоянии. Достаточно консистентности в конечном счёте в подавляющем большинстве случаев, если она необходима вообще.
Довольно странно оставлять объект в неконсистентном состоянии после вызова публичного метода.
Обычная ситуация, например, должно быть заполнено только одно из пары полей в зависимости от значения третьего поля.
Так почему бы не заполнить оба поля за один вызов метода?
Не всегда это легко сделать, даже если есть желание. Например, если заполнение происходит автоматически фреймворком типа DI-контейнера, десериализатора или ORM, который может только по одному значению за раз в сеттеры передавать.
Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?
Ну и да:

$errors = $user->getErrors();
$passwordErrors = $errors['password'];  // можно isset добавить

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

Я поэтому и попросил привести пример (а не абстрактный участок кода), где это действительно будет нужно.

Зачем вы присваиваете модели данных пустое значения поля ради того, чтобы не показывать во view это значение?

Это просто пример, в общем случае у нас могут быть значения, которые можно присваивать из программы, но нельзя присваивать из пользовательского ввода. Другой пример — поле было необязательным, в базе куча записей с пустым значением, потом решили сделать обязательным, при загрузке из БД вызывается __set(), который бросает исключение.
Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?

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

Например в контроллере получить все исключения связанные с паролями, пройтись по ним, записать в лог, а сам контейнер отправить дальше, например во view.
Вот для чего иерархия:

foreach ($multiException->errors as $error) {
  echo $error->getMessage();
}

Единственное, от \Exception наследоваться не стоит. Незачем...
foreach ($multiException->errors… )

зачем так сложно? почему не просто

foreach ($errors as $error)

Я начинаю подозревать, что вы не читали статью. Или немного не так ее поняли.
Что ArrayAccess забыл, да. Что скажете на тему полезности наследования от \Exception?
Вы мне предлагаете статью еще раз пересказать, на этот раз в комментариях лично для вас?
Перечитал ещё раз. Нашёл под спойлером в коде __set().
Другой пример — поле было необязательным, в базе куча записей с пустым значением, потом решили сделать обязательным, при загрузке из БД вызывается __set(), который бросает исключение.

Это называется "криворукие разработчики, не сумели сделать нормально миграцию БД" :)
Простой пример: раньше указывать адрес было не обязательно и никто не указывал, а теперь обязательно. Предлагаете всем пользователям при миграции прописать БОМЖ?
NULL прописать. В SQL NULL — это неизвестное значение. Ровно то, что вам и нужно.

Почему из этого вдруг стала вытекать необходимость бросать исключение при создании моделей из записей БД — мне неведомо. Я бы не стал так делать.
Тут дело в том, что если модель заполняется из пользовательского ввода, то кидать надо, а если выборкой из базы, то нет.
"Волга впадает в Каспийское море" (с)

Если валидацию делать грамотно, как проверку на соответствие значения контексту и сценарию, то ваш вопрос отпадет сам собой.
Итак, у нас есть метод findOne($id). Он получает данные из БД, делает $model = new User(), и устанавливает свойство $model->address = $dbData['address'], при этом $dbData['address'] = null. Вызывается метод __set().
Что именно вы предлагаете в нем делать, что означает "делать валидацию грамотно"? Считать, что NULL не является пустым значением и не бросать RequiredException? Или при загрузке из БД устанавливать специальную константу $model->scenario = "LOAD_FROM_DB", а в сеттере или валидаторе ее проверять?
Я предлагаю при валидации учитывать не только $key и $val, но и $this. И тогда всё встает на свои места.
$this — это объект. У него есть состояние. В том числе может быть состояние "создан, чтобы быть заполненным фактическими данными из БД".
В том числе может быть состояние «создан, чтобы быть заполненным фактическими данными из БД».

Дырявая абстракция?
Сценарий использования? Ну да, не самая лучшая. Но вы же знаете, все нетривиальные — дырявы.
Вижу в данном подходе несколько неудобных моментов:

  • Каждый выброс исключения прерывает текущий поток исполнения. Код валидации из последовательного превращается в скачущий туда-сюда, и всё обмазывается несколькими уровнями вложенного перехвата исключений — понять его и проследить логику сложнее.
  • Результат валидации отделяется от модели: получается, что модель либо полностью валидна, либо не может существовать впринципе. При отображении результатов валидации часто бывает нужно получить именно эти невалидные значения: например, поменять «Пароль слишком короткий» на «Пароль из {x} символов слишком короткий» — в вашем случае для этого придется менять валидатор.
  • Непонятно, как валидировать связанные поля. Например, поле для подтверждения пароля или опциональные поля, зависящие от переключателя. В вашем случае порядок валидации полей никто не гарантирует, и одно поле может быть проверено до окончательного заполнения всей модели.
При всем уважении не вижу упомянутых вами минусов.

  • Суть концепции мультиисключения как раз и состоит в том, что код НЕ ПРЕРЫВАЕТСЯ одним исключением. Вместо этого специальные методы накапливают коллекцию исключений и бросают ее.
  • Да, результат валидации отделяется от модели. В этом и смысл. Модель не нужна, если она невалидна.
  • Валидатор менять не надо. Просто передайте в конструктор исключения ссылку на модель. Это же ООП! Это же зависимости!
  • Совершенно понятно. Просто напишите еще один валидатор, не для этапа заполнения данными, а классический beforeSave(). И, сюрприз — он тоже может бросать мультиисключение!

P.S. И да. В модели не может быть поля подтверждения пароля. В форме — может.
Суть концепции мультиисключения как раз и состоит в том, что код НЕ ПРЕРЫВАЕТСЯ одним исключением.

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

Просто передайте в конструктор исключения ссылку на модель.

Если мы создаем объект в блоке try и в процессе создания вылетает исключение, объектом больше пользоваться нельзя, поскольку его целостность не гарантируется. Выносить ссылку на него в блок catch — очень плохая идея.

Я предлагаю вам несколько пересмотреть терминологию. Валидировать-то нужно не модель, а саму форму, и если она валидна — применять данные из нее к модели. Тогда всё становится на свои места:

  • Ошибки привязываются к полям формы, что более естественно для фронтенда
  • Модель будет всегда валидна, а форма — всегда полностью инициализирована
  • Не нужно разносить валидацию в разные места, если есть связанные поля
Я ровно об этом и говорю тут. Выше. Модель для примера была дана...
На самом деле, все такие параллельные валидации должны отработать ещё на кленте, так что ничего страшного, что сервер не прогоняет полную валидацию формы, а останавливается на первом же косяке.
Сервер вообще не может валидировать форму. У него нет никаких форм.
А что у него тогда есть?
У сервера-то?
Данные от пользователя. Неорганизованные. Заведомо грязные, невалидные и вредоносные.
И модели, описывающие то, какими должны быть организованные и чистые данные, синхронные с хранилищем.

А дальше уже вопрос вашей архитектуры, как вы одно превратите в другое.

Но, конечно же, никаких форм на сервере нет. Это вам всякие Yii врут нещадно.
"Это есть в Symfony" и "Это хорошая, годная архитектура" — не синонимы.
ОК. Почему не должно быть никаких форм на сервере? Форма на сервере, в понимании перечисленных фреймворков (и Yii тоже) отвечает за то, чтобы грязные данные из реквеста проверить и признать валидными или нет.
Тогда и назовите это Валидатор. Или компонент "Вход пользователя". Или как-то иначе. Но не формой.

Форм на сервере не должно быть, потому что форм на сервере нет. Независимо от степени полёта вашей фантазии )))
Это, в общем-то, предмет не моей фантазии. Так уж повелось, что это называют формой практически все популярные фреймворки. Если я это буду назвать как-то ещё, суть от этого не изменится.

Кстати, Yii это зовёт form model, а не просто form. Сам класс называется Model. Назвать это валидатором будет неправильно потому что по факту это не валидатор, а набор данных плюс набор валидаторов плюс, опционально, какая-то логика их обработки.
Мы сейчас погрязнем в терминологическом споре, потому что сколько участников беседы, столько значений у одних и тех же терминов.

Я не пользовался ни Symfony, ни Yii. Зато в знакомом мне ASP.NET MVC есть понятия "модель" и "вьюмодель". Первое — понятно, объект из базы данных. А вот второе — это некая проекция модели, то есть данные, которые показываются пользователю или приходят от него после отправки формы. Именно это я имел в виду, говоря про "форму на сервере". И валидируется обычно именно она, а не модель, связанная с БД.
Именно так. Как это ни обозвать, смысл именно этот.
Нет. Смысла в этом нет. Ровно ноль.

Не существует объекта под названием "Модель формы". Это крайне неудачный паттерн, который не соответствует реальности.

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

Но нет форм на сервере и моделей view на сервере. ASP.NET MVC ошибается.

Корень этой ошибки в том, что вы надеетесь на то, что от клиента к вам придут некие структурированные данные. А если нет? Не придут? Или не структурированные? Вы не можете контролировать процесс HTTP Request, но имеете иллюзию, что контролируете — якобы, что написали в "модели формы" на сервере, то мне и придет от клиента. Это неверно.
Формы на сервере есть, они передаются хттп-запросами, обычно одним из майм-типов, специально созданных для форм. И следует различать задачи проверки корректности (с точки зрения бизнес-процессов в данном контексте) данных формы и задачи выявления ошибок обработки формы. При этом следует иметь в виду, что для пользователя особо без разницы не заполнил он имя пользователя в форме (ошибка запроса) или такое имя уже есть (ошибка обработки запроса) — сообщения об ошибках ему хочется видеть единообразные, привязанные к конкретному полю формы (как сущности интерфейса). С другой стороны очень часто формы и сущности модели соответствуют друг другу полностью и создатели фреймворков и библиотек позволяют особо их не разделять (а иногда даже не позволяют их нормально разделять). В той же Симфони сделано грамотно — объект, инкапсулирующий форму как элемент запроса, имеет объект с данными, который может являться по совместительству объ
Формы на сервере есть, они передаются хттп-запросами

Первый класс, вторая четверть.
И эти люди пытаются мне что-то доказывать...
Content-Type: application/x-www-form-urlencoded вам ни на что не намекает? Может он говорит о том, что в теле запросе содержится веб-форма?
Нет. Мне такой заголовок говорит "кто-то пытается передать мне некий запрос и хочет, чтобы я думал, что к запросу приложены данные, сформированные формой". Так ли это на самом деле — я не знаю.
Какой формой? Вообще, что вы называете формой?
ъектом бизнес-модели, а может не являться. При этом вадидатор проверяет на соответствие каким-то правилам и возвращает подробный результат проверки тот объект, который ему скажут, практически любой объект пхп
Модель не нужна, если она невалидна.

Кому-то иногда не нужна, кому-то иногда нужна, кому-то всегда нужна.

И да. В модели не может быть поля подтверждения пароля.

Может. Кто сказал, что не может?
В модели может быть всё, что сможет в неё запихнуть разработчик.
Это и называется "антиархитектура"
Нельзя говорить о качестве архитектуры не зная о поставленных перед её разработчиком задачах.
Скажите, а у модели могут быть поля Дата рождения и Дата поступления на курс, например. Сами придумаете, какая между ними валидация?
Если рассматривать модель как Entity, то она всегда валидна, нельзя создать не валидную Entity. Так что тут все нормально, про поле для подтверждения пароля Entity вообще ничего не знает, не ее зона ответственности, вы путаете валидацию входных данных, и "валидацию" бизнес объекта.
  1. Для меня любой пользовательский ввод — штатная ситуация. Не увидел аргументов, почему для вас иначе?
  2. Исключения для данной задачи несут логический overhead, так как дают возможность написать код без try/catch и ловить их в любом другом месте программы, что, в отличии от критических ситуаций, бессмысленно. А отказ от функционального подхода на пустом месте, в свою очередь, исключает возможность статического анализа кода (единственная попытка это сделать была в Java с throws, и, фактически, это признано неэффективным, а в PHP и вовсе нет таких возможностей).

Мой код отличается от вашего тем, что исключение, в отличие от того, что вы вернули из метода getErrors() является объектом и, в силу этого факта, имеет тип.

Возвращайте объект.
Ок, вы пишете консольную команду которая создает пользователя, и вы написали так

$username = 'Admin';
$password = 'admin';
$user = new User($username, $password);

Но у вас есть бизнес требование, пароль должен быть больше 8 символов, где и как вы будете делать проверку на это бизнес требование?
А если у меня несколько валидаторов на одно поле? И скажем оба не проходят, то что, будет одна ошибка на одно поле, а о остальных забудем?
Поправте код, при сетере — нужно пробегать валидаторы тоже в цикле и каждый обварачивать траем, а потом создавать коллекцию ошибок на каждое поле!
Хм, минусуете — так хоть аргументируйте чего :)
  • я не вижу смысла валедации при сетере, а не отдельным методом. Если я сетю 5К раз, но сохраняю (обрабатываю) значения только раз — зачем мне каждый раз их валедировать при сетере? Также сетер может вызываться при, скажем, выборке из БД, то тут можно попасть в так… Когда валидаторы поменялись, а данные ещё нет! Любая выборка из БЛ будет порождать ошибки!
Да и отдельным методом (не вызываемом автоматически при каждом чихе) в самом валидируемом объекте далеко не всегда имеет смысл
Ну "при каждом чихе" — это и есть сеттер, но, имхо, там не верно валедировать. Сделайте валидацию перед сохранением, после спец. методом fill, но не сеттерах.
3. Исключения управляют потоком.

Исключения не стоит использовать для управления потоком.
Всем кто хочет понять (или закрепить) в каких случаях стоит, а в каких не стоит, использовать исключения, советую почитать вот это.
try-catch я воспринимаю как некий подвид goto, связанный с ошибками, которые нужно словить далеко от текущей области видимости.
Описанный подход можно конечно использовать, но он мне кажется больше сопряжен с возможностью накосячить.
Ок, допустим мне нужно проверить сразу все правила валидации, чтобы подсветить все ошибочные
поля и указать в чём именно пользователь был не прав.
Как быть?
Ровно об этом и написана статья.

Шаг 1. Для каждого поля пишется валидатор. Валидатор может либо throw Exception либо yield Exception (это не указано в статье, каюсь, надо было написать сразу)
Шаг 2. Нечто, что заполняет модель данными собирает все выброшенные либо сгенерированные валидаторами отдельных полей исключения в объект MultiException
Шаг 3. Бросается MultiException, вы его в нужном месте ловите и обрабатываете.

Если станет интересно — могу дать и конкретный код, реализующий этот подход, и тесты для него
Если можно пример к такому коду:

$rules = [
  'login'  => [ 'required', 'login', 'min_len' => 3, 'max_len' => 15 ],
  'email' => [ 'required', 'email' ],
  'pass'  => [ 'required', 'min_len' => 4 ],
  'phone' => [ 'required', 'phone' ],
];

$model->setRules( $rules );
...
$model->save();
...
$errors = $model->getErrors ();

На выходе ожидаю:

[
  'login' => [ 'required' => 'Login is empty', 'login' => 'Invalid login', ... ],
  ...
]
try {
  $model->fill($DATA); // $DATA - некие внешние данные, либо array, либо IArray
  $model->save();
} catch (MultiException $e) {
  foreach ($e->group('column') as $column => $errors) {
    foreach ($errors as $error) {
      /** @var Exception $error */
      echo $column . '=>' . $error->getMessage();
    }
  }
}
Внутри модели ПРИМЕРНО так:

protected function validateEmail($value) {
  if (empty($value) {
    yield new Exception('Пустой email');
  }
  if (strlen($value) < 3) {
    yield new Exception('Короткий email');
  }
  return true;
}

Разумеется, возможно и динамически

$model->setValidator(string $column, callable $validator); 

но на практике такое применяется реже
В результате вы получаете не невнятный массив, а коллекцию объектов-исключений.

Первый плюс в том, что исключение — это тоже объект, вы можете выстраивать свою иерархию классов, добавлять свои свойства и методы (здесь это уже сделано, есть свойство $exception->column, к примеру).
Второй плюс в том, что коллекция, хотя и похожа на массив, все-таки тоже объект, и у нее множество полезных методов (filter, sort, map, reduce, group, find — да мало ли можно придумать!).
Третий плюс в том, что мультиисключение — это не только массив и не только объект, но и тоже исключение! Его можно поймать, изменить, бросить дальше.
На что только не идут люди, лишь бы не писать на нормальных языках с either и монад трансформерами
Sign up to leave a comment.

Articles

Change theme settings