Вы только гляньте. Вот например Yii и Yii2, получение ошибок валидации модели:
$errors = $model->getErrors();
Symfony, ошибки формы:
$errors = $form->getErrors();
Активно рекламирующийся Pixie (давненько про него ничего не было):
$result = $validator->validate($data);
$errors = $result->errors();
Что тут не так?
Да всё. Всё не так. Весь этот код очень дурно пахнет, он пахнет временами PHP4, спагетти-архитектурой и диким смешением понятий.
Что же делать?
Начать разбираться. С самого начала.
Определим важные понятия.
1. Валидность — это ответ на вопрос «является ли значение допустимым, иначе говоря валидным, в данном контексте». Контекст может быть разным, это и поле в форме, и свойство объекта. Интересно, что ответ «да» на вопрос о валидности не предполагает никакой дополнительной информации, а вот ответ «нет» требует пояснения. Например: пароль невалиден ПОТОМУ ЧТО его длина менее 6 символов.
2. Валидация — процесс проверки валидности. У нас есть с вами некое значение и есть контекст. Валидатор (процесс, осуществляющий валидацию), должен однозначно ответить, валидно ли значение в данном контексте, и если нет — то почему.
3. «Почему» из предыдущего пункта как раз и называют "ошибкой валидации". Ошибки валидации — детальная информация о том, что конкретно вызвало ответ false на вопрос о валидности данных, то есть причина непрохождения валидации. Фактически это не ошибки в смысле «шеф, всё пропало!», а просто некий отчет валидатора, однако слово «ошибка» уже прижилось в среде разработчиков.
4. Правила валидации — функции, принимающие на вход контекст и значение, и возвращающие ответ о валидности. Ответ должен включать в себя и true/false и отчет о валидации, то есть набор ошибок, если такие есть.
С валидацией довольно часто (особенно в некоторых фреймворках, которые до сих пор поддерживают PHP 5.2, не будем показывать на них пальцем) путают sanitize (или по-русски «очистку») значений. Не стоит путать понятия «валидация» и «очистка» (или приведение к каноническому виду), это два совершенно разных процесса.
Хороший пример, который мне нравится: ввод российского телефонного номера. Для валидации достаточно (в общем случае), чтобы в введенной строке было 11 цифр, причем первая из них 7, при произвольном количестве и позициях иных символов. Если это не так — валидация не пройдена. Задача же санитайзера — удалить из этого значения всё, кроме цифр, чтобы мы могли сохранить в БД стандартизованный msisdn.
Почитайте, чтобы окончательно понять разницу: php.net/manual/ru/filter.filters.php
Ну хорошо, а что всё-таки не так?
То, что коллекция ошибок валидации не является исключением.
Все вот эти замечательные
->getErrors()
не исключения. Следовательно мы лишены множества преимуществ:- Исключения типизированы. В фреймворках же, подобных вышеупомянутым, я не могу создать иерархию FormException --> FormFieldException --> FormPasswordFieldException --> FormPasswordFieldNotSameException. Это очень важно, особенно с выходом PHP 7, который делает тайп-хинтинги наконец-то нормой и стандартом
- Исключения инкапсулируют в себе много нужного. Это же ООП! Например: на какой странице (URL) возникла ошибка валидации? Кто пользователь? Какое конкретно поле формы? Какое правило валидации сработало? Наконец «а дай-ка перевод этого сообщения на эстонский». Может ли это всё сделать простой массив сообщений об ошибках? Конечно же нет. (Кстати, достаточно реализовать метод __toString() и исключение в шаблоне продолжит вести себя как простое сообщение об ошибке)
- Исключения управляют потоком. Я могу его бросить. Оно всплывает. Я могу его поймать, а могу поймать и бросить дальше. Массив $errors лишен права управлять потоком кода, поэтому очень неудобен. Как мне с помощью $errors эскалировать обработку ошибок валидации из модели выше, например в контроллер или компонент приложения?
И что же делать?
Попробуем поставить задачу. Что бы хотелось видеть в коде? Ну, скажем, примерно вот такое:
Где-то в активном коде:
try {
$user = new User;
$user->fill($_POST);
$user->save();
redirect('hello.php');
catch (ValidationErrors $e) {
$this->view->assign('errors', $e);
}
Где-то в шаблоне:
<?php foreach ($errors as $error): ?>
<div class="alert alert-danger"><?php echo $error->getMessage(); ?></div>
<?php endforeach; ?>
Суть предлагаемого архитектурного шаблона можно выразить очень кратко: Мультиисключение. Исключение, являющееся коллекцией других исключений.
Как этого добиться? К счастью, современный PHP позволяет нам и не такие трюки.
Превращаем исключение в коллекцию
interface IArrayAccess
extends \ArrayAccess, \Countable, \IteratorAggregate, \Serializable
{
}
Трейт, который реализует этот интерфейс:
trait TArrayAccess
{
protected $storage = [];
protected function innerIsset($offset)
{
return array_key_exists($offset, $this->storage);
}
protected function innerGet($offset)
{
return isset($this->storage[$offset]) ? $this->storage[$offset] : null;
}
protected function innerSet($offset, $value)
{
if ('' == $offset) {
if (empty($this->storage)) {
$offset = 0;
} else {
$offset = max(array_keys($this->storage))+1;
}
}
$this->storage[$offset] = $value;
}
protected function innerUnset($offset)
{
unset($this->storage[$offset]);
}
public function offsetExists($offset)
{
return $this->innerIsset($offset);
}
public function offsetGet($offset)
{
return $this->innerGet($offset);
}
public function offsetSet($offset, $value)
{
$this->innerSet($offset, $value);
}
public function offsetUnset($offset)
{
$this->innerUnset($offset);
}
public function count()
{
return count($this->storage);
}
public function isEmpty()
{
return empty($this->storage);
}
}
// И так далее. Аккуратно реализуем каждый интерфейс из состава IArrayAccess
// Здесь я позволяю себе только одну вольность по сравнению с ванильными массивами - обратите внимание на метод innerIsset(), он вернет true, если элемент коллекции существует, но равен null. Имхо, это более верное поведение.
Я лично добавляю еще один полезный интерфейс и его реализацию трейтом, но он, конечно же, совсем необязателен:
interface ICollection
{
public function add($value);
public function prepend($value);
public function append($value);
public function slice($offset, $length=null);
public function existsElement(array $attributes);
public function findAllByAttributes(array $attributes);
public function findByAttributes(array $attributes);
public function asort();
public function ksort();
public function uasort(callable $callback);
public function uksort(callable $callback);
public function natsort();
public function natcasesort();
public function sort(callable $callback);
public function map(callable $callback);
public function filter(callable $callback);
public function reduce($start, callable $callback);
public function collect($what);
public function group($by);
public function __call($method, array $params = []);
}
и, наконец, собираем всё воедино:
class MultiException
extends \Exception
implements IArrayAccess
{
use TArrayAccess;
}
Простой пример применения
Метод заполнения модели данными.
В модели создаются правила валидации. Они выбрасывают исключения каждый раз, когда значение не проходит валидацию при присваивании его полю модели. Например:
protected function validatePassword($value) {
if (strlen($value) < 3) {
throw new Exception('Недостаточная длина пароля');
}
...
return true;
}
Создаем магический сеттер, который будет автоматически вызывать валидатор для поля. И заодно преобразовывать выброшенное исключение к другому типу, содержащему в себе не просто сообщение об ошибке валидации, но еще и имя поля:
public function __set($key, $val) {
$validator = 'validate' . ucfirst($key);
if (method_exists($this, $validator)) {
try {
if ($this->$validator($value)) {
parent::__set($key, $val);
}
} catch (Exception $e) {
throw new ModelColumnException($key, $e->getMessage());
}
}
}
Создаем метод fill($data), который попытается заполнить модель данными и аккуратно соберет в одно целое все ошибки валидации по отдельным полям:
public function fill($data) {
$errors = new Multiexception;
foreach ($data as $key => $val) {
try {
$this->$key = $val;
} catch (ModelColumnException $e) {
$errors[] = $e;
}
}
if (!$errors->isEmpty()) {
throw $errors;
}
}
Собственно, всё. Можно применять. Куча плюсов:
- Это исключение, значит его можно поймать в нужном месте
- Это массив исключений, так что мы можем в любой момент добавить в него новое исключение или удалить уже обработанное
- Это исключение, поэтому его после некой фазы обработки можно кинуть дальше
- Это объект, поэтому мы можем его легко передать куда угодно
- Это класс, поэтому мы выстраиваем свою иерархию классов
- И, наконец, это все еще исключение, а значит нам доступны все его стандартные свойства и методы. Да-да, и даже getTrace()!
Вместо заключения
Вот это всё, за небольшими нюансами, вполне себе боевой код, который я давно применяю, и даже учу своих студентов применять. Удивлен, что раньше нигде не видел такой простой концепции. Если вдруг я первый — передаю идею и код, приведенный в этой статье, в общественное достояние. Если не первый — простите автору его ошибки (с)
UPD по итогам комментариев
Благодарю всех комментаторов за ценные мысли и мнения.
Суть статьи — не в валидации. Вообще. Валидация — просто неудачный холиворный пример, просто мне не удалось придумать лучшего.
Суть очень проста. В PHP может существовать объект, являющийся и исключением, и коллекцией других исключений одновременно. И это бывает удобно.
Комментарии (126)
michael_vostrikov
17.03.2016 14:32+1Ваш код в статье ничем не отличается от эквивалентного кода без исключений.
$user = new User; $user->load($_POST); if ($user->save()) { redirect('hello.php'); } else { $this->view->assign('errors', $user->getErrors()); }
Можете привести какой-нибудь пример, где такое использование исключений дает что-нибудь полезное?
Еще такой вопрос. Допустим, перед повторным показом формы мне надо установить для поля пустое значение, которое пользователь должен заполнить. Например, сбросить пароль при ошибке авторизации. Получается, я делаю$model->password = ''
и получаю исключение RequiredException?AlexLeonov
17.03.2016 14:44+1Код отличается, и значительно.
Начнем с того, что вы ловите возможные ошибки на этапе синхронизации модели с БД. Я же — на этапе присвоения. Хотя это и не главное.
Мой код отличается от вашего тем, что исключение, в отличие от того, что вы вернули из метода getErrors() является объектом и, в силу этого факта, имеет тип. У меня могут быть разные типы исключений, например UserShortPassword, UserSimplePassword, которые а) наследуются от общего предка UserPasswordException и б) упакованы в коллекцию-итератор, которая сама тоже объект и Throwable. Это позволяет делать мне что-то вроде такого:
catch (FormMultiException $e) { if ($passwordErrors = $e->extractByColumn('password') { // что-то сделали с ошибками про пароль, в лог записали о том, что была попытка подбора, например // и все остальные ошибки бросили дальше throw $e; } }
Такое использование исключений полезно везде. Потому что дает вам то, чего нет в вашем примере — инкапсуляцию, типизацию и использование стандартных механизмов языка вместо велосипедов.
Вопрос же ваш до конца не понял. Зачем вы присваиваете модели данных пустое значения поля ради того, чтобы не показывать во view это значение? Не надо так делать.SamDark
17.03.2016 15:23Допустим, завернём мы наши ошибки в коллекцию типизированных объектов, наследованных от Error. И будем её возвращать, а не выкидывать. Чем это будет хуже?
AlexLeonov
17.03.2016 15:29Тем, что вместо прямого и понятно try...catch...throw наш код превратится в if-hell.
if ($result instanceof Errors) {
…
// и вот тут бы всё, поработали, отработали 5 ошибок из 7, надо бы оставшиеся 2 дальше отправить, а throw нет — что делать?
}
Вам это нравится?
И потом. Возвращать вы говорите. Секундочку, а как вы можете вернуть что-то из, скажем, __set()? Выбросить — можно. А вот вернуть из сеттера или конструктора — нельзя.SamDark
17.03.2016 15:37+3М… и чем 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()]);
SamDark
17.03.2016 15:38Это типичная задачка. Показать ошибки под полями формы и дать исправить.
AlexLeonov
17.03.2016 15:47+1Не надо игнорировать тот факт, что вызов save() невозможен, если не завершился успехом вызов load().
В моем случае исключения не дадут вам проигнорировать. В вашем — вы можете забыть написать очередной if.SamDark
17.03.2016 15:51Ну да, обычно это выглядит так:
if ($post->load($_POST) && $post->save()) { // success! } $this->render...
QuickStudio
17.03.2016 17:47-1Глупая конструкция в контексте Yii2 :)
Есть у меня форма, и в форму еще прокидываю UploadedFile. В случае если $_POST == [] (допустим, обновление только аватара, форма изменения профиля) функция load возвращает false. Даже не знаю насколько это поведение корректноAlexLeonov
17.03.2016 17:49-1Глупый Yii2 )))
Почему "процедура" объекта вообще возвращает что-то, кроме самого объекта?QuickStudio
17.03.2016 17:53Вы про метод load? Он возвращает результат присвоения. http://take.ms/KIO3u
AlexLeonov
17.03.2016 18:07+1Результат присвоения? А зачем?
- если присваивание прошло ожидаемо, мне не нужен true
- если что-то случилось — я хочу знать, что именно, а не просто false
— и для этого есть… пам-пам… механизм под названием "Исключения"!
— а если "случилось" несколько ошибок или проблем? не беда, у нас же исключение может быть коллекцией других исключений! (см. статью)
SamDark
17.03.2016 18:03Почему бы и нет? method chaining не всегда лучшее решение.
AlexLeonov
17.03.2016 18:09Потому что это не нужно. "Результат присваивания" вообще не нужен, понимаете? Если присваивание прошло штатно, мне не нужен true. Если возникли ошибки — мне нужны ошибки, а не false.
Ваш load() — типичная "процедура", то есть метод, лишь меняющий внутреннее состояние объекта, но не возвращающий результат. В этом случае принято возвращать сам объект в его новом состоянии.SamDark
17.03.2016 18:12В контексте статьи и организации валидации через исключения, естественно, понимаю. В контексте решения без исключений нет.
AlexLeonov
17.03.2016 18:16А зачем делать решения без исключений, если гораздо проще и логичнее это сделать с исключениями? Исключение — штатное средство языка. Метод getErrors() — велосипед, изобретенный от нежелания или неумения пользоваться штатными средствами.
Имхо.SamDark
17.03.2016 18:33Потому что они не особо хуже? Я не вижу прям сильного профита в решении из статьи, как и не вижу фатальных недостатков подхода.
AlexLeonov
17.03.2016 15:42Ваш код полностью выглядит так:
$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; }
SamDark
17.03.2016 15:49- Ваш код выше не будет работать потому что
$this->view->post = $post;
у вас никогда не выполнится. Исключение прерывает исполнение блока try. Без finally вы работать не сможете.
- В коде с if валидация не делается при load(). Вы же написали так, как буд-то делается. Два раза делать валидацию я смысла не вижу. Инициализировать массив ошибок и делать merge поэтому нет необходимости.
AlexLeonov
17.03.2016 15:53-4- Да, не выполнится. Но это не аргумент против подхода. Я упрощаю ровно также, как и вы ))
- А хреново, что не делается. Это в Yii, может, и принято валидацию данных в модели проводить только при попытке save(), и молча этот save() отменять, ничего не говоря. В реальных приложениях требование "валидация при присваивании" и "результат валидации сразу" встречается очень даже нередко.
SamDark
17.03.2016 16:09ОК. Давайте не упрощать, но и не сравнивать специфичный для фреймворка код с абстрактным описанием подхода.
Задача:
Есть форма. Пользователь вводит данные. Если данные не верны, вывести соответствующие сообщения под каждым полем с ошибкой.
- Валидация производится в момент присваивания данных модели.
- Метод load() только загружает данные. Метод save() только сохраняет модель. Метод validate() только валидирует.
- validate() либо кидает исключение-контейнер ошибок, либо возвращает этот контейнер ошибок. Получить ошибки из модели после нельзя.
- Модель и ошибки надо передать в view (и то и то нужно чтобы показать форму с ошибками). Допустим, делаем мы это через $this->render(название view, массив данных).
- 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]) }
AlexLeonov
17.03.2016 16:18+1Вы неправы ровно в тот момент, когда говорите "Есть форма, это модель". Это опять какой-то очень странный, антиархитектурный подход.
Разрешите я более точно переформулирую задачу.
Итак, у нас есть некий поток внешних данных. Мы полагаем, что эти данные — нечто, что ввел пользователь в форму создания поста в блоге, но полностью полагаться, конечно же, не можем.
Задача — создать пост в блоге. Или отобразить форму, в которой будет четко и внятно указано, почему это нельзя сделать.
Мой вариант:
// условный контроллер $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') %}
SamDark
17.03.2016 16:30«Есть форма, это модель»
ОК. Да, у меня привычка называть сущности моделями. Суть от этого не меняется. В нашем случае Post — это класс, хранящий данные и умеющий их сохранять и валидировать. Как ни назови.
Мне не ясно, почему вы валидируете два раза. Или мы валидируем при каждом изменении модели, тогда у нас она всегда валидна, что, несомненно, является плюсом. Или валидируем только при сохранении. Можем получить невалидную модель до сохранения её в хранилище, но экономим на количестве проверок. У вас же получается что и валидность модели сразу после присваивания не гарантируется и экономии никакой нет. Зачем?AlexLeonov
17.03.2016 16:35Мне не ясно, почему вы валидируете два раза.
Потому что может быть валидация конкретного значения в контексте поля модели ("Пароль не может быть короче 6 символов"), валидация комплекса данных в контексте модели ("Номер телефона и регион должны соответствовать друг другу") и даже валидация с использованием внешних зависимостей ("Имя пользователя должно быть уникальным")
Соответственно и валидаторы бывают разные. Вы, как разработчик, должны иметь выбор — какие использовать. А фреймворк — давать вам такую возможность.SamDark
17.03.2016 16:49Валидацию в контексте поля вы производите при каждом присваивании?
AlexLeonov
17.03.2016 16:54Я — кто?
Я — "разработчик класса абстрактной модели"? Нет, не произвожу при каждом присваивании. Но даю возможность произвести.
Я — "разработчик модели конкретного класса"? Нет, не произвожу при каждом присваивании. Лишь тогда, когда мне это надо. Захочу для данного поля в данном сценарии и контексте — буду. Не захочу — не буду.
AlexLeonov
17.03.2016 16:28И да, самое главное — при моем подходе во view отправляется не "модель" (а ее быть не может в случае ошибки валидации, модель не имеет права существовать в "разобранном" состоянии), а те данные, что ввел пользователь. Пусть он их видит, корректирует, улучшает, отправляет снова. Но. Это не модель.
vintage
17.03.2016 16:42+1Чуть усложним: регистрация пользователя. Нужно ввести логин, пароль и подтверждение. Облом может случиться в двух местах:
- При проверке пароля и подтверждения.
- При попытке создать пользователя с уже зарегистрированным логином.
Оба случая необходимо обрабатывать одинаково — показывать пользователю сообщение с предложением поменять значение поля.
Исключения позволяют элегантно и единообразно обработать различные ситуации, на каком бы уровне они ни произошли.
Но, конечно, подход с валидацией в сеттерах — не самый лучший. Я бы предложил ввести так называемые "инварианты" — функции, которые исполняются после конструктора и после каждого публичного метода, и проверяют, что состояние объекта удовлетворяет некоторым условиям.AlexLeonov
17.03.2016 16:46На самом деле, конечно же, в случае регистрации пользователя исключения будет бросать не модель пользователя. А компонент регистрации.
А валидация в сеттерах — это один их возможных подходов. Статья-то не про это )))SamDark
17.03.2016 16:49Компонент регистрации будет бросать MultiException?
AlexLeonov
17.03.2016 16:51class RegisterErrors extends MultiException
он будет кидатьSamDark
17.03.2016 16:54Ну то есть компонент регистрации фактически не отличается от модели пользователя. По своей сути это тоже модель. Только она save() делегирует пользователю.
AlexLeonov
17.03.2016 16:56Нет, конечно же )))
Ахаха, перестаньте так шутить )))
Компонент регистрации — это вообще не MVC. Это компонент. То есть некая часть бизнес-логики. Он юзает M, иногда даже V или C в своих грязных целях, но гораздо умнее при этом. И выполняет именно бизнес-процессы, а не beforeSave — save — afterSave, как любят разработчики на одном всем нам известном фреймворке )))SamDark
17.03.2016 17:08M в MVC — это модель в смысле "доменная логика", не в смысле "штука, которая сохраняет данные в базу".
AlexLeonov
17.03.2016 17:12Компонент регистрации управляет множеством объектов доменной области, сводя их поведение к бизнес-сценариями. Например объектами "Пользователь", "Группа", "Письмо о регистрации", "Отчет о количестве зарегистрированных", "Партнер, который привел реферала". Все они управляются в рамках сценария "Регистрация нового пользователя".
Это не MVC. Это то, что использует MVC в своих целях.
VolCh
18.03.2016 09:19Инварианты вещь хорошая, но проверка после каждого публичного метода очень часто избыточна, а зачастую и мешает, заставляя вводить новые публичные методы типа мержа объекта с массивом. Или использовать отражения и подобные технологии. В большинстве случаев необходимо и достаточно проверять целостность конкретной сущности и модели в целом лишь по завершению сценария
vintage
18.03.2016 14:57Да, в идеале было бы автоматом отслеживать зависимости инвариантов и проверять только то, что реально могло повлиять на них.
А метод мёржа с массивом — довольно полезная штука безотносительно инвариантов.VolCh
18.03.2016 15:37Не в зависимостях дело, а в сложности в процессе работы с объектом всегда поддерживать его в консистентном состоянии. Достаточно консистентности в конечном счёте в подавляющем большинстве случаев, если она необходима вообще.
vintage
18.03.2016 16:06Довольно странно оставлять объект в неконсистентном состоянии после вызова публичного метода.
VolCh
18.03.2016 18:27Обычная ситуация, например, должно быть заполнено только одно из пары полей в зависимости от значения третьего поля.
vintage
18.03.2016 18:40Так почему бы не заполнить оба поля за один вызов метода?
VolCh
18.03.2016 20:55Не всегда это легко сделать, даже если есть желание. Например, если заполнение происходит автоматически фреймворком типа DI-контейнера, десериализатора или ORM, который может только по одному значению за раз в сеттеры передавать.
- Да, не выполнится. Но это не аргумент против подхода. Я упрощаю ровно также, как и вы ))
- Ваш код выше не будет работать потому что
michael_vostrikov
17.03.2016 16:14+1Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?
Ну и да:
$errors = $user->getErrors(); $passwordErrors = $errors['password']; // можно isset добавить
А куда вам надо бросать дальше остальные ошибки? Если у нас сохранение в контроллере, нам все ошибки нужны здесь. Если у нас модель сама сохраняет что-то дополнительно, то она и будет обрабатывать. Контроллер не должен знать, что у нас там какая-то валидация в дополнительной модели не прошла, потому что он про дополнительную модель ничего не знает.
Я поэтому и попросил привести пример (а не абстрактный участок кода), где это действительно будет нужно.
Зачем вы присваиваете модели данных пустое значения поля ради того, чтобы не показывать во view это значение?
Это просто пример, в общем случае у нас могут быть значения, которые можно присваивать из программы, но нельзя присваивать из пользовательского ввода. Другой пример — поле было необязательным, в базе куча записей с пустым значением, потом решили сделать обязательным, при загрузке из БД вызывается __set(), который бросает исключение.AlexLeonov
17.03.2016 16:41Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?
MultiException — это контейнер. Я ловлю контейнер, а затем, используя его методы, могу получить нужные мне подмножества элементов этого контейнера.
Например в контроллере получить все исключения связанные с паролями, пройтись по ним, записать в лог, а сам контейнер отправить дальше, например во view.SamDark
17.03.2016 16:53Вот для чего иерархия:
foreach ($multiException->errors as $error) { echo $error->getMessage(); }
Единственное, от \Exception наследоваться не стоит. Незачем...AlexLeonov
17.03.2016 16:58foreach ($multiException->errors… )
зачем так сложно? почему не просто
foreach ($errors as $error)
Я начинаю подозревать, что вы не читали статью. Или немного не так ее поняли.SamDark
17.03.2016 17:09Что ArrayAccess забыл, да. Что скажете на тему полезности наследования от \Exception?
AlexLeonov
17.03.2016 17:13-1Вы мне предлагаете статью еще раз пересказать, на этот раз в комментариях лично для вас?
AlexLeonov
17.03.2016 16:42Другой пример — поле было необязательным, в базе куча записей с пустым значением, потом решили сделать обязательным, при загрузке из БД вызывается __set(), который бросает исключение.
Это называется "криворукие разработчики, не сумели сделать нормально миграцию БД" :)vintage
17.03.2016 16:46Простой пример: раньше указывать адрес было не обязательно и никто не указывал, а теперь обязательно. Предлагаете всем пользователям при миграции прописать БОМЖ?
AlexLeonov
17.03.2016 16:51NULL прописать. В SQL NULL — это неизвестное значение. Ровно то, что вам и нужно.
Почему из этого вдруг стала вытекать необходимость бросать исключение при создании моделей из записей БД — мне неведомо. Я бы не стал так делать.vintage
17.03.2016 17:42+1Тут дело в том, что если модель заполняется из пользовательского ввода, то кидать надо, а если выборкой из базы, то нет.
AlexLeonov
17.03.2016 17:47"Волга впадает в Каспийское море" (с)
Если валидацию делать грамотно, как проверку на соответствие значения контексту и сценарию, то ваш вопрос отпадет сам собой.michael_vostrikov
17.03.2016 18:46Итак, у нас есть метод
findOne($id)
. Он получает данные из БД, делает$model = new User()
, и устанавливает свойство$model->address = $dbData['address']
, при этом$dbData['address'] = null
. Вызывается метод__set()
.
Что именно вы предлагаете в нем делать, что означает "делать валидацию грамотно"? Считать, что NULL не является пустым значением и не бросать RequiredException? Или при загрузке из БД устанавливать специальную константу$model->scenario = "LOAD_FROM_DB"
, а в сеттере или валидаторе ее проверять?AlexLeonov
17.03.2016 19:18Я предлагаю при валидации учитывать не только $key и $val, но и $this. И тогда всё встает на свои места.
$this — это объект. У него есть состояние. В том числе может быть состояние "создан, чтобы быть заполненным фактическими данными из БД".VolCh
18.03.2016 15:38В том числе может быть состояние «создан, чтобы быть заполненным фактическими данными из БД».
Дырявая абстракция?AlexLeonov
18.03.2016 15:59Сценарий использования? Ну да, не самая лучшая. Но вы же знаете, все нетривиальные — дырявы.
impwx
17.03.2016 14:48+1Вижу в данном подходе несколько неудобных моментов:
- Каждый выброс исключения прерывает текущий поток исполнения. Код валидации из последовательного превращается в скачущий туда-сюда, и всё обмазывается несколькими уровнями вложенного перехвата исключений — понять его и проследить логику сложнее.
- Результат валидации отделяется от модели: получается, что модель либо полностью валидна, либо не может существовать впринципе. При отображении результатов валидации часто бывает нужно получить именно эти невалидные значения: например, поменять «Пароль слишком короткий» на «Пароль из {x} символов слишком короткий» — в вашем случае для этого придется менять валидатор.
- Непонятно, как валидировать связанные поля. Например, поле для подтверждения пароля или опциональные поля, зависящие от переключателя. В вашем случае порядок валидации полей никто не гарантирует, и одно поле может быть проверено до окончательного заполнения всей модели.
AlexLeonov
17.03.2016 14:52+1При всем уважении не вижу упомянутых вами минусов.
- Суть концепции мультиисключения как раз и состоит в том, что код НЕ ПРЕРЫВАЕТСЯ одним исключением. Вместо этого специальные методы накапливают коллекцию исключений и бросают ее.
- Да, результат валидации отделяется от модели. В этом и смысл. Модель не нужна, если она невалидна.
- Валидатор менять не надо. Просто передайте в конструктор исключения ссылку на модель. Это же ООП! Это же зависимости!
- Совершенно понятно. Просто напишите еще один валидатор, не для этапа заполнения данными, а классический beforeSave(). И, сюрприз — он тоже может бросать мультиисключение!
P.S. И да. В модели не может быть поля подтверждения пароля. В форме — может.impwx
17.03.2016 17:08+2Суть концепции мультиисключения как раз и состоит в том, что код НЕ ПРЕРЫВАЕТСЯ одним исключением.
Посмотрите на вашу функциюvalidatePassword
. Вместо того, чтобы проверить все правила сразу, она прерывается после первого. О том, что данные не подходят еще по какому-то критерию, пользователь должен узнавать, каждый раз переотправляя форму.
Просто передайте в конструктор исключения ссылку на модель.
Если мы создаем объект в блокеtry
и в процессе создания вылетает исключение, объектом больше пользоваться нельзя, поскольку его целостность не гарантируется. Выносить ссылку на него в блокcatch
— очень плохая идея.
Я предлагаю вам несколько пересмотреть терминологию. Валидировать-то нужно не модель, а саму форму, и если она валидна — применять данные из нее к модели. Тогда всё становится на свои места:
- Ошибки привязываются к полям формы, что более естественно для фронтенда
- Модель будет всегда валидна, а форма — всегда полностью инициализирована
- Не нужно разносить валидацию в разные места, если есть связанные поля
vintage
17.03.2016 17:45На самом деле, все такие параллельные валидации должны отработать ещё на кленте, так что ничего страшного, что сервер не прогоняет полную валидацию формы, а останавливается на первом же косяке.
AlexLeonov
17.03.2016 17:46Сервер вообще не может валидировать форму. У него нет никаких форм.
impwx
17.03.2016 17:47А что у него тогда есть?
AlexLeonov
17.03.2016 17:52У сервера-то?
Данные от пользователя. Неорганизованные. Заведомо грязные, невалидные и вредоносные.
И модели, описывающие то, какими должны быть организованные и чистые данные, синхронные с хранилищем.
А дальше уже вопрос вашей архитектуры, как вы одно превратите в другое.
Но, конечно же, никаких форм на сервере нет. Это вам всякие Yii врут нещадно.SamDark
17.03.2016 18:00Абстракция над данными, приходящими из формы или уходищими в неё, и их валидацией — обычное дело практически во всех фреймворках: https://symfony.com/doc/current/book/forms.html, http://framework.zend.com/manual/current/en/user-guide/forms-and-actions.html/.
AlexLeonov
17.03.2016 18:02+1"Это есть в Symfony" и "Это хорошая, годная архитектура" — не синонимы.
SamDark
17.03.2016 18:09ОК. Почему не должно быть никаких форм на сервере? Форма на сервере, в понимании перечисленных фреймворков (и Yii тоже) отвечает за то, чтобы грязные данные из реквеста проверить и признать валидными или нет.
AlexLeonov
17.03.2016 18:13Тогда и назовите это Валидатор. Или компонент "Вход пользователя". Или как-то иначе. Но не формой.
Форм на сервере не должно быть, потому что форм на сервере нет. Независимо от степени полёта вашей фантазии )))SamDark
17.03.2016 18:27Это, в общем-то, предмет не моей фантазии. Так уж повелось, что это называют формой практически все популярные фреймворки. Если я это буду назвать как-то ещё, суть от этого не изменится.
Кстати, Yii это зовёт form model, а не просто form. Сам класс называется Model. Назвать это валидатором будет неправильно потому что по факту это не валидатор, а набор данных плюс набор валидаторов плюс, опционально, какая-то логика их обработки.
impwx
17.03.2016 18:39+1Мы сейчас погрязнем в терминологическом споре, потому что сколько участников беседы, столько значений у одних и тех же терминов.
Я не пользовался ни Symfony, ни Yii. Зато в знакомом мне ASP.NET MVC есть понятия "модель" и "вьюмодель". Первое — понятно, объект из базы данных. А вот второе — это некая проекция модели, то есть данные, которые показываются пользователю или приходят от него после отправки формы. Именно это я имел в виду, говоря про "форму на сервере". И валидируется обычно именно она, а не модель, связанная с БД.SamDark
17.03.2016 19:26Именно так. Как это ни обозвать, смысл именно этот.
AlexLeonov
17.03.2016 23:44Нет. Смысла в этом нет. Ровно ноль.
Не существует объекта под названием "Модель формы". Это крайне неудачный паттерн, который не соответствует реальности.
Есть "данные извне". Есть "я, компонент, беру нужные мне данные и валидирую и чищу их". Есть "я раскладываю данные по моделям и сценариям, дергая за ниточки бизнес-логики". Даже есть "хелпер построения во view по ее схеме".
Но нет форм на сервере и моделей view на сервере. ASP.NET MVC ошибается.
Корень этой ошибки в том, что вы надеетесь на то, что от клиента к вам придут некие структурированные данные. А если нет? Не придут? Или не структурированные? Вы не можете контролировать процесс HTTP Request, но имеете иллюзию, что контролируете — якобы, что написали в "модели формы" на сервере, то мне и придет от клиента. Это неверно.
VolCh
18.03.2016 09:45+1Формы на сервере есть, они передаются хттп-запросами, обычно одним из майм-типов, специально созданных для форм. И следует различать задачи проверки корректности (с точки зрения бизнес-процессов в данном контексте) данных формы и задачи выявления ошибок обработки формы. При этом следует иметь в виду, что для пользователя особо без разницы не заполнил он имя пользователя в форме (ошибка запроса) или такое имя уже есть (ошибка обработки запроса) — сообщения об ошибках ему хочется видеть единообразные, привязанные к конкретному полю формы (как сущности интерфейса). С другой стороны очень часто формы и сущности модели соответствуют друг другу полностью и создатели фреймворков и библиотек позволяют особо их не разделять (а иногда даже не позволяют их нормально разделять). В той же Симфони сделано грамотно — объект, инкапсулирующий форму как элемент запроса, имеет объект с данными, который может являться по совместительству объ
AlexLeonov
18.03.2016 09:46-1Формы на сервере есть, они передаются хттп-запросами
Первый класс, вторая четверть.
И эти люди пытаются мне что-то доказывать...VolCh
18.03.2016 15:44Content-Type: application/x-www-form-urlencoded вам ни на что не намекает? Может он говорит о том, что в теле запросе содержится веб-форма?
AlexLeonov
18.03.2016 16:00+1Нет. Мне такой заголовок говорит "кто-то пытается передать мне некий запрос и хочет, чтобы я думал, что к запросу приложены данные, сформированные формой". Так ли это на самом деле — я не знаю.
VolCh
18.03.2016 09:51ъектом бизнес-модели, а может не являться. При этом вадидатор проверяет на соответствие каким-то правилам и возвращает подробный результат проверки тот объект, который ему скажут, практически любой объект пхп
VolCh
18.03.2016 15:40Модель не нужна, если она невалидна.
Кому-то иногда не нужна, кому-то иногда нужна, кому-то всегда нужна.
И да. В модели не может быть поля подтверждения пароля.
Может. Кто сказал, что не может?
alexpogodin
19.03.2016 02:05Скажите, а у модели могут быть поля Дата рождения и Дата поступления на курс, например. Сами придумаете, какая между ними валидация?
Big_Shark
19.03.2016 09:30Если рассматривать модель как Entity, то она всегда валидна, нельзя создать не валидную Entity. Так что тут все нормально, про поле для подтверждения пароля Entity вообще ничего не знает, не ее зона ответственности, вы путаете валидацию входных данных, и "валидацию" бизнес объекта.
arvitaly
18.03.2016 00:17+1- Для меня любой пользовательский ввод — штатная ситуация. Не увидел аргументов, почему для вас иначе?
- Исключения для данной задачи несут логический overhead, так как дают возможность написать код без try/catch и ловить их в любом другом месте программы, что, в отличии от критических ситуаций, бессмысленно. А отказ от функционального подхода на пустом месте, в свою очередь, исключает возможность статического анализа кода (единственная попытка это сделать была в Java с throws, и, фактически, это признано неэффективным, а в PHP и вовсе нет таких возможностей).
Мой код отличается от вашего тем, что исключение, в отличие от того, что вы вернули из метода getErrors() является объектом и, в силу этого факта, имеет тип.
Возвращайте объект.Big_Shark
19.03.2016 09:46+1Ок, вы пишете консольную команду которая создает пользователя, и вы написали так
$username = 'Admin'; $password = 'admin'; $user = new User($username, $password);
Но у вас есть бизнес требование, пароль должен быть больше 8 символов, где и как вы будете делать проверку на это бизнес требование?
Rathil
18.03.2016 01:05+1А если у меня несколько валидаторов на одно поле? И скажем оба не проходят, то что, будет одна ошибка на одно поле, а о остальных забудем?
Поправте код, при сетере — нужно пробегать валидаторы тоже в цикле и каждый обварачивать траем, а потом создавать коллекцию ошибок на каждое поле!
Rathil
18.03.2016 01:12- я не вижу смысла валедации при сетере, а не отдельным методом. Если я сетю 5К раз, но сохраняю (обрабатываю) значения только раз — зачем мне каждый раз их валедировать при сетере? Также сетер может вызываться при, скажем, выборке из БД, то тут можно попасть в так… Когда валидаторы поменялись, а данные ещё нет! Любая выборка из БЛ будет порождать ошибки!
VolCh
18.03.2016 15:48Да и отдельным методом (не вызываемом автоматически при каждом чихе) в самом валидируемом объекте далеко не всегда имеет смысл
Rathil
18.03.2016 17:02Ну "при каждом чихе" — это и есть сеттер, но, имхо, там не верно валедировать. Сделайте валидацию перед сохранением, после спец. методом fill, но не сеттерах.
egor_nullptr
18.03.2016 19:573. Исключения управляют потоком.
Исключения не стоит использовать для управления потоком.
Всем кто хочет понять (или закрепить) в каких случаях стоит, а в каких не стоит, использовать исключения, советую почитать вот это.
akubintsev
18.03.2016 21:33-2try-catch я воспринимаю как некий подвид goto, связанный с ошибками, которые нужно словить далеко от текущей области видимости.
Описанный подход можно конечно использовать, но он мне кажется больше сопряжен с возможностью накосячить.
nickon
18.03.2016 22:13Ок, допустим мне нужно проверить сразу все правила валидации, чтобы подсветить все ошибочные
поля и указать в чём именно пользователь был не прав.
Как быть?AlexLeonov
19.03.2016 13:48Ровно об этом и написана статья.
Шаг 1. Для каждого поля пишется валидатор. Валидатор может либо throw Exception либо yield Exception (это не указано в статье, каюсь, надо было написать сразу)
Шаг 2. Нечто, что заполняет модель данными собирает все выброшенные либо сгенерированные валидаторами отдельных полей исключения в объект MultiException
Шаг 3. Бросается MultiException, вы его в нужном месте ловите и обрабатываете.
Если станет интересно — могу дать и конкретный код, реализующий этот подход, и тесты для негоnickon
19.03.2016 15:49Если можно пример к такому коду:
$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', ... ], ... ]
AlexLeonov
19.03.2016 18:30try { $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(); } } }
AlexLeonov
19.03.2016 18:32Внутри модели ПРИМЕРНО так:
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);
но на практике такое применяется реже
AlexLeonov
19.03.2016 18:53В результате вы получаете не невнятный массив, а коллекцию объектов-исключений.
Первый плюс в том, что исключение — это тоже объект, вы можете выстраивать свою иерархию классов, добавлять свои свойства и методы (здесь это уже сделано, есть свойство $exception->column, к примеру).
Второй плюс в том, что коллекция, хотя и похожа на массив, все-таки тоже объект, и у нее множество полезных методов (filter, sort, map, reduce, group, find — да мало ли можно придумать!).
Третий плюс в том, что мультиисключение — это не только массив и не только объект, но и тоже исключение! Его можно поймать, изменить, бросить дальше.
ImLiar
18.03.2016 22:36На что только не идут люди, лишь бы не писать на нормальных языках с either и монад трансформерами
bobermaniac
Данные не проходят валидацию — это не исключительная ситуация, поэтому такой код семантически некорректен.
AlexLeonov
Отчего же не исключительная? Я с вами не согласен. Непрохождение валидации и есть самый правильный пример исключения.
Штатно мы ожидаем, что пользователь введет в форму верный логин и пароль. Пользователь ошибся. Это — исключительная ситуация, то есть такая ситуация, когда мы не можем дальше идти по штатному пути исполнения, но при этом такую ситуацию предвидим и знаем, что с ней делать.
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
Внешние данные (от пользователя) сделали дальнейшие действия по базовому алгоритму (авторизацию) бессмысленными. Это — исключение.
bobermaniac
Ok, let's talk 'bout philosophy.
Штатная исключительная ситуация — это ситуация, которую программа способна распознать и продолжить корректную работу после ее возникновения. К таковым относятся, к примеру, ошибка валидации, ошибка аутентификации, и прочие ошибки, которые программист способен предусмотреть и обработать без потери целостности и консистентности состояния системы.
Нештатная исключительная ситуация — это сиутация, при возникновении которой дальнейшая работа программы невозможна ввиду недетерминированности ее внутреннего состояния. К примеру, к таковым относятся удаление файла БД в приложении.
Где-то между ними болтается класс ситуаций, которые невозможно распознать напрямую, но в то же время их возможно изолировать без нарушения внутреннего состояния приложения. Например, если вы внезапно получили через web api вмеcто json какой-то странный html с 200 кодом возврата. Понятно, что сделать с ним вы ничего не можете, но совершенно необязательно это разрушает ваше приложение.
Я утверждаю, что обрабатывать штатные исключительные ситуации необходимо с помощью кодов возврата. Это может быть что угодно, принятое для вашего языка — от getlasterror до монады error. Нештатные исключительные ситуации должны выбрасывать терминальные исключения и завершать работу приложения (с 500 ошибкой для веба). То что болтается посередине обрабатывается исходя из возможности восстановить работоспособность приложения.
То есть согласно моей позиции исключения могут быть только терминальными, но допустимо использовать их обработчики, которые, условно говоря, перед завершением приложения покажут пользователю красивую картинку, что что-то пошло не так и мы это чиним.
Я, конечно, не могу утверждать, что это единственно возможный подход, однако мой опыт подсказывает мне, что он самый оправданный.
AlexLeonov
Уложите пожалуйста мне в код возврата информацию ['Неверный email', 'Слишком короткий пароль', 'И про капчу ты, уважаемый юзер, тоже забыл']
bobermaniac
На это очень тяжело отвечать серьезно, потому что в plain old C именно так и делали, и выглядело это вот так:
https://www.openssl.org/docs/manmaster/crypto/ERR_error_string.html
Конечно, подход не самый красивый, но он связан в первую очередь с ограниченим языка. Вы в PHP с объектной моделью можете возвращать некий ValidationResult, который содержит всю необходимую информацию.
AlexLeonov
Вы не поверите, но я ровно это и делаю в подходе, который описан в статье. Возвращаю (бросаю) некий ValidationErrors, который является коллекцией ValidationColumnError.
Вся суть только в том, что и отдельный Error и их коллекция плюсом ко всем своим плюсам еще являются Throwable. И это очень удобно на практике.
bobermaniac
Я и не утверждаю, что это неудобно. Я утверждаю, что это некорректно.
AlexLeonov
Я уважаю ваше мнение, однако спорю с ним, и утверждаю, что использование исключений в данном контексте вполне корректно.
Это удобное средство инкапсуляции информации об ошибках и управления потоком программы. Я не вижу причин, почему исключения в приложении должны быть исключительно терминальными (фактически — аналогом фатальных ошибок).
Dreyk
Тут вопрос не в том что возвращать, а как. Практически во всех языках программирования разбрасываться исключенями — не самое дешевое занятие. "Don't use Exceptions for flow control" — основной гайдлайн почти везде.
Исключение указывает на "исключительную" ситуацию.
Ситуация "Юзер ввел неправильные данные" — не является исключительной, это один из юз-кейсов программы.
Стремление пробросить исключение, чтобы оно всплыло наверх через несколько слоев логики, — признак плохо продуманной архитектуры
AlexLeonov
В PHP нет такого гайдлайна. И не в PHP тоже — находится только одна статья в MSDN десятилетней давности. Весьма спорная статья в которой речь лишь о том, что в древнем .Net throw на 3 порядка медленнее чем return. Какое это имеет отношение к моей статье?
И кстати. Я уверен, что в PHP нет ситуации, когда стоимость исключения в сравнении с не-исключением стала бы критично важной.
Это самая натуральная исключительная ситуация. Ожидаемая нами. Дальнейшее нормальное выполнение программы невозможно, у нас проблема, начинаем с ней работать.
bobermaniac
Ну я, если честно, не очень понимаю, почему при вводе неверных креденшалов у вас «дальнейшее выполнение программы невозможно».
Пользователь облажался в одной букве, а вы в ответ АЛЯРМ ААА ПАНИКА НЕПРАВИЛЬНЫЙ ПАРОЛЬ ВСЕ В ИСКЛЮЧЕНИЕ.
Ну несерьезно это.
AlexLeonov
Странно. Почему же вы не понимаете? Пользователь ввел неверные данные для входа. Следовательно дальнейшее ШТАТНОЕ выполнение программы (а именно — процедура аутентификации и авторизации) разумеется невозможно. Или вы предлагаете пускать любого с любым паролем? Конечно же нет.
В полном соответствии с принципом единой ответственности компонент авторизации выбрасывает исключение (или их коллекцию). Тем самым он заканчивает свою работу (и действительно, делать ему больше нечего) и передает ответственность другим слоям кода, которые уже должны решить, что делать дальше. Если это веб-контроллер — покажем снова форму с ошибками (а мультиисключение и есть эти ошибки). Если это некое API — вернем нужный HTTP-код и опять же сообщим о возникших ошибках.
Я этого нигде не говорил.
rinat_crone
Странно, что и вы не хотите услышать собеседников, хоть сами к этому призываете. Правильно говорят выше – исключение это когда вам ломают ноги и вы становитесь нетрудоспособным (коннекта к БД, допустим, нет – это исключение). Ошибка валидации – это когда вместо кофе приносят чай, на трудоспособность не сказывается, просим заменить чай на кофе -> profit.
Почитайте language-agnostic дискуссию на stackoverflow: http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control.
AlexLeonov
Я вас слышу. Но разве обязан быть с вами согласен? Нет.
Это ваше мнение. Мое другое — нет, не только когда ломают ноги.
Если бы все были согласны с вашей точкой зрения, дискуссии бы не было. А она есть. И да, вы же читали ту дискуссию, которую мне желаете почитать?
Вот вам пара цитат:
Снова тот же избитый аргумент про старый .Net. Давно уже опровергнутый.
И тут же:
Ну и под конец дискуссии:
Так что дискуссия по ссылке еще раз подтверждает только то, что вопрос спорный.
rinat_crone
Успехов Вам в написании и тестировании Ваших Исключительных Приложений!
AlexLeonov
Спасибо. И вам тоже.
AlexLeonov
И кстати, статья-то не про валидацию и не про пароль. Жаль, что вы это не заметили.
VolCh
В контексте типового использования пхп, а именно как cgi-скрипта, именно невозможно продолжить нормальное исполнение программы. Все, что можем сделать — корректно освободить ресурсы и вывести сообщение об ошибке, желательно с указанием причин.
SamDark
Смотря кто его читает и какие у него границы исключительности ситуации.