Складской учет в интернет-магазине (на примере Yii2)
-
- Сообщения: 120
- Зарегистрирован: 2014.01.06, 13:46
Складской учет в интернет-магазине (на примере Yii2)
Добрый день! После просмотра доклада Дмитрия Науменко "Рецепты для Yii2", решил отрефакторить интернет-магазин и привести там все в порядок.
Решил начать с простого складского учета. Есть склады, на них приходят поступления товаров и продаются товары. Сильно упрощенная схема ниже.
Хочу сделать красивую группу моделей, поэтому выделил для них отдельный неймспейс \frontend\models\good
Теперь про бизнес-логику: ее надо в отдельные классы выносить. Вот тут я встал. Для фиксирования продажи менеджер использует сразу 2 модели (Sell - для выбора продавца, GoodSellForm - для ввода названия товара, цены и количества [менеджер может продать товар не со склада, тогда автоматически создается товар с указанным названием, дефолтными значениями и списывается со склада]).
Ограничение: если товар, указанный менеджером есть в каталоге, количество товара в продаже не может быть больше чем есть на складе.
У меня единственная мысль как сделать это более-менее правильно: внедрить эту проверку в правила валидации товаров модели GoodSellForm, но насколько это нормально - делать такую глубокую валидацию с привлечением сторонних моделей?
Сейчас этой "валидацией" занимается сам GoodSell в момент проведения продажи (продает все подряд, как только встречает ошибку - откатывает транзакцию). В таком случае не удобно возвращать ошибки наверх, в модель GoodSellForm, для отображения ошибки менеджеру.
Собственно вопрос еще раз, более конкретно: куда писать валидацию товаров в продаже и кто ей должен заниматся?
Решил начать с простого складского учета. Есть склады, на них приходят поступления товаров и продаются товары. Сильно упрощенная схема ниже.
Хочу сделать красивую группу моделей, поэтому выделил для них отдельный неймспейс \frontend\models\good
Теперь про бизнес-логику: ее надо в отдельные классы выносить. Вот тут я встал. Для фиксирования продажи менеджер использует сразу 2 модели (Sell - для выбора продавца, GoodSellForm - для ввода названия товара, цены и количества [менеджер может продать товар не со склада, тогда автоматически создается товар с указанным названием, дефолтными значениями и списывается со склада]).
Ограничение: если товар, указанный менеджером есть в каталоге, количество товара в продаже не может быть больше чем есть на складе.
У меня единственная мысль как сделать это более-менее правильно: внедрить эту проверку в правила валидации товаров модели GoodSellForm, но насколько это нормально - делать такую глубокую валидацию с привлечением сторонних моделей?
Сейчас этой "валидацией" занимается сам GoodSell в момент проведения продажи (продает все подряд, как только встречает ошибку - откатывает транзакцию). В таком случае не удобно возвращать ошибки наверх, в модель GoodSellForm, для отображения ошибки менеджеру.
Собственно вопрос еще раз, более конкретно: куда писать валидацию товаров в продаже и кто ей должен заниматся?
Re: Складской учет в интернет-магазине (на примере Yii2)
Выводите всё в сообщении над формой:
Код: Выделить всё
public function actionSell()
{
$form = Yii::createObject(GoodSellForm::className());
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
try {
$this->sellService->sell($form);
Yii::$app->session->setFlash('success', 'Продано.');
return $this->redirect(['index']);
} catch (\DomainException $e) {
Yii::$app->session->setFlash('error', $e->getMessage());
}
}
return $this->render('sell', [
'form' => $form,
]);
}
Код: Выделить всё
class SellService
{
public function sell(GoodSellForm $form)
{
...
if ($form->amount > $amount = $this->warehouse->getCountForProduct($product->id)) {
throw new \DomainException('Доступно только ' . $amount . ' единиц.');
}
...
}
}
-
- Сообщения: 120
- Зарегистрирован: 2014.01.06, 13:46
Re: Складской учет в интернет-магазине (на примере Yii2)
Не удобно для пользователя если в одной продаже было много товаров. Лучше если ошибка будет возле конкретного товара, по которому не сошлись остатки.ElisDN писал(а):Выводите всё в сообщении над формой:
То есть бизнес-логика в сервисе планируется, но к этому времени нужно быть 100% уверенным что операция не заставит остаток идти в минус
И поясните пожалуйста строку
Код: Выделить всё
$this->sellService->sell($form);
Re: Складской учет в интернет-магазине (на примере Yii2)
А почему бы это не сделать при валидации формы?ElisDN писал(а):Код: Выделить всё
if ($form->load(Yii::$app->request->post()) && $form->validate())
Код: Выделить всё
if ($form->amount > $amount = $this->warehouse->getCountForProduct($product->id)) { throw new \DomainException('Доступно только ' . $amount . ' единиц.'); }
Re: Складской учет в интернет-магазине (на примере Yii2)
undestroyer писал(а):Не удобно для пользователя если в одной продаже было много товаров. Лучше если ошибка будет возле конкретного товара, по которому не сошлись остатки.
Тогда валидируйте ещё и в форме в rules():SiZE писал(а):А почему бы это не сделать при валидации формы?
Код: Выделить всё
['amount', 'required'],
['amount', 'integer'],
['amount', function ($attribute) {
if ($this->$attribute > $amount = $this->warehouse->getCountForProduct($product->id)) {
$this->addError($attribute, 'Доступно только ' . $amount . ' единиц.');
}
}],
Из DI-контейнера в конструктор прилетело:undestroyer писал(а):Каким образом произошло подключение sellService к контроллеру?
Код: Выделить всё
class SellController extends Controller
{
private $sellService;
public function __construct($id, $module, SellService $sellService, $config = []) {
$this->sellService = $sellService;
parent::__construct($id, $module, $config);
}
}
Код: Выделить всё
class GoodSellForm extends Model
{
private $warehause;
public function __construct(Warehause $warehause, $config = []) {
$this->warehause = $warehause;
parent::__construct($config);
}
...
}
-
- Сообщения: 120
- Зарегистрирован: 2014.01.06, 13:46
Re: Складской учет в интернет-магазине (на примере Yii2)
ElisDN писал(а):Из DI-контейнера в конструктор прилетело:undestroyer писал(а):Каким образом произошло подключение sellService к контроллеру?
Код: Выделить всё
class SellController extends Controller { private $sellService; public function __construct($id, $module, SellService $sellService, $config = []) { $this->sellService = $sellService; parent::__construct($id, $module, $config); } }
Код: Выделить всё
class GoodSellForm extends Model { private $warehause; public function __construct(Warehause $warehause, $config = []) { $this->warehause = $warehause; parent::__construct($config); } ... }
Спасибо, DI я как-то упустил из вида.
В GoodSellForm конструктор просит Warehouse. Warehouse - это AR-модель Склада из БД? То есть взаимодействие идет в порядке:
Почему не сделать какой-то класс, назовем его "МенеджерСклада", который предоставляет интерфейс напрямую к товарам на складам? По-сути надо сделать набор методов в стиле:Форма товара в продажу -> Склад -> Товар на складе
- получитьОстатокТовараНаСкладе(Товар,Склад):integer
- изъятьТоварСоСклада(Товар,Склад,Количество):bool
- добавитьТоварНаСклад(Товар,Склад,Количество):bool
Этим я пытаюсь убрать прямую работу с моделью склада, которая по своей сути никакого отношения к операции не имеет. Алгоритм тогда будет таким:Форма товара в продажу -> СервисПродажи -> МенеджерСклада -> Товар на складе
- Контроллер загрузил данные формы и провел ее валидацию
- Контроллер передал набор данных в СервисПродажи
- СервисПродажи создает новую транзакцию
- СервисПродажи создает новую продажу
- СервисПродажи поочередно создает ТоварПродажи (модель с товаром, складом, ценой и количеством)
- СервисПродажи вызывает МенеджерСклада для уменьшения остатка товаров
- СервисПродажи завершает транзакцию, возвращает результат работы
- Контроллер выдает результат пользователю
Re: Складской учет в интернет-магазине (на примере Yii2)
Нет, AR через DI не грузят.undestroyer писал(а):В GoodSellForm конструктор просит Warehouse. Warehouse - это AR-модель Склада из БД?
Да, Warehouse - это и есть "Склад".undestroyer писал(а):Почему не сделать какой-то класс, назовем его "МенеджерСклада", который предоставляет интерфейс напрямую к товарам на складам?
-
- Сообщения: 120
- Зарегистрирован: 2014.01.06, 13:46
Re: Складской учет в интернет-магазине (на примере Yii2)
Спасибо, начинаю пониматьElisDN писал(а):Нет, AR через DI не грузят.undestroyer писал(а):В GoodSellForm конструктор просит Warehouse. Warehouse - это AR-модель Склада из БД?
Да, Warehouse - это и есть "Склад".undestroyer писал(а):Почему не сделать какой-то класс, назовем его "МенеджерСклада", который предоставляет интерфейс напрямую к товарам на складам?
Вот есть у нас "Склад"/"МенеджерСклада", который добавляет/убирает товары со склада. У него есть 3 метода (добавь/убери/скажи сколько есть), которые предоставляют интерфейс для работы с товарами на этом складе. К нему обращаются МенеджерПродажи и МенеджерПоступления
Как сделать обработку ошибок?
Сценарий:
- Контроллер получил данные, загнал в форму, провалидировал - все ок
- МенеджерПродажи получил данные о новой продаже
- МенеджерПродажи вызвал операцию уменьшения остатка МенеджераСклада
- МенеджерСклада не нашел данных о нужном товаре
Как передать данные через слои МенеджерСклада -> МенеджерПродажи -> ФормаПродажи ?
Re: Складской учет в интернет-магазине (на примере Yii2)
Всё как раз и должно в ядре исключения выбрасывать. Если нужно именно специфическую ошибку в конкретное поле поместить, то выделите эту ошибку в отдельный Exception:undestroyer писал(а):МенеджерСклада может и Exception выбросить, но это не красиво, в идеале надо ошибку передать наверх, вплоть до модели.
Код: Выделить всё
class ProductAmountException extends \DomainException
{
private $productId;
public function __construct($productId, $message, Exception $previous = null) {
parent::__construct($message, 0, $previous);
}
public function getProductId() {
return $this->productId;
}
}
Код: Выделить всё
throw new ProductAmountException($productId, 'Доступно только ' . $amount . ' единиц.');
Код: Выделить всё
try {
$this->sellService->sell($form);
Yii::$app->session->setFlash('success', 'Продано.');
return $this->redirect(['index']);
} catch (ProductAmountException $e) {
$form->getProductFormFor($e->getProductId())->addError('amount', $e->getMessage());
} catch (\DomainException $e) {
Yii::$app->session->setFlash('error', $e->getMessage());
}