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

Вы только гляньте. Вот например 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()
не исключения. Следовательно мы лишены множества преимуществ:

  1. Исключения типизированы. В фреймворках же, подобных вышеупомянутым, я не могу создать иерархию FormException --> FormFieldException --> FormPasswordFieldException --> FormPasswordFieldNotSameException. Это очень важно, особенно с выходом PHP 7, который делает тайп-хинтинги наконец-то нормой и стандартом
  2. Исключения инкапсулируют в себе много нужного. Это же ООП! Например: на какой странице (URL) возникла ошибка валидации? Кто пользователь? Какое конкретно поле формы? Какое правило валидации сработало? Наконец «а дай-ка перевод этого сообщения на эстонский». Может ли это всё сделать простой массив сообщений об ошибках? Конечно же нет. (Кстати, достаточно реализовать метод __toString() и исключение в шаблоне продолжит вести себя как простое сообщение об ошибке)
  3. Исключения управляют потоком. Я могу его бросить. Оно всплывает. Я могу его поймать, а могу поймать и бросить дальше. Массив $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)


  1. bobermaniac
    17.03.2016 12:43
    +9

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


    1. AlexLeonov
      17.03.2016 12:48
      +4

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

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

      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

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

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


      1. bobermaniac
        17.03.2016 12:59
        +1

        Ok, let's talk 'bout philosophy.

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

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

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

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

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

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


        1. AlexLeonov
          17.03.2016 13:02

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

          Уложите пожалуйста мне в код возврата информацию ['Неверный email', 'Слишком короткий пароль', 'И про капчу ты, уважаемый юзер, тоже забыл']


          1. bobermaniac
            17.03.2016 13:10

            На это очень тяжело отвечать серьезно, потому что в plain old C именно так и делали, и выглядело это вот так:
            https://www.openssl.org/docs/manmaster/crypto/ERR_error_string.html

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


            1. AlexLeonov
              17.03.2016 13:17

              Вы не поверите, но я ровно это и делаю в подходе, который описан в статье. Возвращаю (бросаю) некий ValidationErrors, который является коллекцией ValidationColumnError.

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


              1. bobermaniac
                17.03.2016 13:46
                +1

                Я и не утверждаю, что это неудобно. Я утверждаю, что это некорректно.


                1. AlexLeonov
                  17.03.2016 13:48
                  +3

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

                  Это удобное средство инкапсуляции информации об ошибках и управления потоком программы. Я не вижу причин, почему исключения в приложении должны быть исключительно терминальными (фактически — аналогом фатальных ошибок).


              1. Dreyk
                17.03.2016 13:48

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

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


                1. AlexLeonov
                  17.03.2016 13:51
                  +1

                  «Don't use Exceptions for flow control» — основной гайдлайн почти везде.

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

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

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

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


                  1. bobermaniac
                    17.03.2016 13:54

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

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

                    Ну несерьезно это.


                    1. AlexLeonov
                      17.03.2016 13:57
                      +1

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

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

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

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

                      Я этого нигде не говорил.


                      1. rinat_crone
                        17.03.2016 14:51

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

                        Почитайте language-agnostic дискуссию на stackoverflow: http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control.


                        1. AlexLeonov
                          17.03.2016 15:02
                          +2

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

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

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

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

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

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

                          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.

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


                          1. rinat_crone
                            17.03.2016 15:03
                            -3

                            Успехов Вам в написании и тестировании Ваших Исключительных Приложений!


                            1. AlexLeonov
                              17.03.2016 15:04
                              +2

                              Спасибо. И вам тоже.


                    1. AlexLeonov
                      17.03.2016 14:03
                      +1

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


                    1. VolCh
                      18.03.2016 09:04
                      +1

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


    1. SamDark
      17.03.2016 18:37

      Смотря кто его читает и какие у него границы исключительности ситуации.


  1. AlexLeonov
    17.03.2016 13:02
    -1

    ...


  1. 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?


    1. AlexLeonov
      17.03.2016 14:44
      +1

      Код отличается, и значительно.

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

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

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

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

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


      1. SamDark
        17.03.2016 15:23

        Допустим, завернём мы наши ошибки в коллекцию типизированных объектов, наследованных от Error. И будем её возвращать, а не выкидывать. Чем это будет хуже?


        1. AlexLeonov
          17.03.2016 15:29

          Тем, что вместо прямого и понятно try...catch...throw наш код превратится в if-hell.
          if ($result instanceof Errors) {

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

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

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


          1. 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()]);


            1. SamDark
              17.03.2016 15:38

              Это типичная задачка. Показать ошибки под полями формы и дать исправить.


              1. AlexLeonov
                17.03.2016 15:47
                +1

                Не надо игнорировать тот факт, что вызов save() невозможен, если не завершился успехом вызов load().
                В моем случае исключения не дадут вам проигнорировать. В вашем — вы можете забыть написать очередной if.


                1. SamDark
                  17.03.2016 15:51

                  Ну да, обычно это выглядит так:

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


                  1. QuickStudio
                    17.03.2016 17:47
                    -1

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


                    1. AlexLeonov
                      17.03.2016 17:49
                      -1

                      Глупый Yii2 )))

                      Почему "процедура" объекта вообще возвращает что-то, кроме самого объекта?


                      1. QuickStudio
                        17.03.2016 17:53

                        Вы про метод load? Он возвращает результат присвоения. http://take.ms/KIO3u


                        1. AlexLeonov
                          17.03.2016 18:07
                          +1

                          Результат присвоения? А зачем?

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


                      1. SamDark
                        17.03.2016 18:03

                        Почему бы и нет? method chaining не всегда лучшее решение.


                        1. AlexLeonov
                          17.03.2016 18:09

                          Потому что это не нужно. "Результат присваивания" вообще не нужен, понимаете? Если присваивание прошло штатно, мне не нужен true. Если возникли ошибки — мне нужны ошибки, а не false.

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


                          1. SamDark
                            17.03.2016 18:12

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


                            1. AlexLeonov
                              17.03.2016 18:16

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

                              Имхо.


                              1. SamDark
                                17.03.2016 18:33

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


                    1. SamDark
                      17.03.2016 17:53

                      При чём тут вообще Yii?


                      1. QuickStudio
                        17.03.2016 18:03

                        Увидел и вспомнилось


            1. 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;
              }


              1. SamDark
                17.03.2016 15:49

                1. Ваш код выше не будет работать потому что $this->view->post = $post; у вас никогда не выполнится. Исключение прерывает исполнение блока try. Без finally вы работать не сможете.

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


                1. AlexLeonov
                  17.03.2016 15:53
                  -4

                  1. Да, не выполнится. Но это не аргумент против подхода. Я упрощаю ровно также, как и вы ))

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


                  1. SamDark
                    17.03.2016 16:09

                    ОК. Давайте не упрощать, но и не сравнивать специфичный для фреймворка код с абстрактным описанием подхода.

                    Задача:

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

                    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])
                    }


                    1. 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') %}


                      1. SamDark
                        17.03.2016 16:30

                        «Есть форма, это модель»

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

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


                        1. AlexLeonov
                          17.03.2016 16:35

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

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

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


                          1. SamDark
                            17.03.2016 16:49

                            Валидацию в контексте поля вы производите при каждом присваивании?


                            1. AlexLeonov
                              17.03.2016 16:54

                              Я — кто?

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

                              Я — "разработчик модели конкретного класса"? Нет, не произвожу при каждом присваивании. Лишь тогда, когда мне это надо. Захочу для данного поля в данном сценарии и контексте — буду. Не захочу — не буду.


                    1. AlexLeonov
                      17.03.2016 16:28

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


                      1. SamDark
                        17.03.2016 16:32

                        Это не повлияет на код примера никак.


                    1. vintage
                      17.03.2016 16:42
                      +1

                      Чуть усложним: регистрация пользователя. Нужно ввести логин, пароль и подтверждение. Облом может случиться в двух местах:

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

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

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

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


                      1. AlexLeonov
                        17.03.2016 16:46

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

                        А валидация в сеттерах — это один их возможных подходов. Статья-то не про это )))


                        1. SamDark
                          17.03.2016 16:49

                          Компонент регистрации будет бросать MultiException?


                          1. AlexLeonov
                            17.03.2016 16:51

                            class RegisterErrors extends MultiException

                            он будет кидать


                            1. SamDark
                              17.03.2016 16:54

                              Ну то есть компонент регистрации фактически не отличается от модели пользователя. По своей сути это тоже модель. Только она save() делегирует пользователю.


                              1. AlexLeonov
                                17.03.2016 16:56

                                Нет, конечно же )))
                                Ахаха, перестаньте так шутить )))

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


                                1. SamDark
                                  17.03.2016 17:08

                                  M в MVC — это модель в смысле "доменная логика", не в смысле "штука, которая сохраняет данные в базу".


                                  1. AlexLeonov
                                    17.03.2016 17:12

                                    Компонент регистрации управляет множеством объектов доменной области, сводя их поведение к бизнес-сценариями. Например объектами "Пользователь", "Группа", "Письмо о регистрации", "Отчет о количестве зарегистрированных", "Партнер, который привел реферала". Все они управляются в рамках сценария "Регистрация нового пользователя".

                                    Это не MVC. Это то, что использует MVC в своих целях.


                      1. VolCh
                        18.03.2016 09:19

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


                        1. vintage
                          18.03.2016 14:57

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

                          А метод мёржа с массивом — довольно полезная штука безотносительно инвариантов.


                          1. VolCh
                            18.03.2016 15:37

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


                            1. vintage
                              18.03.2016 16:06

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


                              1. VolCh
                                18.03.2016 18:27

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


                                1. vintage
                                  18.03.2016 18:40

                                  Так почему бы не заполнить оба поля за один вызов метода?


                                  1. VolCh
                                    18.03.2016 20:55

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


      1. michael_vostrikov
        17.03.2016 16:14
        +1

        Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?
        Ну и да:

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

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

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

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

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


        1. AlexLeonov
          17.03.2016 16:41

          Как именно вы используете эту иерархию с UserShortPassword, UserSimplePassword, если у вас всегда нужно ловить MultiException?

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

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


          1. SamDark
            17.03.2016 16:53

            Вот для чего иерархия:

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

            Единственное, от \Exception наследоваться не стоит. Незачем...


            1. AlexLeonov
              17.03.2016 16:58

              foreach ($multiException->errors… )

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

              foreach ($errors as $error)

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


              1. SamDark
                17.03.2016 17:09

                Что ArrayAccess забыл, да. Что скажете на тему полезности наследования от \Exception?


                1. AlexLeonov
                  17.03.2016 17:13
                  -1

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


                  1. SamDark
                    17.03.2016 17:23
                    -1

                    Перечитал ещё раз. Нашёл под спойлером в коде __set().


        1. AlexLeonov
          17.03.2016 16:42

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

          Это называется "криворукие разработчики, не сумели сделать нормально миграцию БД" :)


          1. vintage
            17.03.2016 16:46

            Простой пример: раньше указывать адрес было не обязательно и никто не указывал, а теперь обязательно. Предлагаете всем пользователям при миграции прописать БОМЖ?


            1. AlexLeonov
              17.03.2016 16:51

              NULL прописать. В SQL NULL — это неизвестное значение. Ровно то, что вам и нужно.

              Почему из этого вдруг стала вытекать необходимость бросать исключение при создании моделей из записей БД — мне неведомо. Я бы не стал так делать.


              1. vintage
                17.03.2016 17:42
                +1

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


                1. AlexLeonov
                  17.03.2016 17:47

                  "Волга впадает в Каспийское море" (с)

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


                  1. 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", а в сеттере или валидаторе ее проверять?


                    1. AlexLeonov
                      17.03.2016 19:18

                      Я предлагаю при валидации учитывать не только $key и $val, но и $this. И тогда всё встает на свои места.
                      $this — это объект. У него есть состояние. В том числе может быть состояние "создан, чтобы быть заполненным фактическими данными из БД".


                      1. VolCh
                        18.03.2016 15:38

                        В том числе может быть состояние «создан, чтобы быть заполненным фактическими данными из БД».

                        Дырявая абстракция?


                        1. AlexLeonov
                          18.03.2016 15:59

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


  1. impwx
    17.03.2016 14:48
    +1

    Вижу в данном подходе несколько неудобных моментов:

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


    1. AlexLeonov
      17.03.2016 14:52
      +1

      При всем уважении не вижу упомянутых вами минусов.

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

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


      1. impwx
        17.03.2016 17:08
        +2

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

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

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

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

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

        • Ошибки привязываются к полям формы, что более естественно для фронтенда
        • Модель будет всегда валидна, а форма — всегда полностью инициализирована
        • Не нужно разносить валидацию в разные места, если есть связанные поля


        1. AlexLeonov
          17.03.2016 17:14

          Я ровно об этом и говорю тут. Выше. Модель для примера была дана...


        1. vintage
          17.03.2016 17:45

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


          1. AlexLeonov
            17.03.2016 17:46

            Сервер вообще не может валидировать форму. У него нет никаких форм.


            1. impwx
              17.03.2016 17:47

              А что у него тогда есть?


              1. AlexLeonov
                17.03.2016 17:52

                У сервера-то?
                Данные от пользователя. Неорганизованные. Заведомо грязные, невалидные и вредоносные.
                И модели, описывающие то, какими должны быть организованные и чистые данные, синхронные с хранилищем.

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

                Но, конечно же, никаких форм на сервере нет. Это вам всякие Yii врут нещадно.


                1. 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/.


                  1. AlexLeonov
                    17.03.2016 18:02
                    +1

                    "Это есть в Symfony" и "Это хорошая, годная архитектура" — не синонимы.


                    1. SamDark
                      17.03.2016 18:09

                      ОК. Почему не должно быть никаких форм на сервере? Форма на сервере, в понимании перечисленных фреймворков (и Yii тоже) отвечает за то, чтобы грязные данные из реквеста проверить и признать валидными или нет.


                      1. AlexLeonov
                        17.03.2016 18:13

                        Тогда и назовите это Валидатор. Или компонент "Вход пользователя". Или как-то иначе. Но не формой.

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


                        1. SamDark
                          17.03.2016 18:27

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

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


                        1. impwx
                          17.03.2016 18:39
                          +1

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

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


                          1. SamDark
                            17.03.2016 19:26

                            Именно так. Как это ни обозвать, смысл именно этот.


                            1. AlexLeonov
                              17.03.2016 23:44

                              Нет. Смысла в этом нет. Ровно ноль.

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

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

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

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


                        1. VolCh
                          18.03.2016 09:45
                          +1

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


                          1. AlexLeonov
                            18.03.2016 09:46
                            -1

                            Формы на сервере есть, они передаются хттп-запросами

                            Первый класс, вторая четверть.
                            И эти люди пытаются мне что-то доказывать...


                            1. VolCh
                              18.03.2016 15:44

                              Content-Type: application/x-www-form-urlencoded вам ни на что не намекает? Может он говорит о том, что в теле запросе содержится веб-форма?


                              1. AlexLeonov
                                18.03.2016 16:00
                                +1

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


                                1. VolCh
                                  18.03.2016 18:28

                                  Какой формой? Вообще, что вы называете формой?


                          1. VolCh
                            18.03.2016 09:51

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


      1. VolCh
        18.03.2016 15:40

        Модель не нужна, если она невалидна.

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

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

        Может. Кто сказал, что не может?


        1. AlexLeonov
          18.03.2016 16:00

          А кто сказал, что может? Yii?


      1. alexpogodin
        19.03.2016 02:05

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


    1. Big_Shark
      19.03.2016 09:30

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


  1. arvitaly
    18.03.2016 00:17
    +1

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

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

    Возвращайте объект.


    1. Big_Shark
      19.03.2016 09:46
      +1

      Ок, вы пишете консольную команду которая создает пользователя, и вы написали так

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

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


  1. Rathil
    18.03.2016 01:05
    +1

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


    1. Rathil
      18.03.2016 17:03
      -1

      Хм, минусуете — так хоть аргументируйте чего :)


  1. Rathil
    18.03.2016 01:12

    • я не вижу смысла валедации при сетере, а не отдельным методом. Если я сетю 5К раз, но сохраняю (обрабатываю) значения только раз — зачем мне каждый раз их валедировать при сетере? Также сетер может вызываться при, скажем, выборке из БД, то тут можно попасть в так… Когда валидаторы поменялись, а данные ещё нет! Любая выборка из БЛ будет порождать ошибки!


    1. VolCh
      18.03.2016 15:48

      Да и отдельным методом (не вызываемом автоматически при каждом чихе) в самом валидируемом объекте далеко не всегда имеет смысл


      1. Rathil
        18.03.2016 17:02

        Ну "при каждом чихе" — это и есть сеттер, но, имхо, там не верно валедировать. Сделайте валидацию перед сохранением, после спец. методом fill, но не сеттерах.


  1. egor_nullptr
    18.03.2016 19:57

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

    Исключения не стоит использовать для управления потоком.
    Всем кто хочет понять (или закрепить) в каких случаях стоит, а в каких не стоит, использовать исключения, советую почитать вот это.


  1. akubintsev
    18.03.2016 21:33
    -2

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


  1. nickon
    18.03.2016 22:13

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


    1. AlexLeonov
      19.03.2016 13:48

      Ровно об этом и написана статья.

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

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


      1. 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', ... ],
          ...
        ]


        1. AlexLeonov
          19.03.2016 18:30

          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();
              }
            }
          }


        1. 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); 

          но на практике такое применяется реже


        1. AlexLeonov
          19.03.2016 18:53

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

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


  1. ImLiar
    18.03.2016 22:36

    На что только не идут люди, лишь бы не писать на нормальных языках с either и монад трансформерами