src/Service/ChatService.php line 280

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\ChatMessage;
  4. use App\Entity\User;
  5. use App\Entity\Team;
  6. use App\Entity\Season;
  7. use App\Repository\ChatMessageRepository;
  8. use App\Repository\NotificationRepository;
  9. use App\Entity\Notification;
  10. use Symfony\Component\Security\Core\Security;
  11. class ChatService
  12. {
  13.     // Corrected constructor with proper namespaces for Repositories
  14.     public function __construct(
  15.         private ChatMessageRepository $chatMessageRepository,
  16.         private NotificationRepository $notificationRepository,
  17.         private \App\Repository\TeamRepository $teamRepository,       // Use App\Repository namespace
  18.         private \App\Repository\SeasonRepository $seasonRepository,   // Use App\Repository namespace
  19.         private \App\Repository\UserRepository $userRepository,       // Use App\Repository namespace
  20.         private Security $security
  21.     ) {}
  22.     public function sendMessage(
  23.         string $content,
  24.         string $type,
  25.         ?Team $team null,
  26.         ?Season $season null,
  27.         ?User $recipient null,
  28.         ?string $attachmentUrl null
  29.     ): ChatMessage {
  30.         $user $this->security->getUser();
  31.         
  32.         // Validate based on type
  33.         if ($type === 'team' && !$team) {
  34.             throw new \InvalidArgumentException('Team is required for team messages');
  35.         }
  36.         
  37.         if ($type === 'season' && !$season) {
  38.             throw new \InvalidArgumentException('Season is required for season messages');
  39.         }
  40.         
  41.         if ($type === 'direct' && !$recipient) {
  42.             throw new \InvalidArgumentException('Recipient is required for direct messages');
  43.         }
  44.         // Check recipient's DM preference if this is a direct message
  45.         if ($type === 'direct' && $recipient) {
  46.             $recipientProfile $recipient->getProfile();
  47.             if ($recipientProfile && !$recipientProfile->isDirectMessagesAllowed()) {
  48.                 // It's important to use a specific exception type or error code if the frontend
  49.                 // needs to display a specific message to the sender.
  50.                 // For now, a generic InvalidArgumentException or a custom one.
  51.                 throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException('This user is not accepting direct messages.');
  52.             }
  53.         }
  54.         
  55.         // Create message
  56.         $message = new ChatMessage();
  57.         $message->setUser($user);
  58.         $message->setContent($content);
  59.         $message->setType($type);
  60.         
  61.         if ($team) {
  62.             $message->setTeam($team);
  63.         }
  64.         
  65.         if ($season) {
  66.             $message->setSeason($season);
  67.         }
  68.         
  69.         if ($recipient) {
  70.             $message->setRecipient($recipient);
  71.         }
  72.         
  73.         if ($attachmentUrl) {
  74.             $message->setAttachmentUrl($attachmentUrl);
  75.         }
  76.         
  77.         // Save message
  78.         $this->chatMessageRepository->save($messagetrue);
  79.         
  80.         // Create notifications for recipients
  81.         $this->createNotificationsForMessage($message);
  82.         
  83.         return $message;
  84.     }
  85.     
  86.     private function createNotificationsForMessage(ChatMessage $message): void
  87.     {
  88.         $sender $message->getUser();
  89.         
  90.         if ($message->getType() === 'direct') {
  91.             // Create notification for direct message recipient
  92.             $this->createNotification(
  93.                 $message->getRecipient(),
  94.                 'new_message',
  95.                 'You received a new message',
  96.                 [
  97.                     'messageId' => $message->getId(),
  98.                     'senderId' => $sender->getId()
  99.                 ]
  100.             );
  101.         } elseif ($message->getType() === 'team') {
  102.             // Create notifications for all team members except sender
  103.             $team $message->getTeam();
  104.             if ($team) { // Add null check for team
  105.                 foreach ($team->getMembers() as $member) {
  106.                     if ($member && $member->getId() !== $sender->getId()) { // Add null check for member
  107.                         $this->createNotification(
  108.                             $member,
  109.                             'new_team_message',
  110.                             'New message in team chat',
  111.                             [
  112.                                 'messageId' => $message->getId(),
  113.                                 'teamId' => $team->getId(),
  114.                                 'senderId' => $sender->getId()
  115.                             ]
  116.                         );
  117.                     }
  118.                 }
  119.             }
  120.         } elseif ($message->getType() === 'season') {
  121.             // Create notifications for all season participants except sender
  122.             $season $message->getSeason();
  123.             if ($season) { // Add null check for season
  124.                 foreach ($season->getTeams() as $team) {
  125.                     // Add null check for team within season
  126.                     if ($team) {
  127.                         foreach ($team->getMembers() as $member) {
  128.                             // Add null check for member and ensure not sender
  129.                             if ($member && $member->getId() !== $sender->getId()) {
  130.                                 $this->createNotification(
  131.                                     $member,
  132.                                     'new_season_message',
  133.                                     'New message in season chat',
  134.                                     [
  135.                                         'messageId' => $message->getId(),
  136.                                         'seasonId' => $season->getId(),
  137.                                         'senderId' => $sender->getId()
  138.                                     ]
  139.                                 );
  140.                             }
  141.                         }
  142.                     }
  143.                 }
  144.             }
  145.         }
  146.         // No 'global' case needed here
  147.     // End of createNotificationsForMessage method
  148.     private function createNotification(User $userstring $typestring $message, array $data): void
  149.     {
  150.         $notification = new Notification();
  151.         $notification->setUser($user);
  152.         $notification->setType($type);
  153.         $notification->setContent($message);
  154.         $notification->setData($data);
  155.         $notification->setIsRead(false);
  156.         
  157.         $this->notificationRepository->save($notificationtrue);
  158.     }
  159.     
  160.     public function getMessages(string $type$channelint $limit 50int $offset 0): array
  161.     {
  162.         if ($type === 'team') {
  163.             return $this->chatMessageRepository->findByTeam($channel$limit$offset);
  164.         } elseif ($type === 'season') {
  165.             return $this->chatMessageRepository->findBySeason($channel$limit$offset);
  166.         } elseif ($type === 'direct') {
  167.             $currentUser $this->security->getUser();
  168.             return $this->chatMessageRepository->findDirectMessages($currentUser$channel$limit$offset);
  169.         }
  170.         
  171.         throw new \InvalidArgumentException('Invalid chat type');
  172.     }
  173.     
  174.     public function getNewMessages(string $type$channel\DateTimeInterface $since): array
  175.     {
  176.         if ($type === 'team') {
  177.             return $this->chatMessageRepository->findNewByTeam($channel$since);
  178.         } elseif ($type === 'season') {
  179.             return $this->chatMessageRepository->findNewBySeason($channel$since);
  180.         } elseif ($type === 'direct') {
  181.             $currentUser $this->security->getUser();
  182.             return $this->chatMessageRepository->findNewDirectMessages($currentUser$channel$since);
  183.         }
  184.         
  185.         throw new \InvalidArgumentException('Invalid chat type');
  186.     }
  187.     
  188.     public function markAsRead(ChatMessage $message): void
  189.     {
  190.         $message->markAsRead();
  191.         $this->chatMessageRepository->save($messagetrue);
  192.     }
  193.     /**
  194.      * Get all available channels for a user
  195.      */
  196.     public function getAvailableChannels(User $user): array
  197.     {
  198.         // Get teams the user is a member of
  199.         $teams $this->teamRepository->findByUser($user); // Use injected repository
  200.         
  201.         // Get seasons the user is participating in
  202.         $seasons $this->seasonRepository->findByUser($user); // Use injected repository
  203.         
  204.         // Get users the user has recently messaged
  205.         $recentChatPartners $this->chatMessageRepository->findRecentChatPartners($user);
  206.         
  207.         return [
  208.             'global' => [
  209.                 [
  210.                     'id' => 'global',
  211.                     'name' => 'Global Chat',
  212.                     'type' => 'global'
  213.                 ]
  214.             ],
  215.             'teams' => $teams,
  216.             'seasons' => $seasons,
  217.             'direct' => $recentChatPartners
  218.         ];
  219.     }
  220.     /**
  221.      * Get messages for a specific channel
  222.      */
  223.     public function getMessagesByChannel(string $type, ?int $idint $limit 50int $offset 0): array
  224.     {
  225.         if ($type === 'global') {
  226.             // For global chat, get messages where the user is messaging themselves
  227.             return $this->chatMessageRepository->findGlobalMessages($limit$offset);
  228.         } elseif ($type === 'team') {
  229.             $team $this->teamRepository->find($id); // Use injected repository
  230.             if (!$team) {
  231.                 throw new \InvalidArgumentException('Team not found');
  232.             }
  233.             return $this->chatMessageRepository->findByTeam($team$limit$offset);
  234.         } elseif ($type === 'season') {
  235.             $season $this->seasonRepository->find($id); // Use injected repository
  236.             if (!$season) {
  237.                 throw new \InvalidArgumentException('Season not found');
  238.             }
  239.             return $this->chatMessageRepository->findBySeason($season$limit$offset);
  240.         } elseif ($type === 'direct') {
  241.             $recipient $this->userRepository->find($id);
  242.             if (!$recipient) {
  243.                 // This would likely cause a 404 or specific error, not a 500, but good to be aware of.
  244.                 // error_log("[DEBUG] getMessagesByChannel: Recipient not found for ID: " . ($id ?? 'null'));
  245.                 throw new \InvalidArgumentException('User not found for direct message channel.');
  246.             }
  247.             $currentUser $this->security->getUser();
  248.             if (!$currentUser) {
  249.                 // This should ideally be caught earlier, e.g., in the controller.
  250.                 // error_log("[DEBUG] getMessagesByChannel: Current user not available for direct messages.");
  251.                 throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException('User not authenticated for direct messages.');
  252.             }
  253.             // ---- DEBUG: Return before calling findDirectMessages ----
  254.             // error_log("[DEBUG] getMessagesByChannel: About to call findDirectMessages for recipient ID: " . $recipient->getId() . " and current user ID: " . $currentUser->getId());
  255.             $testMessage = new ChatMessage();
  256.             $testMessage->setContent("Debug: About to fetch DMs for user ID " $recipient->getId());
  257.             $testMessage->setType('direct');
  258.             // $testMessage->setUser($currentUser); // Keep it simple
  259.             // $testMessage->setRecipient($recipient);
  260.             return [$testMessage];
  261.             // ---- END DEBUG ----
  262.             // return $this->chatMessageRepository->findDirectMessages($currentUser, $recipient, $limit, $offset);
  263.         }
  264.         
  265.         throw new \InvalidArgumentException('Invalid channel type');
  266.     }
  267.     /**
  268.      * Get unread counts for all channels
  269.      */
  270.     public function getUnreadCountsByChannel(User $user): array
  271.     {
  272.         $counts = [
  273.             'global' => $this->chatMessageRepository->countUnreadGlobalMessages($user),
  274.             'teams' => [],
  275.             'seasons' => [],
  276.             'direct' => []
  277.         ];
  278.         
  279.         // Get teams the user is a member of
  280.         $teams $this->teamRepository->findByUser($user); // Use injected repository
  281.         foreach ($teams as $team) {
  282.             $counts['teams'][$team->getId()] = $this->chatMessageRepository->countUnreadByUserAndTeam($user$team);
  283.         }
  284.         
  285.         // Get seasons the user is participating in
  286.         $seasons $this->seasonRepository->findByUser($user); // Use injected repository
  287.         foreach ($seasons as $season) {
  288.             $counts['seasons'][$season->getId()] = $this->chatMessageRepository->countUnreadByUserAndSeason($user$season);
  289.         }
  290.         
  291.         // Get users the user has recently messaged
  292.         $recentChatPartners $this->chatMessageRepository->findRecentChatPartners($user);
  293.         foreach ($recentChatPartners as $partner) {
  294.             $counts['direct'][$partner->getId()] = $this->chatMessageRepository->countUnreadDirectMessages($user$partner);
  295.         }
  296.         
  297.         return $counts;
  298.     }
  299.     /**
  300.      * Mark all messages in a channel as read
  301.      */
  302.     public function markChannelAsRead(string $type, ?int $id): void
  303.     {
  304.         $currentUser $this->security->getUser();
  305.         
  306.         if ($type === 'global') {
  307.             $this->chatMessageRepository->markAllGlobalMessagesAsRead($currentUser);
  308.         } elseif ($type === 'team') {
  309.             $team $this->teamRepository->find($id); // Use injected repository
  310.             if (!$team) {
  311.                 throw new \InvalidArgumentException('Team not found');
  312.             }
  313.             $this->chatMessageRepository->markAllAsReadForUserInTeam($currentUser$team);
  314.         } elseif ($type === 'season') {
  315.             $season $this->seasonRepository->find($id); // Use injected repository
  316.             if (!$season) {
  317.                 throw new \InvalidArgumentException('Season not found');
  318.             }
  319.             $this->chatMessageRepository->markAllAsReadForUserInSeason($currentUser$season);
  320.         } elseif ($type === 'direct') {
  321.             $recipient $this->userRepository->find($id); // Use injected repository
  322.             if (!$recipient) {
  323.                 throw new \InvalidArgumentException('User not found');
  324.             }
  325.             $this->chatMessageRepository->markAllDirectMessagesAsRead($currentUser$recipient);
  326.         } else {
  327.             throw new \InvalidArgumentException('Invalid channel type');
  328.         }
  329.     }
  330.     public function getRecentMessagesForWidget(User $userint $limitPerChannelint $maxChannels): array
  331.     {
  332.         $widgetMessages = [];
  333.         $channelsProcessed 0;
  334.         $availableChannels $this->getAvailableChannels($user);
  335.         // Prioritize channels: DMs, Teams, Seasons, Global (can be adjusted)
  336.         $channelOrder = ['direct''teams''seasons''global'];
  337.         foreach ($channelOrder as $channelTypeKey) {
  338.             if (!isset($availableChannels[$channelTypeKey])) {
  339.                 continue;
  340.             }
  341.             $channelsOfType $availableChannels[$channelTypeKey];
  342.             if (!is_array($channelsOfType)) { // Ensure it's an array
  343.                 continue;
  344.             }
  345.             foreach ($channelsOfType as $channelInfo) {
  346.                 if (!$channelInfo) { // Add null check for $channelInfo
  347.                     continue;
  348.                 }
  349.                 if ($channelsProcessed >= $maxChannels) {
  350.                     break 2// Break both loops
  351.                 }
  352.                 $channelId null;
  353.                 $channelName 'Unknown Channel';
  354.                 $actualChannelType $channelTypeKey// 'direct', 'teams', 'seasons', 'global'
  355.                 if ($channelTypeKey === 'global' && isset($channelInfo['id']) && $channelInfo['id'] === 'global') {
  356.                     $channelId 'global';
  357.                     $channelName $channelInfo['name'] ?? 'Global Chat';
  358.                 } elseif (is_object($channelInfo) && method_exists($channelInfo'getId') && method_exists($channelInfo'getName')) {
  359.                     // For Team and Season entities
  360.                     $channelId $actualChannelType '-' $channelInfo->getId();
  361.                     $channelName $channelInfo->getName();
  362.                 } elseif ($channelTypeKey === 'direct' && is_object($channelInfo) && method_exists($channelInfo'getId') && method_exists($channelInfo'getUsername')) {
  363.                     // For User entities (direct messages)
  364.                     $channelId 'direct-' $channelInfo->getId();
  365.                     $channelName $channelInfo->getUsername(); // Or a display name if available
  366.                 } else {
  367.                     continue; // Skip if channel info is not as expected
  368.                 }
  369.                 
  370.                 // Fetch messages for this channel
  371.                 $idForGetMessages null;
  372.                 if ($actualChannelType === 'global') {
  373.                     $idForGetMessages null// getMessagesByChannel expects null for global's $id
  374.                 } elseif (is_object($channelInfo) && method_exists($channelInfo'getId')) {
  375.                     $idForGetMessages $channelInfo->getId();
  376.                 } else {
  377.                     // Should not happen if $channelInfo is structured correctly from getAvailableChannels
  378.                     continue;
  379.                 }
  380.                 $messages $this->getMessagesByChannel(
  381.                     $actualChannelType,
  382.                     $idForGetMessages,
  383.                     $limitPerChannel,
  384.                     // offset
  385.                 );
  386.                 if (!empty($messages)) {
  387.                     $formattedMessages = [];
  388.                     foreach ($messages as $message) {
  389.                         if (!$message instanceof ChatMessage) continue;
  390.                         $sender $message->getUser();
  391.                         $formattedMessages[] = [
  392.                             'id' => $message->getId(),
  393.                             'senderId' => $sender $sender->getId() : null,
  394.                             'senderName' => $sender ? ($sender->getFullName() ?: $sender->getUsername() ?: 'Unknown User') : 'System',
  395.                             'content' => substr($message->getContent() ?? ''0100) . (strlen($message->getContent() ?? '') > 100 '...' ''), // Snippet
  396.                             'createdAt' => $message->getCreatedAt() ? $message->getCreatedAt()->format(\DateTimeInterface::ISO8601) : null,
  397.                             'channelId' => $channelId// Composite ID like "team-123" or "direct-456"
  398.                             'channelType' => $actualChannelType,
  399.                             'channelName' => $channelName// For display in widget
  400.                         ];
  401.                     }
  402.                     // Use a unique key for the channel in the widget, could be $channelId or $channelName
  403.                     // For simplicity, using $channelName, but ensure it's unique enough or use $channelId
  404.                     if (!empty($formattedMessages)) {
  405.                         // Sort messages by createdAt descending to show newest first for each channel
  406.                         usort($formattedMessages, function ($a$b) {
  407.                             $timeA $a['createdAt'] ? strtotime($a['createdAt']) : 0// Treat null as very old
  408.                             $timeB $b['createdAt'] ? strtotime($b['createdAt']) : 0// Treat null as very old
  409.                             
  410.                             if ($timeA === false$timeA 0// Handle potential false from strtotime
  411.                             if ($timeB === false$timeB 0;
  412.                             return $timeB $timeA// For descending order
  413.                         });
  414.                         // Use the unique $channelId as the key for widgetMessages
  415.                         // The frontend will then iterate over Object.values() or use the channelName from the message payload
  416.                         $widgetMessages[$channelId] = $formattedMessages;
  417.                         $channelsProcessed++;
  418.                     }
  419.                 }
  420.             }
  421.         }
  422.         return $widgetMessages;
  423.     }
  424. }