Ответ
Да, создавал специализированные сервисы для работы с БД в нескольких сценариях:
- Сложная бизнес-логика, которую неудобно размещать в репозиториях
- Транзакционные операции, затрагивающие несколько сущностей
- Оптимизированные запросы, когда ORM генерирует неэффективный SQL
- Миграции данных и пакетная обработка
Пример сервиса для работы с финансовыми транзакциями:
// src/Service/TransactionManager.php
class TransactionManager
{
private EntityManagerInterface $em;
private Connection $connection;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->connection = $em->getConnection();
}
/**
* Выполняет денежный перевод между счетами с проверкой баланса
*/
public function transferFunds(int $fromAccountId, int $toAccountId, float $amount): bool
{
$this->em->beginTransaction();
try {
// Блокируем записи для избежания race condition
$fromAccount = $this->connection->executeQuery(
'SELECT * FROM accounts WHERE id = ? FOR UPDATE',
[$fromAccountId]
)->fetchAssociative();
if (!$fromAccount || $fromAccount['balance'] < $amount) {
throw new InsufficientFundsException();
}
// Выполняем перевод одним запросом для атомарности
$this->connection->executeStatement(
'UPDATE accounts SET balance = CASE
WHEN id = ? THEN balance - ?
WHEN id = ? THEN balance + ?
END WHERE id IN (?, ?)',
[$fromAccountId, $amount, $toAccountId, $amount, $fromAccountId, $toAccountId]
);
// Создаем запись о транзакции
$this->createTransactionRecord($fromAccountId, $toAccountId, $amount);
$this->em->commit();
return true;
} catch (Exception $e) {
$this->em->rollback();
throw $e;
}
}
private function createTransactionRecord(int $fromId, int $toId, float $amount): void
{
$stmt = $this->connection->prepare(
'INSERT INTO transactions (from_account_id, to_account_id, amount, created_at)
VALUES (?, ?, ?, NOW())'
);
$stmt->executeStatement([$fromId, $toId, $amount]);
}
}
Преимущества такого подхода:
- Инкапсуляция сложной логики в одном месте
- Контроль транзакций на уровне бизнес-операции
- Производительность за счет нативных SQL-запросов
- Тестируемость — сервис можно мокать в unit-тестах
Ответ 18+ 🔞
Э, слушай, а я тоже такие сервисы городил, ёпта. Когда понимаешь, что репозиторий — это просто ящик с данными, а вся реальная движуха должна быть где-то ещё.
Вот смотри, когда это реально нужно, а не просто архитектурный мастурбат:
- Сложная бизнес-логика, которую в репозиторий пихать — это как в туалете на велосипеде ебаться. Неудобно, неестественно и всем потом за тебя стыдно. Там же должны быть простые
findиsave, а не тридцать проверок, нотификаций и расчётов. - Транзакционные операции, когда тебе надо тронуть сразу пять сущностей. Если одна из них ляжет — откатить всё к ебеням. В репозитории такое делать — терпения ноль ебать, там же контекста нет.
- Оптимизированные запросы, когда ORM рожает такую дичь, что хочется глаза вилкой выколоть. Тридцать джойнов на простую выборку, овердохуища подзапросов. Иногда проще написать один чёткий SQL и не париться.
- Миграции или пакетная обработка данных. Когда надо проебаться по миллиону записей и что-то обновить. Через ORM это будет медленно и печально, а в сервисе можно на чистом Connection'е сделать всё быстро и без соплей.
Вот, например, сервис для денежных переводов. Представь, что это банк, а не очередной пет-проект с котиками.
// src/Service/TransactionManager.php
class TransactionManager
{
private EntityManagerInterface $em;
private Connection $connection;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->connection = $em->getConnection();
}
/**
* Перекидывает бабки со счёта на счёт, да ещё и проверяет, хватит ли их.
*/
public function transferFunds(int $fromAccountId, int $toAccountId, float $amount): bool
{
// Начинаем транзакцию. Всё или ничего.
$this->em->beginTransaction();
try {
// Вот тут важный момент, чувак. Блокируем запись, чтобы два процесса одновременно не спиздили деньги.
// Без этого будет race condition, а это пиздец.
$fromAccount = $this->connection->executeQuery(
'SELECT * FROM accounts WHERE id = ? FOR UPDATE',
[$fromAccountId]
)->fetchAssociative();
// Если счёта нет или денег мало — сразу нахуй, исключение.
if (!$fromAccount || $fromAccount['balance'] < $amount) {
throw new InsufficientFundsException();
}
// Сам перевод. Красиво одним запросом, атомарно.
// Не два апдейта, которые могут разъебаться, а один чёткий CASE.
$this->connection->executeStatement(
'UPDATE accounts SET balance = CASE
WHEN id = ? THEN balance - ?
WHEN id = ? THEN balance + ?
END WHERE id IN (?, ?)',
[$fromAccountId, $amount, $toAccountId, $amount, $fromAccountId, $toAccountId]
);
// И логируем операцию, куда ж без этого.
$this->createTransactionRecord($fromAccountId, $toAccountId, $amount);
// Если дошли сюда без ошибок — коммитим. Всё прошло чики-пуки.
$this->em->commit();
return true;
} catch (Exception $e) {
// Если где-то посередине пиздец случился — откатываем ВСЁ.
// Как будто ничего и не было. Это и есть транзакция, мать её.
$this->em->rollback();
throw $e;
}
}
// Приватный метод для внутреннего употребления. Наружу не торчит.
private function createTransactionRecord(int $fromId, int $toId, float $amount): void
{
$stmt = $this->connection->prepare(
'INSERT INTO transactions (from_account_id, to_account_id, amount, created_at)
VALUES (?, ?, ?, NOW())'
);
$stmt->executeStatement([$fromId, $toId, $amount]);
}
}
И в чём, собственно, профит, спросишь ты? А вот, блядь, в чём:
- Инкапсуляция, ёпта. Вся логика перевода — в одном классе. Не размазана по десяти контроллерам и хелперам. Захотел поменять — идёшь в одно место.
- Контроль над транзакциями. Ты сам решаешь, где начать и где завершить. Всё в твоих руках, а не на откупе у магического
@Transactional. - Производительность. Нативный SQL там, где он нужен. Никаких лишних гидраций объектов в память, когда тебе нужны только цифры.
- Тестируемость. Такой сервис легко замокать в юнит-тестах. Подсунул ему заглушку Connection'а и проверяешь, какие запросы он выполняет. Красота.
В общем, если задача выходит за рамки «достань-сохрани», то сервис — твой верный пёс. Репозиторий — это просто принеси-подай, а всю умственную работу делают другие.