Оригинал: Domain Services in PHP
Перевод для канала Мы ж программист
При создании долговременных приложений быстрые решения обходятся дорого. Если вы когда-нибудь внедряли бизнес-логику прямо в контроллеры, раздутые модели или сервисные классы, которые делают все и ничего одновременно, вы точно знаете, что я имею в виду.
Доменные сервисы – мощный ответ этой неразберихе. Они инкапсулируют ключевую бизнес-логику таким образом, чтобы она была понятной, тестируемой и масштабируемой, не запутываясь в инфраструктуре или технических помехах.
Давайте разберем, как создавать надежные, масштабируемые доменные сервисы на PHP и почему это важнее, чем любой трюк с блестящим фреймворком.
1. Что такое доменный сервис в самом деле?
Доменный сервис – это класс без состояния, содержащий логику, которая не относится целиком к одной сущности. Он функционирует на уровне бизнес-домена, не на инфраструктурном уровне.
Например, расчет скидки пользователя на основе условий лояльности не может логически относиться только к модели Customer
или модели Order
. Это случай, когда отлично подходит сервис типа DiscountCalculatorService
.
2. Почему бы просто не положить логику в модель или контроллер?
Потому что это хаос хард-кодинга.
Толстые модели и божественные (god) контроллеры работают… пока не перестанут. Они образуются невидимое связывание. Контроллер, тесно привязанный к вашей ORM, фреймворку или схеме базы данных, трудно тестировать и улучшать.
Доменные сервисы отделяют основную бизнес-логику от технического мусора.
3. Отсутствие состояния – это сила
Доменный сервис не должен иметь состояние (stateless) – он не хранит что-то в памяти между вызовами.
Сервисы без состояния:
- Проще тестировать
- Лучше переиспользуются
- Проще масштабировать горизонтально
Вы можете внедрять и вызывать их везде, не беспокоясь об их внутреннем состоянии.
4. Принципы, позволяющие работать доменным сервисам
Хороший доменный сервис должен следовать таким принципам:
- Принцип единственной ответственности (Single Responsibility Principle, SRP): Одна работа, но хорошо сделанная.
- Принцип инверсии зависимостей (Dependency Inversion Principle, DIP): Зависимость от абстракций.
- Говори, не спрашивай (Tell, Don’t Ask): Не просто запрашивайте данные, а говорите объектам, что делать.
Эти принципы уменьшают запутанность и повышают удобство обслуживания.
5. Продвинутый пример: PaymentProcessingService
Вот реалистичный сервис, который инкапсулирует бизнес-логику, в то же время не реализуя инфраструктуру.
final class PaymentProcessingService
{
public function __construct(
private PaymentRepositoryInterface $paymentRepository,
private EventDispatcherInterface $eventDispatcher
) {}
public function process(Payment $payment): void
{
if ($payment->isAlreadyProcessed()) {
throw new DomainException("Payment already processed.");
}
$payment->markAsProcessed();
$this->paymentRepository->save($payment);
foreach ($payment->pullDomainEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
}
}
Замечание:
✅ Чистая доменная логика
✅ Никаких запросов в базу данных или HTTP-логики
✅ Интерфейсы абстрагируют взаимодействие с инфраструктурой
6. Не пускайте внутрь инфраструктуру
Позвольте репозиториям управлять хранением, а слушателям событий управлять уведомлениями.
Ваш доменный сервис не должен зависеть от фреймворка. Никакого Laravel, никакой Symfony, никаких ORM – только ваши бизнес-правила.
7. Используйте интерфейсы для абстрагирования зависимостей
Если у вас есть зависимость от чего чего-то типа конвертера валют, делайте зависимость через интерфейс:
interface CurrencyConverter
{
public function convert(Money $amount, string $toCurrency): Money;
}
Такой подход упрощает тестирование и продвигает модульную plug-and-play архитектуру.
8. Доменные сервисы или сервисы приложения
- Доменный сервис: Инкапсулирует правила бизнес-домена.
- Сервис приложения: Оркестрирует рабочие потоки (вызывает доменные сервисы, репозитории и т.д.).
Не смешивайте их. Пусть доменные сервисы управляют тем, что должно произойти, а сервисы приложения – когда и в каком порядке.
9. Тестирование восхитительно просто
Благодаря отсутствию состояния и отделению от инфраструктуры доменные сервисы можно тестировать простым PHPUnit, как тут:
public function test_process_payment_marks_as_processed(): void
{
$payment = Payment::unprocessed();
$service = new PaymentProcessingService(...); // use test doubles
$service->process($payment);
$this->assertTrue($payment->isProcessed());
}
Никаких баз данных. Никаких моков, если вы сами этого не захотите.
10. Инкапсуляция укрепляет доверие
Инкапсулированная логика означает:
- Вы знаете, где ее найти
- Вы знаете, на что она влияет
- Вы можете изменить ее, не сломав все остальное
Ясность улучшает вашу команду и кодовую базу.
11. DDD-подход не обязателен
Чтобы использовать доменные сервисы, вам не нужно целиком внедрять Domain-Driven Design.
Начните с выделения некоторых бизнес-правил из контроллеров и моделей. Делайте постепенный рефакторинг. Пусть ваша архитектура эволюционирует с ростом вашего приложения.
12. Ядро, не зависящее от фреймворка
Фреймворки меняются. Ваши бизнес-правила – не должны.
Доменные сервисы помогут вам мигрировать между Laravel, Symfony или даже не-PHP стеком с меньшей болью. Поддерживайте логику ядра на чистом PHP.
13. Скажите “нет” ненужным функциям
Понятные границы упрощают отказ от внедрения случайной логики, которая компрометирует кодовую базу.
Доменные сервисы действют как привратники, защищая вашу логику от хаоса.
14. Приведите код в соответствие с требованиями бизнеса
Ваш код не должен отражать, как работает бизнес, так же, как и как работает фреймворк.
Доменные сервисы моделируют реальность. Они говорят на языке вашей продуктовой команды. Это улучшает коммуникацию и повышает ясность.
15. Начните с малого. Улучшайте часто. Мыслите на долгую перспективу.
Вам не нужно переделать все прямо завтра.
Вынесите небольшие решения в доменные сервисы. Пишите чистую, тестируему логику. Переиспользуйте ее. Пусть ваша архитектура развивается целенаправленно.