Add Meilisearch event indexing with 3 indexes: global, admin, per-account

- Create EventIndexService with indexEvent() and removeEvent()
- event_global: online events where isSecret=false (public search)
- event_admin: all events regardless of status (admin search)
- event_{accountId}: all events per organizer (account search)
- Integrate indexing in create/edit/delete event controllers
- Try/catch for Meilisearch unavailability (graceful degradation)
- Add 5 unit tests for EventIndexService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-20 17:18:45 +01:00
parent 3cd40f30c0
commit e6213ca66f
3 changed files with 216 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\Payout;
use App\Entity\User;
use App\Service\EventIndexService;
use App\Service\MailerService;
use App\Service\PayoutPdfService;
use App\Service\StripeService;
@@ -290,7 +291,7 @@ class AccountController extends AbstractController
}
#[Route('/mon-compte/evenement/creer', name: 'app_account_create_event', methods: ['GET', 'POST'])]
public function createEvent(Request $request, EntityManagerInterface $em): Response
public function createEvent(Request $request, EntityManagerInterface $em, EventIndexService $eventIndex): Response
{
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
@@ -316,6 +317,8 @@ class AccountController extends AbstractController
$em->persist($event);
$em->flush();
$eventIndex->indexEvent($event);
$this->addFlash('success', 'Evenement cree avec succes.');
return $this->redirectToRoute('app_account', ['tab' => 'events']);
@@ -331,7 +334,7 @@ class AccountController extends AbstractController
}
#[Route('/mon-compte/evenement/{id}/modifier', name: 'app_account_edit_event', methods: ['GET', 'POST'])]
public function editEvent(\App\Entity\Event $event, Request $request, EntityManagerInterface $em): Response
public function editEvent(\App\Entity\Event $event, Request $request, EntityManagerInterface $em, EventIndexService $eventIndex): Response
{
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
@@ -358,6 +361,8 @@ class AccountController extends AbstractController
$em->flush();
$eventIndex->indexEvent($event);
$this->addFlash('success', 'Evenement modifie avec succes.');
return $this->redirectToRoute('app_account', ['tab' => 'events']);
@@ -374,7 +379,7 @@ class AccountController extends AbstractController
}
#[Route('/mon-compte/evenement/{id}/supprimer', name: 'app_account_delete_event', methods: ['POST'])]
public function deleteEvent(\App\Entity\Event $event, EntityManagerInterface $em): Response
public function deleteEvent(\App\Entity\Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
{
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
@@ -384,6 +389,8 @@ class AccountController extends AbstractController
throw $this->createAccessDeniedException();
}
$eventIndex->removeEvent($event);
$em->remove($event);
$em->flush();

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Service;
use App\Entity\Event;
class EventIndexService
{
private const INDEX_GLOBAL = 'event_global';
private const INDEX_ADMIN = 'event_admin';
private const INDEX_PREFIX = 'event_';
public function __construct(
private readonly MeilisearchService $meilisearch,
) {
}
public function indexEvent(Event $event): void
{
try {
$document = $this->toDocument($event);
$accountIndex = self::INDEX_PREFIX.$event->getAccount()->getId();
$this->meilisearch->createIndexIfNotExists(self::INDEX_ADMIN);
$this->meilisearch->addDocuments(self::INDEX_ADMIN, [$document]);
$this->meilisearch->createIndexIfNotExists($accountIndex);
$this->meilisearch->addDocuments($accountIndex, [$document]);
if ($event->isOnline() && !$event->isSecret()) {
$this->meilisearch->createIndexIfNotExists(self::INDEX_GLOBAL);
$this->meilisearch->addDocuments(self::INDEX_GLOBAL, [$document]);
} else {
$this->removeFromGlobal($event);
}
} catch (\Throwable) {
// Meilisearch unavailable, skip indexing
}
}
public function removeEvent(Event $event): void
{
try {
$accountIndex = self::INDEX_PREFIX.$event->getAccount()->getId();
$this->meilisearch->deleteDocument(self::INDEX_ADMIN, $event->getId());
$this->meilisearch->deleteDocument($accountIndex, $event->getId());
$this->removeFromGlobal($event);
} catch (\Throwable) {
// Meilisearch unavailable, skip removal
}
}
private function removeFromGlobal(Event $event): void
{
if ($this->meilisearch->indexExists(self::INDEX_GLOBAL)) {
$this->meilisearch->deleteDocument(self::INDEX_GLOBAL, $event->getId());
}
}
/**
* @return array<string, mixed>
*/
private function toDocument(Event $event): array
{
return [
'id' => $event->getId(),
'title' => $event->getTitle(),
'description' => $event->getDescription(),
'address' => $event->getAddress(),
'zipcode' => $event->getZipcode(),
'city' => $event->getCity(),
'startAt' => $event->getStartAt()?->format('Y-m-d H:i'),
'endAt' => $event->getEndAt()?->format('Y-m-d H:i'),
'isOnline' => $event->isOnline(),
'isSecret' => $event->isSecret(),
'accountId' => $event->getAccount()->getId(),
'accountName' => $event->getAccount()->getCompanyName() ?? $event->getAccount()->getFirstName().' '.$event->getAccount()->getLastName(),
];
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Tests\Service;
use App\Entity\Event;
use App\Entity\User;
use App\Service\EventIndexService;
use App\Service\MeilisearchService;
use PHPUnit\Framework\TestCase;
class EventIndexServiceTest extends TestCase
{
private MeilisearchService $meilisearch;
private EventIndexService $service;
protected function setUp(): void
{
$this->meilisearch = $this->createMock(MeilisearchService::class);
$this->service = new EventIndexService($this->meilisearch);
}
private function createEvent(bool $online = true, bool $secret = false): Event
{
$user = $this->createMock(User::class);
$user->method('getId')->willReturn(42);
$user->method('getCompanyName')->willReturn('Asso Test');
$user->method('getFirstName')->willReturn('Test');
$user->method('getLastName')->willReturn('User');
$event = $this->createMock(Event::class);
$event->method('getId')->willReturn(1);
$event->method('getTitle')->willReturn('Brocante');
$event->method('getDescription')->willReturn('Description');
$event->method('getAddress')->willReturn('12 rue');
$event->method('getZipcode')->willReturn('75001');
$event->method('getCity')->willReturn('Paris');
$event->method('getStartAt')->willReturn(new \DateTimeImmutable('2026-07-01 10:00'));
$event->method('getEndAt')->willReturn(new \DateTimeImmutable('2026-07-01 18:00'));
$event->method('isOnline')->willReturn($online);
$event->method('isSecret')->willReturn($secret);
$event->method('getAccount')->willReturn($user);
return $event;
}
public function testIndexEventOnlineNotSecret(): void
{
$event = $this->createEvent(true, false);
$this->meilisearch->expects(self::exactly(3))
->method('createIndexIfNotExists');
$this->meilisearch->expects(self::exactly(3))
->method('addDocuments');
$this->service->indexEvent($event);
}
public function testIndexEventOfflineRemovesFromGlobal(): void
{
$event = $this->createEvent(false, false);
$this->meilisearch->expects(self::exactly(2))
->method('createIndexIfNotExists');
$this->meilisearch->expects(self::exactly(2))
->method('addDocuments');
$this->meilisearch->expects(self::once())
->method('indexExists')
->with('event_global')
->willReturn(true);
$this->meilisearch->expects(self::once())
->method('deleteDocument')
->with('event_global', 1);
$this->service->indexEvent($event);
}
public function testIndexEventSecretRemovesFromGlobal(): void
{
$event = $this->createEvent(true, true);
$this->meilisearch->expects(self::once())
->method('indexExists')
->with('event_global')
->willReturn(false);
$this->meilisearch->expects(self::never())
->method('deleteDocument');
$this->service->indexEvent($event);
}
public function testRemoveEvent(): void
{
$event = $this->createEvent();
$this->meilisearch->expects(self::exactly(3))
->method('deleteDocument');
$this->meilisearch->expects(self::once())
->method('indexExists')
->with('event_global')
->willReturn(true);
$this->service->removeEvent($event);
}
public function testRemoveEventGlobalIndexNotExists(): void
{
$event = $this->createEvent();
$this->meilisearch->expects(self::once())
->method('indexExists')
->with('event_global')
->willReturn(false);
$this->meilisearch->expects(self::exactly(2))
->method('deleteDocument');
$this->service->removeEvent($event);
}
}