src/Services/MercureService.php line 30

Open in your IDE?
  1. <?php
  2. namespace App\Services;
  3. use App\Entity\User;
  4. use App\Entity\NotificationHistory;
  5. use DateTime;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Exception;
  8. use Psr\Log\LoggerInterface;
  9. use Symfony\Component\Mercure\HubInterface;
  10. use Symfony\Component\Mercure\Update;
  11. use Symfony\Component\Security\Core\Security;
  12. use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
  13. class MercureService
  14. {
  15. private $hub;
  16. private $em;
  17. private $security;
  18. private $notificationHistoryService;
  19. private string $mercureEnabled;
  20. private string $expoPushEnabled;
  21. private $publicUrl;
  22. private LoggerInterface $logger;
  23. public function __construct(
  24. EntityManagerInterface $entityManager,
  25. Security $security,
  26. HubInterface $hub,
  27. NotificationHistoryService $notificationHistoryService,
  28. LoggerInterface $logger,
  29. string $mercureEnabled = '0',
  30. string $expoPushEnabled = '1', // Par défaut activé
  31. ContainerBagInterface $containerBag,
  32. ) {
  33. $this->em = $entityManager;
  34. $this->security = $security;
  35. $this->hub = $hub;
  36. $this->notificationHistoryService = $notificationHistoryService;
  37. $this->logger = $logger;
  38. $this->mercureEnabled = $mercureEnabled;
  39. $this->expoPushEnabled = $expoPushEnabled;
  40. $this->publicUrl = $containerBag->get('publicUrl');
  41. }
  42. public function publish(User $user, $payloadData, array $type) {
  43. try {
  44. if ($user !== $this->security->getUser()) {
  45. $notificationHistory = $this->saveNotification($user, $payloadData, $type);
  46. // 2. Formater les données de notification
  47. $notifData = $this->notificationHistoryService->formatNotificationObject($type);
  48. $notifData['url'] = $this->notificationHistoryService->getNotificationUrl($notifData);
  49. $notifData['notificationId'] = $notificationHistory->getId();
  50. // 3. Envoyer via Mercure (si activé)
  51. if ($this->mercureEnabled === '1') {
  52. $this->sendMercureNotification($user, $payloadData, $notifData);
  53. }
  54. $type = $this->notificationHistoryService->formatNotificationObject($type);
  55. // 4. Envoyer via Expo Push (si activé ET si l'utilisateur a un token)
  56. if ($this->expoPushEnabled === '1') {
  57. $this->sendExpoNotification($user, [
  58. 'title' => $payloadData['title'] ?? 'New Notification',
  59. 'message' => $payloadData['message'] ?? 'You have a new notification',
  60. 'url' => $notifData['url'] ?? '/',
  61. 'screen' => $this->convertUrlToScreen($notifData['url'] ?? '/'), // Convertir URL en screen
  62. 'type' => $type,
  63. 'notificationId' => $notificationHistory->getId() // ID de la notification enregistrée
  64. ]);
  65. }
  66. }
  67. } catch (\Exception $e) {
  68. return false;
  69. }
  70. return true;
  71. }
  72. /**
  73. * Envoie une notification via Mercure
  74. */
  75. private function sendMercureNotification(User $user, array $payloadData, array $notifData): void
  76. {
  77. try {
  78. $payload = json_encode(array_merge($payloadData, $notifData));
  79. $update = new Update(
  80. $this->publicUrl.'/user/' . $user->getId(),
  81. $payload,
  82. true
  83. );
  84. $this->hub->publish($update);
  85. } catch (\Exception $e) {
  86. $this->logger->error('Error sending Mercure notification', [
  87. 'user_id' => $user->getId(),
  88. 'error' => $e->getMessage()
  89. ]);
  90. }
  91. }
  92. /**
  93. * Envoie une notification push via Expo
  94. *
  95. * @param User $user L'utilisateur destinataire
  96. * @param array $notificationData Données de la notification
  97. * @return bool|string
  98. */
  99. private function sendExpoNotification(User $user, array $notificationData)
  100. {
  101. // Vérifier si l'utilisateur a un token Expo
  102. $expoPushToken = $user->getDeviceToken();
  103. if (!$expoPushToken) {
  104. $this->logger->debug('User has no device token', [
  105. 'user_id' => $user->getId()
  106. ]);
  107. return false;
  108. }
  109. // Vérifier que le token est un token Expo valide
  110. if (!str_starts_with($expoPushToken, 'ExponentPushToken[')) {
  111. $this->logger->warning('Invalid Expo push token format', [
  112. 'user_id' => $user->getId(),
  113. 'token' => substr($expoPushToken, 0, 20) . '...'
  114. ]);
  115. return false;
  116. }
  117. try {
  118. // Préparer le payload pour Expo
  119. $payload = [
  120. 'to' => $expoPushToken,
  121. 'sound' => 'default',
  122. 'title' => $notificationData['title'] ?? 'Notification',
  123. 'body' => $notificationData['message'] ?? '',
  124. 'priority' => 'high',
  125. 'data' => [
  126. // Format pour le frontend React Native
  127. 'screen' => $notificationData['screen'] ?? $this->convertUrlToScreen($notificationData['url'] ?? '/'),
  128. 'url' => $notificationData['url'] ?? '/',
  129. 'notificationId' => $notificationData['notificationId'] ?? null,
  130. 'type' => $notificationData['type'] ?? null,
  131. ],
  132. ];
  133. // Envoyer à l'API Expo
  134. $ch = curl_init("https://exp.host/--/api/v2/push/send");
  135. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  136. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  137. 'Content-Type: application/json',
  138. 'Accept: application/json',
  139. 'Accept-Encoding: gzip, deflate'
  140. ]);
  141. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
  142. curl_setopt($ch, CURLOPT_POST, true);
  143. $response = curl_exec($ch);
  144. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  145. $curlError = curl_error($ch);
  146. curl_close($ch);
  147. if ($curlError) {
  148. throw new \Exception("cURL Error: " . $curlError);
  149. }
  150. if ($httpCode !== 200) {
  151. $responseData = json_decode($response, true);
  152. throw new \Exception("Expo API Error (HTTP $httpCode): " . ($responseData['errors'][0]['message'] ?? 'Unknown error'));
  153. }
  154. $result = json_decode($response, true);
  155. // Vérifier si l'envoi a réussi
  156. if (isset($result['data']['status']) && $result['data']['status'] === 'error') {
  157. $errorMessage = $result['data']['message'] ?? 'Unknown error';
  158. $this->logger->error('Expo push notification failed', [
  159. 'user_id' => $user->getId(),
  160. 'error' => $errorMessage,
  161. 'details' => $result['data']
  162. ]);
  163. // Si le token est invalide, le supprimer
  164. if (str_contains($errorMessage, 'InvalidCredentials') ||
  165. str_contains($errorMessage, 'DeviceNotRegistered')) {
  166. $user->setDeviceToken(null);
  167. $this->em->persist($user);
  168. $this->em->flush();
  169. }
  170. return false;
  171. }
  172. $this->logger->info('Expo push notification sent successfully', [
  173. 'user_id' => $user->getId(),
  174. 'ticket_id' => $result['data']['id'] ?? null
  175. ]);
  176. return $response;
  177. } catch (\Exception $e) {
  178. $this->logger->error('Error sending Expo push notification', [
  179. 'user_id' => $user->getId(),
  180. 'error' => $e->getMessage()
  181. ]);
  182. return false;
  183. }
  184. }
  185. public function saveNotification(User $user, $payloadData, $type) {
  186. try {
  187. $notification = new NotificationHistory();
  188. $notification->setUser($user);
  189. $notification->setTitle($payloadData['title']);
  190. $notification->setMessage($payloadData['message']);
  191. $notification->setCreatedAt(new \DateTime());
  192. $notification->setSeen(false);
  193. $notification = $this->notificationHistoryService->setNotificationObject($notification, $type);
  194. $this->em->persist($notification);
  195. $this->em->flush();
  196. return $notification;
  197. } catch (Exception $e) {
  198. return null;
  199. }
  200. }
  201. /**
  202. * Convertit une URL en format screen pour React Native
  203. *
  204. * @param string $url URL à convertir
  205. * @return string
  206. */
  207. private function convertUrlToScreen(string $url): string
  208. {
  209. // Exemples de conversion
  210. // /complaint/123 -> /screens/complaint/Detail
  211. // /contract/456 -> /screens/contract/Detail
  212. // /dashboard -> /screens/Dashboard
  213. $url = trim($url, '/');
  214. // Mapping des routes
  215. $routeMapping = [
  216. 'complaint' => '/screens/complaint/Detail',
  217. 'contract' => '/screens/contract/Detail',
  218. 'payment' => '/screens/payment/List',
  219. 'dashboard' => '/screens/Dashboard',
  220. 'profil' => '/screens/account/Profil',
  221. ];
  222. foreach ($routeMapping as $key => $screen) {
  223. if (str_starts_with($url, $key)) {
  224. return $screen;
  225. }
  226. }
  227. // Par défaut, retourner l'URL telle quelle ou Dashboard
  228. return $url ? '/screens/' . $url : '/screens/Dashboard';
  229. }
  230. }