PHP-приложение для расчёта и применения скидок в корзине покупок. Построено на принципах Clean Architecture и Domain-Driven Design.
- Расчёт скидок на основе различных стратегий (процентные, фиксированные)
- Поддержка разделяемых и неразделяемых скидок
- Ограничение количества скидок
- Ограничение процента скидки от стоимости товара
- Приоритизация скидок по размеру
- PHP 8.5 с Composer
- Symfony Console — CLI-команды
- PHP-DI — внедрение зависимостей
- PHPUnit 11.x — тестирование
- PHPStan — статический анализ (уровень 8)
- PHP 8.1+
- Docker (опционально)
make initcomposer install# Через Docker
make run
# Локально
php bin/cli app:example# Через Docker
make test
# Локально
composer run test# Через Docker
make stan
# Локально
composer run stancomposer run checksrc/
├── Application/UseCase/ # Сценарии использования
├── Domain/
│ ├── CartProcessor/ # Обработчики корзины (Chain of Responsibility)
│ ├── Collection/ # Коллекции
│ ├── DiscountStrategy/ # Стратегии скидок (Strategy)
│ ├── Entity/ # Сущности
│ ├── Factory/ # Фабрики
│ ├── Service/ # Доменные сервисы
│ └── ValueObject/ # Value Objects
├── Presentation/Cli/ # CLI-команды
├── Presentation/Tool/ # Форматтеры
└── Shared/Collection/ # Базовые классы
- 121 юнит-тест — покрытие доменной логики
- 2 интеграционных теста — тестирование CLI-команды
Стратегии скидок находятся в файле src/Domain/Factory/DiscountStrategiesFactory.php.
Обработчики корзины находятся в файле src/Domain/Factory/CartProcessorFactory.php.
{
"items": [
{
"id": 1,
"name": "Платиновая ручка",
"price": 1000,
"oldPrice": null,
"image": null
},
...
],
"discounts": [
{
"itemId": 1,
"name": "Фиксированная скидка общая",
"type": "fixed",
"separation": "separable",
"price": 10
},
...
],
"itemsTotalPrice": 1900,
"discountsTotalPrice": 230,
"totalPrice": 1670
}<?php
declare(strict_types=1);
use App\Domain\Entity\Cart;
use App\Domain\Entity\CartItem;
use App\Domain\Collection\CartItems;
use App\Domain\Service\DiscountFiller;
use App\Domain\Service\DiscountCalculator;
// Создаём товары
$items = new CartItems(
new CartItem(id: 1, name: 'Ноутбук', price: 50000),
new CartItem(id: 2, name: 'Мышь', price: 1500),
new CartItem(id: 3, name: 'Клавиатура', price: 3000),
);
// Создаём корзину
$cart = new Cart(id: 1);
$cart->setItems($items);
// Применяем скидки
$filler = new DiscountFiller($strategies);
$cart = $filler->fill($cart);
// Рассчитываем итоговые скидки
$calculator = new DiscountCalculator($separableProcessor, $nonSeparableProcessor);
$cart = $calculator->calculate($cart);
// Получаем результат
$totalPrice = $cart->getTotalPrice();
$discountsTotal = $cart->getDiscounts()->getTotalPrice();<?php
use App\Domain\DiscountStrategy\AbstractDiscountStrategy;
use App\Domain\ValueObject\DiscountType;
use App\Domain\ValueObject\DiscountSeparation;
class VIPDiscountStrategy extends AbstractDiscountStrategy
{
public function __construct()
{
parent::__construct(
name: 'VIP-скидка 15%',
size: 15,
separation: DiscountSeparation::SEPARABLE,
itemId: 1, // только для первого товара
);
}
public function getType(): DiscountType
{
return DiscountType::PERCENT;
}
protected function calculateDiscount(int $price): int
{
return (int)($price * $this->getSize() / 100);
}
}<?php
use App\Domain\Service\DiscountCalculator;
use App\Domain\Service\DiscountFiller;
use App\Presentation\Tool\CartFormatter;
$container = require __DIR__ . '/config/di.php';
$filler = $container->get(DiscountFiller::class);
$calculator = $container->get(DiscountCalculator::class);
$formatter = $container->get(CartFormatter::class);
$cart = new Cart(id: 1);
$cart->setItems(new CartItems(
new CartItem(id: 1, name: 'Товар', price: 1000),
));
$cart = $filler->fill($cart);
$cart = $calculator->calculate($cart);
echo $formatter->formatAsJson($cart);| Тип | Описание |
|---|---|
PERCENT |
Процентная скидка от цены товара |
FIXED |
Фиксированная скидка в рублях |
| Тип | Описание |
|---|---|
SEPARABLE |
Скидка может применяться совместно с другими |
NONSEPARABLE |
Скидка применяется отдельно (исключает другие) |
Система использует паттерн Chain of Responsibility для обработки скидок:
RearrangeCartProcessor— сортировка скидок по размеруMaxPercentCartProcessor— ограничение процента скидкиMaxCountCartProcessor— ограничение количества скидок
MIT