<?php
namespace App\Controller;
use App\Entity\HelpMission;
use App\Entity\InterestActivity;
use App\Entity\InterestActivityRegistration;
use App\Entity\Notification;
use App\Entity\ThematicGroup;
use App\Entity\ThematicGroupMember;
use App\Entity\User;
use App\Manager\EventNotificationManager;
use App\Repository\HelpMissionRepository;
use App\Repository\InterestActivityRegistrationRepository;
use App\Repository\InterestActivityRepository;
use App\Repository\ThematicGroupRepository;
use App\Service\AgendaReminderService;
use App\Service\ContentLifecycleManager;
use App\Service\ContentConversationManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function index(): Response
{
return $this->render('home/index.html.twig');
}
// #[Route('/', name: 'app_home')]
// public function index(Request $request): Response
// {
// return $this->forward('App\Controller\W1Controller::home');
// // $response = $this->render('home/index.html.twig', [], new Response());
// // $response->setPublic();
// // $response->setMaxAge(60);
// // $response->setSharedMaxAge(300);
// // return $response;
// }
#[Route('/mockup', name: 'app_mockup')]
public function mockup(): Response
{
return $this->render('home/mockup.html.twig');
}
#[Route('/api/home/feed', name: 'app_home_feed', methods: ['GET'])]
public function feed(Request $request, EntityManagerInterface $entityManager): JsonResponse
{
$allowedTypes = ['all', 'activity', 'mission', 'group'];
$selectedType = strtolower(trim((string) $request->query->get('type', 'all')));
if (!in_array($selectedType, $allowedTypes, true)) {
$selectedType = 'all';
}
$selectedLocation = $this->normalizeLocation((string) $request->query->get('location', ''));
$page = max(1, (int) $request->query->get('page', 1));
$perPage = max(6, min(30, (int) $request->query->get('perPage', 12)));
$offset = ($page - 1) * $perPage;
$now = new \DateTimeImmutable();
$archiveThreshold = ContentLifecycleManager::archiveThreshold($now);
// On charge un volume raisonnable par type puis on trie globalement.
$limitPerType = max(40, min(250, ($page * $perPage) + 40));
$posts = [];
/** @var User|null $currentUser */
$currentUser = $this->getUser() instanceof User ? $this->getUser() : null;
$joinedPosts = $this->buildJoinedPostIndex($entityManager, $currentUser);
$activityRows = $entityManager->createQueryBuilder()
->select('a.id AS id, a.title AS title, a.description AS description, a.city AS city, a.createdAt AS createdAt, a.updatedAt AS updatedAt, a.startAt AS startAt, a.endAt AS endAt, a.status AS status, u.id AS userId, u.username AS username, u.image AS userImage')
->from(InterestActivity::class, 'a')
->innerJoin('a.organizer', 'u')
->where('((a.status = :activityStatus AND (a.endAt IS NULL OR a.endAt > :now)) OR (a.status IN (:activityArchivedStatuses) AND COALESCE(a.updatedAt, a.endAt, a.createdAt) > :archiveThreshold))')
->setParameter('activityStatus', InterestActivity::STATUS_OPEN)
->setParameter('activityArchivedStatuses', [InterestActivity::STATUS_CLOSED, InterestActivity::STATUS_CANCELED])
->setParameter('now', $now)
->setParameter('archiveThreshold', $archiveThreshold)
->orderBy('a.createdAt', 'DESC')
->setMaxResults($limitPerType)
->getQuery()
->getArrayResult();
foreach ($activityRows as $row) {
$sourceId = (int) $row['id'];
$isJoined = in_array($sourceId, $joinedPosts['activity'], true);
$archiveBadge = $this->resolveArchiveBadge((string) ($row['status'] ?? ''), $row['endAt'] ?? null, $now);
$posts[] = $this->buildPost(
'activity',
$sourceId,
(string) ($row['username'] ?? 'Utilisateur'),
(string) ($row['title'] ?? ''),
(string) ($row['description'] ?? ''),
$row['createdAt'] ?? null,
'/activities/interet/' . $sourceId,
$this->resolveProfileUrl($row),
$this->resolveAvatarUrl($row),
'%s propose l activite "%s".',
(string) ($row['city'] ?? ''),
!$isJoined && $archiveBadge === null,
'creation',
$isJoined,
$archiveBadge,
$row['startAt'] ?? null,
$row['endAt'] ?? null
);
}
$missionRows = $entityManager->createQueryBuilder()
->select('m.id AS id, m.title AS title, m.description AS description, m.city AS city, m.createdAt AS createdAt, m.updatedAt AS updatedAt, m.endAt AS endAt, m.status AS status, u.id AS userId, u.username AS username, u.image AS userImage')
->from(HelpMission::class, 'm')
->innerJoin('m.creator', 'u')
->where('((m.status IN (:missionStatuses) AND (m.endAt IS NULL OR m.endAt > :missionNow)) OR (m.status = :missionDoneStatus AND COALESCE(m.updatedAt, m.endAt, m.createdAt) > :archiveThreshold))')
->setParameter('missionStatuses', [HelpMission::STATUS_OPEN, HelpMission::STATUS_IN_PROGRESS])
->setParameter('missionDoneStatus', HelpMission::STATUS_DONE)
->setParameter('missionNow', $now)
->setParameter('archiveThreshold', $archiveThreshold)
->orderBy('m.createdAt', 'DESC')
->setMaxResults($limitPerType)
->getQuery()
->getArrayResult();
foreach ($missionRows as $row) {
$sourceId = (int) $row['id'];
$isJoined = in_array($sourceId, $joinedPosts['mission'], true);
$archiveBadge = $this->resolveArchiveBadge((string) ($row['status'] ?? ''), $row['endAt'] ?? null, $now);
$posts[] = $this->buildPost(
'mission',
$sourceId,
(string) ($row['username'] ?? 'Utilisateur'),
(string) ($row['title'] ?? ''),
(string) ($row['description'] ?? ''),
$row['createdAt'] ?? null,
'/missions/entraide/' . $sourceId,
$this->resolveProfileUrl($row),
$this->resolveAvatarUrl($row),
'%s a besoin d aide pour "%s".',
(string) ($row['city'] ?? ''),
!$isJoined && $archiveBadge === null,
'creation',
$isJoined,
$archiveBadge,
null,
$row['endAt'] ?? null
);
}
$groupRows = $entityManager->createQueryBuilder()
->select('g.id AS id, g.title AS title, g.description AS description, g.city AS city, g.createdAt AS createdAt, u.id AS userId, u.username AS username, u.image AS userImage')
->from(ThematicGroup::class, 'g')
->innerJoin('g.owner', 'u')
->orderBy('g.createdAt', 'DESC')
->setMaxResults($limitPerType)
->getQuery()
->getArrayResult();
foreach ($groupRows as $row) {
$sourceId = (int) $row['id'];
$isJoined = in_array($sourceId, $joinedPosts['group'], true);
$posts[] = $this->buildPost(
'group',
$sourceId,
(string) ($row['username'] ?? 'Utilisateur'),
(string) ($row['title'] ?? ''),
(string) ($row['description'] ?? ''),
$row['createdAt'] ?? null,
'/groups/thematic/' . $sourceId,
$this->resolveProfileUrl($row),
$this->resolveAvatarUrl($row),
'%s lance le groupe "%s".',
(string) ($row['city'] ?? ''),
!$isJoined,
'creation',
$isJoined
);
}
$participationRows = $entityManager->createQueryBuilder()
->select('n.id AS id, n.message AS message, n.redirectUrl AS redirectUrl, n.dateCreation AS createdAt, n.content AS content, su.id AS userId, su.username AS username, su.image AS userImage')
->from(Notification::class, 'n')
->innerJoin('n.sourceUser', 'su')
->where('n.type = :type')
->andWhere('n.status != :deleted')
->setParameter('type', 'Feed.Participation')
->setParameter('deleted', Notification::STATUS_DELETED)
->orderBy('n.dateCreation', 'DESC')
->setMaxResults($limitPerType * 3)
->getQuery()
->getArrayResult();
foreach ($participationRows as $row) {
$params = $this->extractNotificationParams((string) ($row['content'] ?? ''));
$postType = (string) ($params['sourceType'] ?? 'feed');
$sourceId = max(0, (int) ($params['sourceId'] ?? 0));
$sourceSnapshot = $this->resolveFeedSourceSnapshot($entityManager, $postType, $sourceId, $now);
if (!$sourceSnapshot['visible']) {
continue;
}
$posts[] = [
'id' => sprintf('participation-%d', (int) ($row['id'] ?? 0)),
'type' => $postType,
'sourceId' => $sourceId,
'kind' => 'participation',
'joinable' => false,
'username' => (string) ($row['username'] ?? 'Utilisateur'),
'title' => (string) ($params['title'] ?? ''),
'description' => (string) ($params['description'] ?? ''),
'message' => (string) ($row['message'] ?? ''),
'link' => (string) ($params['link'] ?? $row['redirectUrl'] ?? '/'),
'profileUrl' => $this->resolveProfileUrl($row),
'avatarUrl' => $this->resolveAvatarUrl($row),
'locationLabel' => $sourceSnapshot['locationLabel'],
'archiveBadge' => $sourceSnapshot['archiveBadge'],
'startAt' => $sourceSnapshot['startAt'],
'endAt' => $sourceSnapshot['endAt'],
'createdAt' => (($row['createdAt'] ?? null) instanceof \DateTimeInterface)
? $row['createdAt']->format(\DateTimeInterface::ATOM)
: (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
];
}
usort(
$posts,
static fn (array $a, array $b): int => strcmp((string) ($b['createdAt'] ?? ''), (string) ($a['createdAt'] ?? ''))
);
$locations = $this->extractLocations($posts);
if ($selectedType !== 'all') {
$posts = array_values(array_filter(
$posts,
static fn (array $post): bool => ($post['type'] ?? '') === $selectedType
));
}
if ($selectedLocation !== '') {
$posts = array_values(array_filter(
$posts,
fn (array $post): bool => $this->normalizeLocation((string) ($post['locationLabel'] ?? '')) === $selectedLocation
));
}
$total = count($posts);
$slice = array_slice($posts, $offset, $perPage);
$hasMore = ($offset + $perPage) < $total;
return $this->json([
'posts' => array_values($slice),
'page' => $page,
'perPage' => $perPage,
'total' => $total,
'hasMore' => $hasMore,
'type' => $selectedType,
'location' => $selectedLocation,
'locations' => $locations,
]);
}
private function buildPost(
string $type,
int $id,
string $username,
string $title,
string $description,
mixed $createdAt,
string $link,
string $profileUrl,
string $avatarUrl,
string $messageTemplate,
string $locationLabel,
bool $joinable,
string $kind,
bool $joined = false,
?array $archiveBadge = null,
mixed $startAt = null,
mixed $endAt = null
): array {
$safeTitle = trim($title) !== '' ? trim($title) : 'contenu';
$created = $createdAt instanceof \DateTimeInterface ? $createdAt : new \DateTimeImmutable();
return [
'id' => sprintf('%s-%d', $type, $id),
'type' => $type,
'sourceId' => $id,
'kind' => $kind,
'joinable' => $joinable,
'joined' => $joined,
'username' => $username,
'title' => $safeTitle,
'description' => $description,
'message' => sprintf($messageTemplate, $username, $safeTitle),
'link' => $link,
'profileUrl' => $profileUrl,
'avatarUrl' => $avatarUrl,
'locationLabel' => trim($locationLabel),
'archiveBadge' => $archiveBadge,
'startAt' => $startAt instanceof \DateTimeInterface ? $startAt->format(\DateTimeInterface::ATOM) : null,
'endAt' => $endAt instanceof \DateTimeInterface ? $endAt->format(\DateTimeInterface::ATOM) : null,
'createdAt' => $created->format(\DateTimeInterface::ATOM),
];
}
/**
* @param array<int, array<string, mixed>> $posts
* @return array<int, string>
*/
private function extractLocations(array $posts): array
{
$labels = [];
foreach ($posts as $post) {
$location = trim((string) ($post['locationLabel'] ?? ''));
if ($location === '') {
continue;
}
$labels[$this->normalizeLocation($location)] = $location;
}
natcasesort($labels);
return array_values($labels);
}
private function normalizeLocation(string $value): string
{
$normalized = trim(mb_strtolower($value));
$normalized = preg_replace('/\s+/', ' ', $normalized) ?? $normalized;
return $normalized;
}
private function resolvePostLocation(EntityManagerInterface $entityManager, string $postType, int $sourceId): string
{
if ($sourceId <= 0) {
return '';
}
return match ($postType) {
'activity' => $entityManager->getRepository(InterestActivity::class)->find($sourceId)?->getCity() ?? '',
'group' => $entityManager->getRepository(ThematicGroup::class)->find($sourceId)?->getCity() ?? '',
'mission' => $entityManager->getRepository(HelpMission::class)->find($sourceId)?->getCity() ?? '',
default => '',
};
}
/**
* @return array{visible:bool, locationLabel:string, archiveBadge:?array{kind:string}, startAt:?string, endAt:?string}
*/
private function resolveFeedSourceSnapshot(
EntityManagerInterface $entityManager,
string $postType,
int $sourceId,
\DateTimeImmutable $now
): array {
if ($sourceId <= 0) {
return [
'visible' => false,
'locationLabel' => '',
'archiveBadge' => null,
'startAt' => null,
'endAt' => null,
];
}
$archiveThreshold = ContentLifecycleManager::archiveThreshold($now);
return match ($postType) {
'activity' => $this->resolveEntityFeedSnapshot(
$entityManager->getRepository(InterestActivity::class)->find($sourceId),
fn (InterestActivity $activity): bool => ($activity->getStatus() === InterestActivity::STATUS_OPEN && ($activity->getEndAt() === null || $activity->getEndAt() > $now))
|| (in_array($activity->getStatus(), [InterestActivity::STATUS_CLOSED, InterestActivity::STATUS_CANCELED], true)
&& ($activity->getUpdatedAt() ?? $activity->getEndAt() ?? $activity->getCreatedAt()) > $archiveThreshold),
fn (InterestActivity $activity): string => $activity->getCity(),
fn (InterestActivity $activity): ?array => $this->resolveArchiveBadge($activity->getStatus(), $activity->getEndAt(), $now),
fn (InterestActivity $activity): ?\DateTimeInterface => $activity->getStartAt(),
fn (InterestActivity $activity): ?\DateTimeInterface => $activity->getEndAt()
),
'mission' => $this->resolveEntityFeedSnapshot(
$entityManager->getRepository(HelpMission::class)->find($sourceId),
fn (HelpMission $mission): bool => (in_array($mission->getStatus(), [HelpMission::STATUS_OPEN, HelpMission::STATUS_IN_PROGRESS], true) && ($mission->getEndAt() === null || $mission->getEndAt() > $now))
|| ($mission->getStatus() === HelpMission::STATUS_DONE
&& ($mission->getUpdatedAt() ?? $mission->getEndAt() ?? $mission->getCreatedAt()) > $archiveThreshold),
fn (HelpMission $mission): string => $mission->getCity(),
fn (HelpMission $mission): ?array => $this->resolveArchiveBadge($mission->getStatus(), $mission->getEndAt(), $now),
static fn (): ?\DateTimeInterface => null,
fn (HelpMission $mission): ?\DateTimeInterface => $mission->getEndAt()
),
'group' => $this->resolveEntityFeedSnapshot(
$entityManager->getRepository(ThematicGroup::class)->find($sourceId),
static fn (?ThematicGroup $group): bool => $group instanceof ThematicGroup,
fn (ThematicGroup $group): string => $group->getCity(),
static fn (): ?array => null,
static fn (): ?\DateTimeInterface => null,
static fn (): ?\DateTimeInterface => null
),
default => [
'visible' => false,
'locationLabel' => '',
'archiveBadge' => null,
'startAt' => null,
'endAt' => null,
],
};
}
/**
* @template T of object
* @param T|null $entity
* @param callable(T|null):bool $visibilityResolver
* @param callable(T):string $locationResolver
* @param callable(T):(?array{kind:string}) $badgeResolver
* @param callable(T):(?\DateTimeInterface) $startAtResolver
* @param callable(T):(?\DateTimeInterface) $endAtResolver
* @return array{visible:bool, locationLabel:string, archiveBadge:?array{kind:string}, startAt:?string, endAt:?string}
*/
private function resolveEntityFeedSnapshot(
?object $entity,
callable $visibilityResolver,
callable $locationResolver,
callable $badgeResolver,
callable $startAtResolver,
callable $endAtResolver
): array {
if (!$visibilityResolver($entity)) {
return [
'visible' => false,
'locationLabel' => '',
'archiveBadge' => null,
'startAt' => null,
'endAt' => null,
];
}
return [
'visible' => true,
'locationLabel' => trim((string) $locationResolver($entity)),
'archiveBadge' => $badgeResolver($entity),
'startAt' => ($startAtResolver($entity) instanceof \DateTimeInterface) ? $startAtResolver($entity)->format(\DateTimeInterface::ATOM) : null,
'endAt' => ($endAtResolver($entity) instanceof \DateTimeInterface) ? $endAtResolver($entity)->format(\DateTimeInterface::ATOM) : null,
];
}
/**
* @return array{kind:string}|null
*/
private function resolveArchiveBadge(string $status, mixed $endAt, \DateTimeImmutable $now): ?array
{
if ($endAt instanceof \DateTimeInterface && $endAt <= $now) {
return [
'kind' => 'expired',
];
}
if (in_array($status, [InterestActivity::STATUS_CLOSED, InterestActivity::STATUS_CANCELED, HelpMission::STATUS_DONE], true)) {
return [
'kind' => 'closed',
];
}
return null;
}
/**
* @param array<string, mixed> $row
*/
private function resolveProfileUrl(array $row): string
{
$userId = (int) ($row['userId'] ?? 0);
if ($userId <= 0) {
return '/user/profil';
}
return '/user/' . $userId;
}
/**
* @param array<string, mixed> $row
*/
private function resolveAvatarUrl(array $row): string
{
$raw = trim((string) ($row['userImage'] ?? ''));
if ($raw === '') {
return '/officials-imgs/logo.webp';
}
if (str_starts_with($raw, '/var/')) {
return '/officials-imgs/logo.webp';
}
if (str_starts_with($raw, '/public/')) {
return substr($raw, 7);
}
if (str_starts_with($raw, 'http://') || str_starts_with($raw, 'https://') || str_starts_with($raw, '/')) {
return $raw;
}
return '/' . ltrim($raw, '/');
}
/**
* @return array<string, mixed>
*/
private function extractNotificationParams(string $content): array
{
if ($content === '') {
return [];
}
try {
$decoded = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($decoded)) {
return [];
}
$first = $decoded[0] ?? null;
if (!is_array($first)) {
return [];
}
$params = $first['params'] ?? [];
return is_array($params) ? $params : [];
} catch (\Throwable) {
return [];
}
}
#[Route('/api/home/feed/participate/{sourceType}/{sourceId}', name: 'app_home_feed_participate', methods: ['POST'])]
public function participate(
string $sourceType,
int $sourceId,
EntityManagerInterface $entityManager,
EventNotificationManager $eventNotificationManager,
AgendaReminderService $agendaReminderService,
ContentConversationManager $contentConversationManager,
InterestActivityRepository $interestActivityRepository,
InterestActivityRegistrationRepository $interestActivityRegistrationRepository,
ThematicGroupRepository $thematicGroupRepository,
HelpMissionRepository $helpMissionRepository
): JsonResponse {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
/** @var User $user */
$user = $this->getUser();
$normalizedType = strtolower(trim($sourceType));
if (!in_array($normalizedType, ['activity', 'mission', 'group'], true)) {
throw new BadRequestHttpException('Type de contenu invalide.');
}
if ($sourceId <= 0) {
throw new BadRequestHttpException('Identifiant invalide.');
}
$now = new \DateTimeImmutable();
$message = '';
$description = '';
$title = '';
$link = '/';
if ($normalizedType === 'activity') {
/** @var InterestActivity|null $activity */
$activity = $entityManager->getRepository(InterestActivity::class)->find($sourceId);
if (!$activity) {
return $this->json(['ok' => false, 'message' => 'Activite introuvable.'], 404);
}
if ($activity->getOrganizer() && $activity->getOrganizer()->getId() === $user->getId()) {
return $this->json(['ok' => false, 'message' => 'Vous etes deja organisateur de cette activite.'], 409);
}
if ($activity->getStatus() !== InterestActivity::STATUS_OPEN) {
return $this->json(['ok' => false, 'message' => 'Cette activite n accepte plus de participations.'], 409);
}
if ($activity->getStartAt() && $activity->getStartAt() < $now->modify('-2 hours')) {
return $this->json(['ok' => false, 'message' => 'La participation est fermee pour cette activite.'], 409);
}
$existing = $interestActivityRegistrationRepository->findOneBy([
'activity' => $activity,
'user' => $user,
]);
if ($existing && in_array($existing->getStatus(), [
InterestActivityRegistration::STATUS_REGISTERED,
InterestActivityRegistration::STATUS_PARTICIPATED,
], true)) {
return $this->json(['ok' => true, 'already' => true, 'message' => 'Vous participez deja a cette activite.']);
}
if ($interestActivityRepository->countRegisteredSpots($activity) >= $activity->getCapacity()) {
return $this->json(['ok' => false, 'message' => 'Cette activite est complete.'], 409);
}
if (!$existing) {
$existing = (new InterestActivityRegistration())
->setActivity($activity)
->setUser($user)
->setCreatedAt($now);
}
$existing
->setStatus(InterestActivityRegistration::STATUS_REGISTERED)
->setUpdatedAt($now);
$entityManager->persist($existing);
$entityManager->flush();
$contentConversationManager->ensureParticipant(
$contentConversationManager->ensureForInterestActivity($activity),
$user
);
$title = $activity->getTitle();
$description = $activity->getDescription();
$link = '/activities/interet/' . $activity->getId();
$message = sprintf('%s participe a l activite "%s".', $user->getUsername(), $title);
} elseif ($normalizedType === 'mission') {
/** @var HelpMission|null $mission */
$mission = $helpMissionRepository->find($sourceId);
if (!$mission) {
return $this->json(['ok' => false, 'message' => 'Mission introuvable.'], 404);
}
if ($mission->getCreator() && $mission->getCreator()->getId() === $user->getId()) {
return $this->json(['ok' => false, 'message' => 'Vous ne pouvez pas repondre a votre propre mission.'], 409);
}
if ($mission->getResponder() && $mission->getResponder()->getId() === $user->getId()) {
return $this->json(['ok' => true, 'already' => true, 'message' => 'Vous participez deja a cette entraide.']);
}
if ($mission->getStatus() === HelpMission::STATUS_OPEN) {
if ($mission->getResponder() && $mission->getResponder()->getId() !== $user->getId()) {
return $this->json(['ok' => false, 'message' => 'Cette mission est deja prise en charge.'], 409);
}
$mission
->setResponder($user)
->setStatus(HelpMission::STATUS_IN_PROGRESS)
->setUpdatedAt($now);
$entityManager->persist($mission);
$entityManager->flush();
$contentConversationManager->ensureParticipant(
$contentConversationManager->ensureForHelpMission($mission),
$user
);
} else {
return $this->json(['ok' => false, 'message' => 'Cette mission ne peut plus etre rejointe.'], 409);
}
$title = $mission->getTitle();
$description = $mission->getDescription();
$link = '/missions/entraide/' . $mission->getId();
$beneficiary = $mission->getCreator()?->getUsername() ?? 'cet utilisateur';
$message = sprintf('%s a accepte d aider %s pour "%s".', $user->getUsername(), $beneficiary, $title);
} else {
/** @var ThematicGroup|null $group */
$group = $thematicGroupRepository->find($sourceId);
if (!$group) {
return $this->json(['ok' => false, 'message' => 'Groupe introuvable.'], 404);
}
/** @var ThematicGroupMember|null $existing */
$existing = $entityManager->getRepository(ThematicGroupMember::class)->findOneBy([
'thematicGroup' => $group,
'user' => $user,
]);
if ($existing && $existing->getStatus() === ThematicGroupMember::STATUS_ACTIVE) {
return $this->json(['ok' => true, 'already' => true, 'message' => 'Vous participez deja a ce groupe.']);
}
if ($existing && $existing->getStatus() === ThematicGroupMember::STATUS_BANNED) {
return $this->json(['ok' => false, 'message' => 'Votre acces a ce groupe est bloque.'], 403);
}
if ($group->getStatus() !== ThematicGroup::STATUS_OPEN) {
return $this->json(['ok' => false, 'message' => 'Ce groupe n accepte plus de participations.'], 409);
}
if ($thematicGroupRepository->countActiveMembers($group) >= $group->getCapacity()) {
return $this->json(['ok' => false, 'message' => 'Ce groupe a atteint sa capacite maximale.'], 409);
}
if (!$existing) {
$existing = (new ThematicGroupMember())
->setThematicGroup($group)
->setUser($user)
->setRole(ThematicGroupMember::ROLE_MEMBER)
->setCreatedAt($now);
}
$existing
->setStatus(ThematicGroupMember::STATUS_ACTIVE)
->setUpdatedAt($now);
$entityManager->persist($existing);
$entityManager->flush();
$contentConversationManager->ensureParticipant(
$contentConversationManager->ensureForThematicGroup($group),
$user
);
$title = $group->getTitle();
$description = $group->getDescription();
$link = '/groups/thematic/' . $group->getId();
$message = sprintf('%s rejoint la discussion "%s".', $user->getUsername(), $title);
}
if ($message !== '') {
$eventNotificationManager->notify(
$user,
[$user],
'Feed.Participation',
$message,
$link,
[
'sourceType' => $normalizedType,
'sourceId' => $sourceId,
'title' => $title,
'description' => $description,
'link' => $link,
'action' => 'participate',
],
true
);
// Répercute immédiatement la participation dans l'agenda utilisateur.
$agendaReminderService->syncRemindersForUser($user);
}
return $this->json([
'ok' => true,
'message' => $message,
]);
}
private function hasParticipationPost(
EntityManagerInterface $entityManager,
User $user,
string $sourceType,
int $sourceId
): bool {
$needleType = sprintf('"sourceType":"%s"', addslashes($sourceType));
$needleId = sprintf('"sourceId":%d', $sourceId);
$count = (int) $entityManager->createQueryBuilder()
->select('COUNT(n.id)')
->from(Notification::class, 'n')
->where('n.type = :type')
->andWhere('n.sourceUser = :user')
->andWhere('n.status != :deleted')
->andWhere('n.content LIKE :needleType')
->andWhere('n.content LIKE :needleId')
->setParameter('type', 'Feed.Participation')
->setParameter('user', $user)
->setParameter('deleted', Notification::STATUS_DELETED)
->setParameter('needleType', '%' . $needleType . '%')
->setParameter('needleId', '%' . $needleId . '%')
->getQuery()
->getSingleScalarResult();
return $count > 0;
}
/**
* @return array{activity:list<int>, mission:list<int>, group:list<int>}
*/
private function buildJoinedPostIndex(EntityManagerInterface $entityManager, ?User $user): array
{
$index = [
'activity' => [],
'mission' => [],
'group' => [],
];
if (!$user) {
return $index;
}
$index['activity'] = array_values(array_unique(array_map('intval', array_column(
$entityManager->createQueryBuilder()
->select('IDENTITY(r.activity) AS sourceId')
->from(InterestActivityRegistration::class, 'r')
->where('r.user = :user')
->andWhere('r.status IN (:statuses)')
->setParameter('user', $user)
->setParameter('statuses', [
InterestActivityRegistration::STATUS_REGISTERED,
InterestActivityRegistration::STATUS_PARTICIPATED,
])
->getQuery()
->getArrayResult(),
'sourceId'
))));
$index['mission'] = array_values(array_unique(array_map('intval', array_column(
$entityManager->createQueryBuilder()
->select('m.id AS sourceId')
->from(HelpMission::class, 'm')
->where('m.responder = :user')
->setParameter('user', $user)
->getQuery()
->getArrayResult(),
'sourceId'
))));
$index['group'] = array_values(array_unique(array_map('intval', array_column(
$entityManager->createQueryBuilder()
->select('IDENTITY(m.thematicGroup) AS sourceId')
->from(ThematicGroupMember::class, 'm')
->where('m.user = :user')
->andWhere('m.status = :status')
->setParameter('user', $user)
->setParameter('status', ThematicGroupMember::STATUS_ACTIVE)
->getQuery()
->getArrayResult(),
'sourceId'
))));
return $index;
}
}