Наибольшую опасности для приложения представляют входные данные. Это ни для кого не секрет и часто большая часть усилий направленно именно на их контроль и преобразование. Такой подход называется “Инвариантом” и предполагает наличие одного допустимого состояния для каждого объекта системы. Рассмотрим пример:
Этот простой класс бизнес-модели описывает пользователей системы, которые могут быть зарегистрированы с помощью метода registerUser
. Зададимся вопросом - кто и где должен проверять входные данные пользователя, такие как его $login
и $password
, а так же какие ограничения накладываются на эти данные?
Предположим что бизнес требования для данных о пользователе системы у нас следующие:
- Логин пользователя должен содержать только латинские буквы в любом регистре, цифры и символ подчеркивания, а так же иметь длину от 1 до 10 байт
- Пароль пользователя должен содержать только латинские буквы в любом регистре, цифры, символ подчеркивания и тире, а так же иметь длину от 5 до 20 байт
Одним из решений для валидации входных данных является включение проверяющей логики в бизнес-модель:
Другим решением является использование стороннего (внешнего) валидатора вида:
Использоваться такой класс может следующим образом:
В большинстве случаев можно ограничиться первым вариантом решения, для предотвращения использования объектов, находящихся в недопустимом состоянии. Я предпочитаю смешивать эти решения, реализуя валидацию во внешнем классе и агреригуя его в валидируемом объекте:
Это позволяет связать проверяемую и проверяющую логику, но при этом избежать “засорения” объектов бизнес-модели, а так же сделать решение более гибким, за счет возможности быстрой замены валидатора.
Аккумуляция ошибок
Предложенные решения достаточно удобны в использовании, но они ничего не сообщают о причинах невалидности модели. Конечно мы могли бы использовать исключения с описательными сообщениями, вместо возврата булевого значения, но это ведет к двум проблемам:
- Для верификации объекта придется использовать конструкцию
try/catch
, что не очень удобно - При невалидности модели мы получим информацию только о первом невалидном свойстве, так как выброс исключения остановит дальнейшую валидацию
Более подходящим решением является аккумуляция ошибок. Реализуется оно достаточно просто и одинаково как для внутреннего, так и для внешнего валидатора:
Пример использования:
Как видно из примера, метод isValid
был заменен на validate
. Это связано с тем, что валидация с аккумуляцией ошибок не просто проверяет валидность данных бизнес-модели, но и определяет, что именно не соответствует бизнес-ограничениям. Валидны ли данные можно определить с помощью подсчета записей, возвращаемых методом getErrors
валидатора.
Контекстуальная валидация
Немного усложним нашу бизнес-модель, добавив следующее свойство классу User
:
Электронный адрес пользователя является необязательным свойством, но он должен быть указан для вызова метода Notificator::notifyFromEmail
(email-рассылка).
Такая валидация называется “Контекстуальной” или “Зависимой от потребителя”. Другими словами логика валидации объекта может изменяться, в зависимости от того, как предполагается им воспользоваться. Часто используется минимум один контекстуальный валидатор, проверяющий состояние объекта перед записью его в базу данных. Иногда он дополняется вторым контекстуальным валидатором, проверяющим состояние объекта перед выводом его пользователю (для защиты от XSS, на пример).
Реализуется такая валидация довольно просто. Достаточно всего лишь предоставить по отдельной логике валидации для каждого из возможных контекстов. При использовании внешнего валидатора это может быть реализовано с помощью нескольких методов, по одному для каждой проверки, либо с использованием множества классов. Мы рассмотрим пример контекстуальной валидации с внутренней логикой:
Как видно, метод validateNotification
предварительно вызывает метод validatePersist
. Это сделано для демонстрации возможности смешивания логики валидации.
Применяется такое решение следующим образом: