← Назад к вопросам

Как при поиске вставить статус в запись контроллера?

1.2 Junior🔥 71 комментариев
#Фреймворки

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный и очень практичный вопрос. Он затрагивает ключевые аспекты архитектуры современного PHP-приложения: разделение ответственности (SoC), инъекцию зависимостей (DI) и работу с состоянием.

Прямое изменение статуса внутри метода контроллера — это прямое нарушение принципа единственной ответственности. Контроллер должен лишь принимать HTTP-запрос, координировать действия сервисов и возвращать ответ. Логика изменения статуса, валидации и бизнес-правил принадлежит другим слоям.

Вот как это правильно реализовать, рассмотрев несколько подходов от простого к более продвинутому.

Подход 1: Использование Сервисного Слоя (Рекомендуемый)

Это наиболее чистый и тестируемый подход. Мы выносим всю бизнес-логику в отдельный сервисный класс.

1. Создаем Сервис (Service Class): Он содержит метод для поиска и обновления статуса.

<?php
// App\Service\SearchStatusService.php

namespace App\Service;

use App\Entity\Record;
use App\Repository\RecordRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;

class SearchStatusService
{
    public function __construct(
        private RecordRepository $recordRepository,
        private EntityManagerInterface $entityManager
    ) {}

    public function findAndSetStatus(Request $request, string $searchTerm, string $newStatus): ?Record
    {
        // 1. Выполняем поиск (бизнес-логика поиска может быть и сложнее)
        $record = $this->recordRepository->findOneBy(['title' => $searchTerm]);

        // 2. Если запись найдена - обновляем статус
        if ($record) {
            $record->setStatus($newStatus);
            // Можно добавить логику: кто изменил, по какому запросу
            $record->setLastIp($request->getClientIp());

            // 3. Сохраняем изменения
            $this->entityManager->flush();
        }

        // 4. Возвращаем объект или null
        return $record;
    }
}

2. Используем Сервис в Контроллере: Контроллер становится тонким и понятным.

<?php
// App\Controller\RecordController.php

namespace App\Controller;

use App\Service\SearchStatusService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class RecordController extends AbstractController
{
    #[Route('/search-and-update', name: 'record_search_update', methods: ['GET'])]
    public function searchAndUpdate(
        Request $request,
        SearchStatusService $searchService
    ): Response {
        $searchTerm = $request->query->get('q', '');
        $newStatus = 'viewed'; // Статус может приходить из запроса, конфига и т.д.

        // Вся сложная логика инкапсулирована в сервисе
        $record = $searchService->findAndSetStatus($request, $searchTerm, $newStatus);

        if (!$record) {
            return $this->json(['message' => 'Record not found'], Response::HTTP_NOT_FOUND);
        }

        return $this->json([
            'id' => $record->getId(),
            'title' => $record->getTitle(),
            'status' => $record->getStatus(),
        ]);
    }
}

Преимущества:

  • Тестируемость: Сервис можно легко протестировать в изоляции (unit-тест), подменяя репозиторий и EntityManager.
  • Переиспользование: Логику можно вызвать из другого контроллера, команды CLI или обработчика очереди.
  • Читаемость: Контроллер описывает что происходит, сервис — как.

Подход 2: Событийно-Ориентированная Архитектура (Event-Driven)

Идеально, если изменение статуса — это побочный эффект, который может запускать другие действия (отправка уведомления, логирование, инвалидация кеша).

1. Создаем Событие:

// App\Event\RecordFoundEvent.php
namespace App\Event;

use App\Entity\Record;
use Symfony\Contracts\EventDispatcher\Event;

class RecordFoundEvent extends Event
{
    public const NAME = 'record.found';

    public function __construct(private Record $record) {}

    public function getRecord(): Record
    {
        return $this->record;
    }
}

2. Создаем Подписчика (Subscriber) или Слушателя (Listener):

// App\EventSubscriber\RecordStatusSubscriber.php
namespace App\EventSubscriber;

use App\Event\RecordFoundEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class RecordStatusSubscriber implements EventSubscriberInterface
{
    public function __construct(private EntityManagerInterface $entityManager) {}

    public static function getSubscribedEvents(): array
    {
        return [
            RecordFoundEvent::NAME => 'onRecordFound',
        ];
    }

    public function onRecordFound(RecordFoundEvent $event): void
    {
        $record = $event->getRecord();
        $record->setStatus('processed_by_event');
        // Flush может быть вынесен в отдельный подписчик на post_flush
        $this->entityManager->flush();
    }
}

3. Контроллер теперь только инициирует событие:

// В методе контроллера или сервиса
$record = $this->repository->findOneBy(['title' => $searchTerm]);
if ($record) {
    $this->eventDispatcher->dispatch(new RecordFoundEvent($record), RecordFoundEvent::NAME);
}

Подход 3: Прямой вызов в Контроллере (Антипаттерн, но для простых случаев)

Иногда для очень простых CRUD-операций допустимо, но я настоятельно не рекомендую это для продакшена.

// НЕ РЕКОМЕНДУЕТСЯ ДЛЯ СЛОЖНЫХ ПРИЛОЖЕНИЙ
#[Route('/update-status', methods: ['POST'])]
public function updateStatus(
    Request $request,
    EntityManagerInterface $em,
    RecordRepository $repository
): Response {
    $record = $repository->find($request->request->get('id'));
    if (!$record) {
        throw $this->createNotFoundException();
    }

    // ПРЯМОЕ ИЗМЕНЕНИЕ В КОНТРОЛЛЕРЕ - ПЛОХАЯ ПРАКТИКА
    $record->setStatus($request->request->get('status'));
    $em->flush();

    return $this->redirectToRoute('record_index');
}

Почему это плохо:

  1. Нарушение SoC: Контроллер знает, как менять статус.
  2. Сложность тестирования: Для теста нужен полный HTTP-стек и база данных.
  3. Дублирование кода: Если статус нужно менять из другого места, логику придется копировать.
  4. Сложность расширения: Добавить проверку прав, логирование или другие действия до/после изменения становится очень трудно.

Итог и Рекомендации

  • Всегда используйте Сервисный слой (Подход 1). Это стандарт для современных фреймворков (Laravel, Symfony, Yii). Он дает наилучший баланс между простотой и гибкостью.
  • Рассматривайте Событийную модель (Подход 2) для сложных, слабосвязанных побочных действий, особенно в высоконагруженных или модульных системах.
  • Избегайте прямого изменения в контроллере (Подход 3). Это допустимо только для прототипов или тривиальных админ-панелей. В долгосрочной перспективе такой код становится неуправляемым.

Ключевой принцип: Контроллер — это координатор, а не исполнитель. Он получает запрос, делегирует задачу соответствующему сервису, обрабатывает возможные исключения и форматирует ответ для клиента. Вставка статуса при поиске — это бизнес-логика, которая должна жить в сервисе, сущности или обработчике событий.

Как при поиске вставить статус в запись контроллера? | PrepBro