- Валидация в приложении на PHP (часть 1 — валидация доменного слоя)
- Валидация Entity
- Проверка данных в конструкторе
- Проверка данных в методе
- Используйте Value Objects для проверки отдельных значений
- Whole value concept (Quantity pattern)
- Не нужно создавать для Entity сервисы валидации
- Связь с другой сущностью
- Заключение
- Проверка валидации на php
Валидация в приложении на PHP (часть 1 — валидация доменного слоя)
Статья рассчитана не на новичков, потому нормально, если по ходу чтения какие-то понятия будут вам неизвестны, я постарался коротко раскрыть их здесь, а также указал ссылки на посты в моём телеграм канале Beer::PHP , которые могут чуть подробнее раскрыть то или иное понятие.
В следующей части мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики.
Валидация Entity
Рано или поздно, пользовательские данные переданные в наше приложение попадают внутрь Entity.
Entity — это объекты, которые хранят состояние вашего приложения.
Но не «просто хранят». Сущность всегда должна защищать свои доменные инварианты и следить за тем, чтобы она находилась в согласованном состоянии.
Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.
Entity не должна существовать в вашем приложении если внутри неполные или невалидные данные.
Проверка данных в конструкторе
Конструктор должен принимать все параметры, которые обязательны для существования сущности, а также валидировать их перед тем, как присвоить значение свойству. Все необязательные параметры могут быть заданы значениями по-умолчанию или быть присвоенными отдельными методами, в которых также следует добавлять проверки перед присваиванием.
В конструкторе необходимо проверять, что данные адекватны, например, что значения находятся в допустимом диапазоне, все значения присутствуют и т.д. В случае если что-то не так — вы должны выбрасывать исключения.
Но ведь мы же не будем показывать пользователям исключения?
Всё правильно, исключения не для пользователей. Exceptions, трассировка и контекст должны быть видны только разработчикам. Все исключения выброшенные разработчиком должны быть обработаны перед тем как вывести пользователю что-то на экран.
Проверка данных в методе
Когда обновление определенного поля фактически представляет действие, выполняемое над объектом, определите для него метод в самой сущности. Задача такого метода также заключается в проверке предоставленных ему данных, он должен убедиться, что можно обновить данные, учитывая текущее состояние объекта.
Например вы работаете с заказами. Заказ товара может быть отменен, если он не доставлен. Вместо того чтобы где-то вне сущности делать:
$order->getStatus(); // isn't delivered $order->setCancel();
Определите метод cancel(), который будет выполнять проверки внутри сущности и если всё согласовано — менять её состояние.
class Order < // . public function cancel(): void < if ($this->status === STATUS::DELIVERED) < throw new LogicException( sprintf( 'Order %s has already been delivered', $this->id->asString() ) ); > $this->status = STATUS::CANCEL; > >
Используйте Value Objects для проверки отдельных значений
Данный подход позволяет и делегировать проверки, и переиспользовать их в дальнейшем в других частях нашего приложения. Для примера возьмем класс Account, который уже валидирует свои данные в конструкторе и в одном из методов:
class Account < private string $accountNumber; private float $amount = 0.00; private string $currency = 'USD'; const NUMBER_OF_CHARACTERS = 16; public function __construct(string $accountNumber) < if (strlen($accountNumber) !== self::NUMBER_OF_CHARACTERS) < // throw exception >$this->accountNumber = $accountNumber; > public function putMoney(float $amount, string $currency) < if ($amount if ($currency !== $this->currency) < //thow exception >$this->amount += $amount; > >
Мы можем отдельно вынести AccountNumber, переместив в него всю валидацию.
class AccountNumer < const NUMBER_OF_CHARACTERS = 16; private string $accountNumber; public function __construct(string $accountNumber) < if (strlen($accountNumber) !== self::NUMBER_OF_CHARACTERS) < // throw exception >// another rules $this->accountNumber = $accountNumber; > public function __toString(): string < return $this->accountNumber; > >
Отдельно выделить Value Object Money который также может взять на себя операцию сложения для логики пополнения счета.
class Currency extends SplEnum // Пока не 8.1 < const __default = self::USD; const USD = 'USD'; const EUR = 'EUR'; // etc. >class Money < private float $amount; private string $currency; public function __construct(float $amount, Currency $currency) < if ($amount $this->amount = $amount; $this->currency = $currency; > public function currency(): string < return $this->currency; > public function amount(): float < return $this->amount; > public function append(Money $money) < if ($money->currency() !== $this->currency) < // throw exception >$amount = $this->amount + $money->amount(); return new self($amount, $this->currency); > >
Тогда наша Entity будет иметь примерно следующий вид:
class Account < private AccountNumer $accountNumber; private Money $money; public function __construct(AccountNumer $accountNumber) < $this->accountNumber = $accountNumber; $this->money = new Money(0.00, 'USD'); > public function putMoney(Money $money) < $this->money = $this->money->append($money); > >
Так как в основной сущности мы уже работаем с валидными Value Objects, то нет необходимости проверять что-то дополнительно внутри сущности, мы и так всё затайпхинтили.
Whole value concept (Quantity pattern)
Я часто вижу, что этому концепту уделяют мало внимания при проектировании Value Objects, потому решил отдельно на нём остановиться.
Следует создавать и использовать объекты, которые имеют значение в рамках вашего бизнеса.
Идея простая. Представим, что у нас есть геопозиция. Чтобы понять где именно находится точка нам нужна и широта и долгота. Поскольку сами по себе «широта» или «долгота» не имеют смысла друг без друга, значит они должны находиться в одном месте, внутри одного объекта. Другими словами не нужно создавать отдельные VO, если сами по себе они ничего не значат, а только являются составляющей другого объекта.
Наш пример (Money). У нас есть сумма денег, которую нам нужно сложить с другой суммой. Чтобы принять решение можем ли мы сложить две amount, мы должны проверить currency. Поскольку currency напрямую влияет на логику вычислений, то оно должно находиться там-же, где и amount.
Это может быть что угодно, такие штуки как валюта, координаты, календарный период, номер телефона, расстояние, вес и т.д.
Eсли у нас есть данные которые влияют на логику — они должны быть частью состояния объекта где эта логика реализована. Да-да, вычисления (логика) также должны находиться внутри (например сложение/вычитание денег или вычисление расстояния в случае с гео).
Если же в объекте хранятся данные которые на логику реализованную в этом объекте никак не влияют — было бы неплохо эти данные оттуда вынести чтобы не мешали.
Это не значит, что нужно совсем перестать оборачивать в VO примитивные типы (строки, числа и т.д.). Это значит, что при проектировании стоит задумываться о целесообразности того или иного объекта в вашей предметной области.
Не нужно создавать для Entity сервисы валидации
В доменном слое это усложнит вам жизнь. Вам придется делать бесконечные и ненужные геттеры внутри Entity (ведь для валидатора данные нужно будет как-то извлечь), следить за тем что нужно обновить сервис в случае изменения самой сущности и не забывать его вызвать каждый раз при её создании.
Связь с другой сущностью
Отношения лучше выстраивать с помощью идентификаторов, а не по ссылкам на объект. Таким образом мы понижаем связанность (Low Coupling), а также убираем возможность нежелательных изменений, которые могут происходить внутри связанной сущности.
Если в качестве связи с другой сущностью в метод или в конструктор мы передаём ID, то мы наверняка не можем быть уверены, что Entity с таким ID существует в рамках нашей системы, ведь на входе мы можем убедиться лишь в том, что ID соответствует определенному шаблону (например UUID).
Заключение
Правильное проектирование валидации бизнес логики само по себе сильно упростит вам жизнь. Оперируйте в вашем приложении только полными, валидными и консистентными объектами.
В следующей части мы поговорим с вами о пользовательской валидации и подробнее разберём исключения.
Для самых нетерпеливых уже есть короткая заметка в телеграм канале Beer::PHP. Подписывайтесь, чтобы получать информацию первыми.
Как по мне достаточно важный, хотя и холиварный вопрос. Эта статья аккумулирует в себе те практики, которые мне близки и которых я придерживаюсь в разработке.
Проверка валидации на php
Пример #1 Валидация e-mail адреса, используя функцию filter_var()
$email_a = ‘joe@example.com’ ;
$email_b = ‘bogus’ ;
?php
if ( filter_var ( $email_a , FILTER_VALIDATE_EMAIL )) echo «E-mail адрес ‘ $email_a ‘ указан верно.\n» ;
>
if ( filter_var ( $email_b , FILTER_VALIDATE_EMAIL )) echo «E-mail адрес ‘ $email_b ‘ указан верно.\n» ;
> else echo «E-mail адрес ‘ $email_b ‘ указан неверно.\n» ;
>
?>
Результат выполнения данного примера:
E-mail адрес 'joe@example.com' указан верно. E-mail адрес 'bogus' указан неверно.
Пример #2 Валидация IP-адреса, используя функцию filter_var()
if ( filter_var ( $ip_a , FILTER_VALIDATE_IP )) echo «IP-адрес ‘ $ip_a ‘ указан верно.» ;
>
if ( filter_var ( $ip_b , FILTER_VALIDATE_IP )) echo «IP-адрес ‘ $ip_b ‘ указан верно.» ;
>
?>
Результат выполнения данного примера:
Адрес '127.0.0.1' указан верно.
Пример #3 Дополнительные параметры функции filter_var()
$int_a = ‘1’ ;
$int_b = ‘-1’ ;
$int_c = ‘4’ ;
$options = array(
‘options’ => array(
‘min_range’ => 0 ,
‘max_range’ => 3 ,
)
);
if ( filter_var ( $int_a , FILTER_VALIDATE_INT , $options ) !== FALSE ) echo «Число A ‘ $int_a ‘ является верным (от 0 до 3).\n» ;
>
if ( filter_var ( $int_b , FILTER_VALIDATE_INT , $options ) !== FALSE ) echo «Число B ‘ $int_b ‘ является верным (от 0 до 3).\n» ;
>
if ( filter_var ( $int_c , FILTER_VALIDATE_INT , $options ) !== FALSE ) echo «Число C ‘ $int_c ‘ является верным (от 0 до 3).\n» ;
>
?php
$options [ ‘options’ ][ ‘default’ ] = 1 ;
if (( $int_c = filter_var ( $int_c , FILTER_VALIDATE_INT , $options )) !== FALSE ) echo «Число C ‘ $int_c ‘ является верным (от 0 и 3).» ;
>
?>
Результат выполнения данного примера:
Число A '1' является верным (от 0 до 3). Число C '1' является верным (от 0 до 3).