Pull to refresh

Comments 22

Так Star Wars Day вчера (4 мая) же был

У меня студенты на первом уроке первого курса по PHP учатся писать условия, как завещал великий мастер :))

Судя по коду, правильнее так


try {
// ...
} catch (Exceptions $errors) {
  foreach ($errors as $error) {
    echo $error->getMessage();
  }
} catch (Exception $error) {
   echo $error->getMessage();
}

Т.к. Exceptions не наследуется от Exception


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

Ветка
} catch (Exception $error) {

в данном конкретном случае не нужна, поскольку конструктор стандартного класса все без исключения Throwable заворачивает в Exceptions: https://github.com/RunnMe/Core/blob/master/src/Core/Std.php#L53

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

И все-таки не понятно, зачем ошибки валидации делать исключениями

Как уже ниже написали, я в свое время писал даже отдельную статью на эту тему. Вкратце:
1. Инкапсулировать всю информацию об ошибке в один объект
2. Выстраивать иерархию классов таких объектов
3. Иметь возможность разделить возникновение ошибки валидации (throw) и ее обработки (catch) любым количеством уровней кода, не заботясь о цепочке возвратов.
Всё это нас приводит к исключениям. Другого подобного механизма в PHP нет.
в данном конкретном случае не нужна, поскольку конструктор стандартного класса все без исключения Throwable заворачивает в Exceptions: https://github.com/RunnMe/Core/blob/master/src/Core/Std.php#L53

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

В этом конкретном примере (валидация в конструкторе объекта класса Std) вы всегда из конструктора будете ловить именно объект класса Exceptions — коллекцию ВСЕХ исключений, которые были выброшены внутри конструктора различными валидаторами. Даже если в этой коллекции будет всего одно исключение.

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

Так а в чем профит-то? Конкретно можно, пожалуйста, что вам это дает? Какое преимущество перед использованием обычного message bag?
Была отдельная статья на эту тему: https://habrahabr.ru/post/279501/
Если после ее прочтения у вас останутся вопросы — задавайте, с удовольствием на них отвечу.

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

tl;dr Я так и не понял, вы вроде бы за type safety (коллекции, нул объекты) и вроде как и нет (неявное возвращение значений при помощи throw вместо явного).


получает особую суперспособность: его можно «выбросить» (throw).

Это в свою очередь порождает сайд эффекты. С великой силой приходит великая ответственность.


Имплементация интерфейса JsonSerializable неспроста добавлена сразу же в Runn\Core\Exception: это явный задел на будущее

Что делать если я захочу в debug режиме добавлять помимо кода ошибки и описания еще и стэктрэйс? Налицо нарушение open/close принципа. Да, это просто, но намного проще будет сделать так:


class JsonExceptionFormatter implements ExceptionFormatter
{
    private $shouldIncludeDebugInfo;

    public function __construct(bool $debug)
    {
        $this->shouldIncludeDebugInfo = $debug;
    }

    public function format(Throwable $e)
    {
         return new JsonResponse(array_merge(
              [
                  'code' => $e->getCode(),
                  'message'
              ],
              $this->debugInfo(),
         )
    }

    private function debugInfo(\Throwable $exception): array
    {
        if (!$this->shouldIncludeDebugInfo) return [];

        return [
            'trace' => $e->getTrace(),
        ];
    }
}

Профит:


  • разделение ответственности. Мы не помещаем логику форматирования данных в данные.
  • Универсально. Мы можем сделать кучу разных форматтеров и выбирать их исходя из настроек или вообще через content negotiation.

Это мультиисключение:
механизм валидации, предусмотренный в стандартных объектах «Runn Me!».

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


Сравним запись:


try {
    $validator->validate($data);
} catch (Exceptions $validationErrors)
{
    $this->handleValidationErrors($validationErrors);
}

// vs

$errors = $validator->validate($data, $rules);

if ($errors->count() !== 0)
    $this->handleValidationErrors();

профит:


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

{
    "status_code": 422,
    "message": "Please check your data",
    "violations": [
         {"path": "/foo/bar", "message": "Value should not be blank"},
         {"path": "/foo/email", "message": "Invalid email address"}
    ]
}

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


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

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


Определили метод, он получает на вход то значение, которое вы намереваетесь присвоить свойству

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


перейдем к теме комплексных (очень хочется произнести это слово с ударением на «е»: «комплЕксных») объектов.

Вы выкинули в мусорку инкапсуляцию, о каких объектах вы говорите? Это структуры данных, property bags. Тупые и бесполезные.


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

Поздравляю. Вы переизобрели Design by Contract/AOP. Можно просто повесить на класс описание инвариантов и трекать все при помощи какого-нибудь goaop. Выйдет тоже что и у вас но за счет прокси объектов а не наследования.

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

В любом случае внимательно перечитаю ваш текст еще раз. Спасибо.
вы не задали ни одного вопроса

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


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

Так и дискуссии-то нет, уважаемый коллега. О чем мы дискутируем?

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

Что мне тут ответить в порядке дискуссии? Я во всех своих примерах в статье явно выкидываю коллекцию ошибок. Настолько явно, что «явнее» быть не может.

Вы считаете, что валидатор должен возвращать или «успех» или коллекцию ошибок именно оператором return. Для меня это странная идея, поэтому я разделяю возврат информации об успешности/неуспешности валидации (return boolean) и выброс коллекции ошибок валидации (throw Exceptions). Для меня это нормальная практика, проверенная годами и тоннами промышленного кода. Но не повод для дискуссии. Если вы считаете, что так делать плохо — ОК, я уважаю ваше мнение, но не вижу причин для спора и тем более не вижу возможности вам что-то доказать.

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

И так далее.
Я сколько слежу за Вашими ответами от поста к посту и Вы постоянно апеллируете к каким-то аргументам наподобие: «проверено временем», какие-то «тонны промышленных проектов» и т.п. При этом, судя по всему, даже не понимаете, что когда вам аргументируют, почему то, что вы делаете — плохо с точки зрения дизайна, люди руководствуются т.н «лучшими практиками», которые проверены не одним человеком(Вами), а огромным сообществом. И масштаб промышленных проектов, использующих те практики, о которых говорит уважаемый Fesor и их качество значительно выше даже самых смелых Ваших фантазий. Люди, наподобие Эрика Эванса, например, положили годы на анализ и формализацию накопленного СООБЩЕСТВОМ опыта и его формализацию. Вы же просто игнорируете все, что вам говорят и считаете себя самым умным, при этом, совершенно не гнушаясь, откровенным и беспочвенным обсиранием этих самых практик(хороший пример — ваша статья про валидацию. Опытным программистам, при взгляде на ваши подходы, проблемы, которые могут возникнуть при использовании вашего кода очевидны, а вот новички действительно могут повестись на все это и начать отстреливать себе ноги.

А нам потом работай с Вашими продуктами и, не дай Бог, еще и исправляй.
Вы же просто игнорируете все, что вам говорят

Неправда. Я благодарен за каждый комментарий.

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

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

и вот этим
а вот новички действительно могут повестись на все это и начать отстреливать себе ноги.


хотя он и не содержит ничего практически интересного — одни эмоции


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

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

посмотрим на интерфейс вашего валидатора:


    /**
     * @param mixed $value
     * @return bool
     */
    abstract public function validate($value): bool;

где тут "явно"? Ответьте пожалуйста. Ни намека что этот метод возвращает хоть что-то кроме bool. Есть такая штука — правило наименьшего удивления при проектировании интерфейсов. Ваш компонент — это ваш продукт. Им должно быть удобно пользоваться. И под удобно это значит что все должно происходить явно, без необходимости на каждый чих читать доку.


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

нет, валидатор должен возвращать ТОЛЬКО коллекцию ошибок:


public function validate($data, $rules, $groups): ConstraintViolationsList;

тут все явно. А отсутствие ошибок будет говорить нам что данные валидны. Я вам уже писал подробно об этом в комментариях к предыдущему вашему посту.


я разделяю возврат информации об успешности/неуспешности валидации (return boolean) и выброс коллекции ошибок валидации (throw Exceptions).

вот только в вашем варианте результатом работы функции всеравно будет либо true либо исключение. Никто и никогда не увидит false. Более того, за счет того что выбрасывается исключение даже в этом самом true смысла никакого нет. Нам не нужны условия так как мы вместо них используем обработку исключений. А стало быть мы можем выкинуть bool и заменить его на простой список ошибок, без выброса исключений. А если вам лень сделать проверку — не вопрос, делаем простенький адаптер с методом validateAndFailOnErrors. Все явно, можно менять поведение под нужны задачи… И избавляемся от кастыля под названием Exceptions.


Прокомментируйте.


Для меня это нормальная практика, проверенная годами и тоннами промышленного кода.

для того что бы фразы "проверенная годами" и "тоннами промышленного кода" имела хоть какой-то вес, ваш собеседник должен знать:


  • Ваш уровень и опыт работы
  • Квалификацию, ибо 10 лет опыта могут быть как 10 лет опыта так и 1 год опыта повторенный 10 раз.
  • Уровень проектов. Весьма субъективная метрика, можно начать с обсуждения человеко-лет изначальной разработки, перешли ли проекты на поддержку или все так же активно развиваются (ибо качество решений постигается не в период начальной разработки а проверяется потоком изменений в течении жизни проекта). Так же интересует динамика количества багов.

Ровно тоже самое про интерфейс JsonSerializable.

тогда ответьте на мой вопрос.


Что делать если я захочу в debug режиме добавлять помимо кода ошибки и описания еще и стэктрэйс?

Вы против — ОК, я вас услышал, спасибо.

выводов вы для себя не сделали.


p.s. глянул тесты. Откройте для себя провайдеры данных.

UFO just landed and posted this here
UFO just landed and posted this here
Sign up to leave a comment.

Articles