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:
@@ -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();
|
||||
|
||||
|
||||
81
src/Service/EventIndexService.php
Normal file
81
src/Service/EventIndexService.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
125
tests/Service/EventIndexServiceTest.php
Normal file
125
tests/Service/EventIndexServiceTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user