- Система отдает данные пользователю - например, новости.
- Система получает данные пользователя - скажем, личные данные для магазина.
К сожалению, люди не идеальны, и не всегда указывают нужную информацию. Есть хороший рабочий принцип - не доверять данным от пользователя. Для такого "недоверия" принято использовать валидацию данных.
Итак, начнем. Опишем класс:
class Customer { public $firstName; public $lastName; public $age; public $gender; }
Допустим, что нужно обязательно запомнить lastName, $age должен быть больше 18 и gender нужно указать обязательно, и он может быть либо "male", либо "female". Как это сделать? Попробуем решение в лоб:
class SomeController { public function actionBuy() { $customer = $this->getCustomer(); if( !is_empty($customer->lastName) && !is_empty($customer->age) && $customer->age > 18 && !is_empty($customer->gender) && in_array($customer->gender, array("male", "female")) ) { echo "Correct!"; } else { echo "Invalid!"; } } }
Отстойно! Что будет, если потребуется где-то еще провалидировать обьект? И как получить список ошибок? Сделаем для начала классический рефакторинг - вынесем этот ужасный огромный if() прямо в модель, которую валидируем:
class Customer { ... public function validate() { $violations = array(); if(is_empty($this->lastName)) $violations[] = "Last name required"; if(is_empty($this->age) $violations[] = "Age required"; if(is_empty($this->gender)) $violations[] = "Gender required"; if(!in_array($this->gender, array("male", "female"))) $violations[] = "Invalid gender"; } }
Теперь уже намного лучше. Как минимум, вся валидация скрыта от контроллера, и мы можем получить список ошибок, при желании даже сделать интернационализацию!
class SomeController { public function actionBuy() { $customer = $this->getCustomer(); $errors = $customer->validate(); if(count($errors) == 0) { echo "Correct!"; } else { echo "Invalid! list on errors:"; foreach($errors as $error) echo _($error); } } }
Но проблемы решены не все. Что делать, если логика валидации усложняется? Что делать, если паттерны проверок дублируются? А как же еще не раздувать код метода валидации?
Приходит понимание, что модель начинает содержать слишком много логики, да еще в некотором роде сервисной - мы же описали, какие поля нужны! Это можно назвать контрактом на использование кода. Для решения таких сложностей, нужно поступить очень просто.
Отказаться от валидации данных в модели. Это должен делать валидатор.
Отлично, вот оно решение. Но правильный программист - ленивый программист, следовательно попробуем поискать готовое решение.
1) Yii framework - очень быстрый, удобный фреймфорк со своим валидатором. Он бы нам помог, когда мы выносили валидацию в саму модель - в принципе так и сделано в этом фреймворке, модель хранит конфигурацию для валидации данных, сама запускает валидатор, сама заботится о получении списка ошибок и т.п. Все делает сама. К сожалению, модель является фабрикой, конфигом и конфигуратором для компонента валидации, в добавок еще и использует сама. Это работает, но как выяснили, далеко не во всех ситуациях.
2) Zend framework. Действительно, почему бы и нет? Лично я не выбрал его, так как для него приходится много допиливать в реальной работе - в пакете нет декларативного конфига, в котором можно явно указать, как валидировать тот или иной класс.
3) Symfony 2 validation component - вот то, что пришлось мне по вкусу! Это validation framework, c декларативным конфигом, с большой возможностью расширения существующего функционала, и кроме прочего, основан на спецификации jsr303
Перепишем немного код на использование symfory
class Customer { public $firstName; public $lastName; public $age; public $gender; } class SomeController { public function actionBuy() { $customer = $this->getCustomer(); $errors = $this->getValidator()->validate($customer); if(count($errors) == 0) { echo "Correct!"; } else { echo "Invalid! list on errors:"; foreach($errors as $error) echo _($error->getMessage()); } } }
Для того, чтобы использовать этот компонент, нужен дистрибутив symfony 2, и в bootstrap файле вашего приложения подключить библиотеку и инициализировать ее при помощи конфига. Наш конфиг в формате yaml может выглядеть так:
Customer: properties: lastName: - NotBlank: ~ age: - NotBlank: ~ - Type: { type: "numeric" } - Min: { limit: 18 } gender: - NotBlank: ~ - Choice: { choices: ["male", "female"]}
Что получили: модель предоставляет контракт на использование, но понятия не имеет, как себя использовать, если что не так - кидает ошибку. В архитектуре не заложено, как именно валидировать обьект, точнее - логика домена не содержит код, который ее прямо не касается, такой как валидация. Вся логика, связанная с обсуживанием модели, вынесена за ее пределы и может как угодно видоизменяться и расширяться - например, мы можем описать свой валидатор с хитрозаумными правилами, и использовать его где хочется.
Итог - низкая связанность кода, прозрачное изменение компонентов, масштабирование логики и отсутствие избыточного кода, логику которого перенесли в декларативную часть - в конфиг.