Писал ли собственные сервисы для работы с базой данных?

Ответ

Да, создавал специализированные сервисы для работы с БД в нескольких сценариях:

  1. Сложная бизнес-логика, которую неудобно размещать в репозиториях
  2. Транзакционные операции, затрагивающие несколько сущностей
  3. Оптимизированные запросы, когда ORM генерирует неэффективный SQL
  4. Миграции данных и пакетная обработка

Пример сервиса для работы с финансовыми транзакциями:

// 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+ 🔞

Э, слушай, а я тоже такие сервисы городил, ёпта. Когда понимаешь, что репозиторий — это просто ящик с данными, а вся реальная движуха должна быть где-то ещё.

Вот смотри, когда это реально нужно, а не просто архитектурный мастурбат:

  1. Сложная бизнес-логика, которую в репозиторий пихать — это как в туалете на велосипеде ебаться. Неудобно, неестественно и всем потом за тебя стыдно. Там же должны быть простые find и save, а не тридцать проверок, нотификаций и расчётов.
  2. Транзакционные операции, когда тебе надо тронуть сразу пять сущностей. Если одна из них ляжет — откатить всё к ебеням. В репозитории такое делать — терпения ноль ебать, там же контекста нет.
  3. Оптимизированные запросы, когда ORM рожает такую дичь, что хочется глаза вилкой выколоть. Тридцать джойнов на простую выборку, овердохуища подзапросов. Иногда проще написать один чёткий SQL и не париться.
  4. Миграции или пакетная обработка данных. Когда надо проебаться по миллиону записей и что-то обновить. Через 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'а и проверяешь, какие запросы он выполняет. Красота.

В общем, если задача выходит за рамки «достань-сохрани», то сервис — твой верный пёс. Репозиторий — это просто принеси-подай, а всю умственную работу делают другие.