[Разработка веб-сайтов, PHP, Symfony, Yii, Laravel] Валидация в приложении на PHP (часть 1 — валидация доменного слоя)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Как по мне достаточно важный, хотя и холиварный вопрос. Эта статья аккумулирует в себе те практики, которые мне близки и которых я придерживаюсь в разработке. Статья рассчитана не на новичков, потому нормально, если по ходу чтения какие-то понятия будут вам неизвестны, я постарался коротко раскрыть их здесь, а также указал ссылки на посты в моём телеграм канале Beer::PHP , которые могут чуть подробнее раскрыть то или иное понятие.В следующей части мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики. Валидация EntityРано или поздно, пользовательские данные переданные в наше приложение попадают во внутрь Entity.
Entity — это объекты, которые хранят состояние вашего приложения.
Но не "просто хранят". Сущность всегда должна защищать свои доменные инварианты и следить за тем, чтобы она находилась в согласованном состоянии.
Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.
Entity не должна существовать в вашем приложении если внутри неполные или невалидные данные.Как этого добиться?Проверка данных в конструктореКонструктор должен принимать все параметры, которые обязательны для существования сущности, а также валидировать их перед тем, как присвоить значение свойству. Все необязательные параметры могут быть заданы значениями по-умолчанию или быть присвоенными отдельными методами, в которых также следует добавлять проверки перед присваиванием.В конструкторе необходимо проверять, что данные адекватны, например, что значения находятся в допустимом диапазоне, все значения присутствуют и т.д. В случае если что-то не так - вы должны выбрасывать исключения.
class Order
{
private Product $product;
private int $quantity;
public function __construct(Product $product, int $quantity)
{
if ($quantity <= 0) {
throw new MinQuantityException();
}
}
}
Но ведь мы же не будем показывать пользователям исключения?Всё правильно, исключения не для пользователей. 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 <= 0) {
// throw exception
}
if ($currency !== $this->currency) {
//thow exception
}
$this->amount += $amount;
}
}
Мы можем отдельно вынести AccountNumber, переместив в него всю валидацию.Код 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 который также может взять на себя операцию сложения для логики пополнения счета. Код 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 <= 0) {
// throw exception
}
$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. Подписывайтесь, чтобы получать информацию первыми.
===========
Источник:
habr.com
===========
Похожие новости:
- [PHP, Программирование] PHP 8: встроенное наблюдение (перевод)
- [Разработка веб-сайтов, JavaScript, VueJS] Nuxt.js и поисковики. Решение проблем с СЕО для Google и Yandex
- [Разработка веб-сайтов, Java, API, Google API, Софт] gRPC сервер с нуля
- [Разработка веб-сайтов, ReactJS] ReactJs и бизнес логика в Актерах
- [PHP, Анализ и проектирование систем, SQL] Идеальный каталог, базовая библиотека
- [Разработка веб-сайтов, Тестирование IT-систем, Тестирование веб-сервисов, Тестирование мобильных приложений, Agile] Может ли автоматизированное тестирование заменить ручное и другие «дурацкие» вопросы
- [Информационная безопасность, Разработка веб-сайтов, API, Тестирование веб-сервисов] Nemesida WAF 2021: защита сайтов и API от хакерских атак
- [Laravel] Проверка тестов PHP API на соответствие определениям OpenAPI — пример Laravel
- [Разработка веб-сайтов, Usability, Управление сообществом, Дизайн, Научно-популярное] Неуважительный дизайн (перевод)
- [Высокая производительность, Разработка веб-сайтов, JavaScript, Программирование, Клиентская оптимизация] Оптимизации в вебе — дорого, сложно, и уже не нужно?
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_php, #_symfony, #_yii, #_laravel, #_php, #_validation, #_validatsija (валидация), #_entity, #_domain, #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_php, #_symfony, #_yii, #_laravel
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 03:47
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Как по мне достаточно важный, хотя и холиварный вопрос. Эта статья аккумулирует в себе те практики, которые мне близки и которых я придерживаюсь в разработке. Статья рассчитана не на новичков, потому нормально, если по ходу чтения какие-то понятия будут вам неизвестны, я постарался коротко раскрыть их здесь, а также указал ссылки на посты в моём телеграм канале Beer::PHP , которые могут чуть подробнее раскрыть то или иное понятие.В следующей части мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики. Валидация EntityРано или поздно, пользовательские данные переданные в наше приложение попадают во внутрь Entity. Entity — это объекты, которые хранят состояние вашего приложения.
Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.
class Order
{ private Product $product; private int $quantity; public function __construct(Product $product, int $quantity) { if ($quantity <= 0) { throw new MinQuantityException(); } } } $order->getStatus();
// isn't delivered $order->setCancel(); 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; } } 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 <= 0) { // throw exception } if ($currency !== $this->currency) { //thow exception } $this->amount += $amount; } } 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; } } 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 <= 0) { // throw exception } $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); } } 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); } } Следует создавать и использовать объекты, которые имеют значение в рамках вашего бизнеса.
=========== Источник: habr.com =========== Похожие новости:
Разработка веб-сайтов ), #_php, #_symfony, #_yii, #_laravel |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 03:47
Часовой пояс: UTC + 5