Как при поиске вставить статус в запись контроллера?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный и очень практичный вопрос. Он затрагивает ключевые аспекты архитектуры современного 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');
}
Почему это плохо:
- Нарушение SoC: Контроллер знает, как менять статус.
- Сложность тестирования: Для теста нужен полный HTTP-стек и база данных.
- Дублирование кода: Если статус нужно менять из другого места, логику придется копировать.
- Сложность расширения: Добавить проверку прав, логирование или другие действия до/после изменения становится очень трудно.
Итог и Рекомендации
- Всегда используйте Сервисный слой (Подход 1). Это стандарт для современных фреймворков (Laravel, Symfony, Yii). Он дает наилучший баланс между простотой и гибкостью.
- Рассматривайте Событийную модель (Подход 2) для сложных, слабосвязанных побочных действий, особенно в высоконагруженных или модульных системах.
- Избегайте прямого изменения в контроллере (Подход 3). Это допустимо только для прототипов или тривиальных админ-панелей. В долгосрочной перспективе такой код становится неуправляемым.
Ключевой принцип: Контроллер — это координатор, а не исполнитель. Он получает запрос, делегирует задачу соответствующему сервису, обрабатывает возможные исключения и форматирует ответ для клиента. Вставка статуса при поиске — это бизнес-логика, которая должна жить в сервисе, сущности или обработчике событий.