<?php
namespace App\Services;
use App\Entity\User;
use App\Entity\NotificationHistory;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
class MercureService
{
private $hub;
private $em;
private $security;
private $notificationHistoryService;
private string $mercureEnabled;
private string $expoPushEnabled;
private $publicUrl;
private LoggerInterface $logger;
public function __construct(
EntityManagerInterface $entityManager,
Security $security,
HubInterface $hub,
NotificationHistoryService $notificationHistoryService,
LoggerInterface $logger,
string $mercureEnabled = '0',
string $expoPushEnabled = '1', // Par défaut activé
ContainerBagInterface $containerBag,
) {
$this->em = $entityManager;
$this->security = $security;
$this->hub = $hub;
$this->notificationHistoryService = $notificationHistoryService;
$this->logger = $logger;
$this->mercureEnabled = $mercureEnabled;
$this->expoPushEnabled = $expoPushEnabled;
$this->publicUrl = $containerBag->get('publicUrl');
}
public function publish(User $user, $payloadData, array $type) {
try {
if ($user !== $this->security->getUser()) {
$notificationHistory = $this->saveNotification($user, $payloadData, $type);
// 2. Formater les données de notification
$notifData = $this->notificationHistoryService->formatNotificationObject($type);
$notifData['url'] = $this->notificationHistoryService->getNotificationUrl($notifData);
$notifData['notificationId'] = $notificationHistory->getId();
// 3. Envoyer via Mercure (si activé)
if ($this->mercureEnabled === '1') {
$this->sendMercureNotification($user, $payloadData, $notifData);
}
$type = $this->notificationHistoryService->formatNotificationObject($type);
// 4. Envoyer via Expo Push (si activé ET si l'utilisateur a un token)
if ($this->expoPushEnabled === '1') {
$this->sendExpoNotification($user, [
'title' => $payloadData['title'] ?? 'New Notification',
'message' => $payloadData['message'] ?? 'You have a new notification',
'url' => $notifData['url'] ?? '/',
'screen' => $this->convertUrlToScreen($notifData['url'] ?? '/'), // Convertir URL en screen
'type' => $type,
'notificationId' => $notificationHistory->getId() // ID de la notification enregistrée
]);
}
}
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* Envoie une notification via Mercure
*/
private function sendMercureNotification(User $user, array $payloadData, array $notifData): void
{
try {
$payload = json_encode(array_merge($payloadData, $notifData));
$update = new Update(
$this->publicUrl.'/user/' . $user->getId(),
$payload,
true
);
$this->hub->publish($update);
} catch (\Exception $e) {
$this->logger->error('Error sending Mercure notification', [
'user_id' => $user->getId(),
'error' => $e->getMessage()
]);
}
}
/**
* Envoie une notification push via Expo
*
* @param User $user L'utilisateur destinataire
* @param array $notificationData Données de la notification
* @return bool|string
*/
private function sendExpoNotification(User $user, array $notificationData)
{
// Vérifier si l'utilisateur a un token Expo
$expoPushToken = $user->getDeviceToken();
if (!$expoPushToken) {
$this->logger->debug('User has no device token', [
'user_id' => $user->getId()
]);
return false;
}
// Vérifier que le token est un token Expo valide
if (!str_starts_with($expoPushToken, 'ExponentPushToken[')) {
$this->logger->warning('Invalid Expo push token format', [
'user_id' => $user->getId(),
'token' => substr($expoPushToken, 0, 20) . '...'
]);
return false;
}
try {
// Préparer le payload pour Expo
$payload = [
'to' => $expoPushToken,
'sound' => 'default',
'title' => $notificationData['title'] ?? 'Notification',
'body' => $notificationData['message'] ?? '',
'priority' => 'high',
'data' => [
// Format pour le frontend React Native
'screen' => $notificationData['screen'] ?? $this->convertUrlToScreen($notificationData['url'] ?? '/'),
'url' => $notificationData['url'] ?? '/',
'notificationId' => $notificationData['notificationId'] ?? null,
'type' => $notificationData['type'] ?? null,
],
];
// Envoyer à l'API Expo
$ch = curl_init("https://exp.host/--/api/v2/push/send");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
'Accept-Encoding: gzip, deflate'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_POST, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
throw new \Exception("cURL Error: " . $curlError);
}
if ($httpCode !== 200) {
$responseData = json_decode($response, true);
throw new \Exception("Expo API Error (HTTP $httpCode): " . ($responseData['errors'][0]['message'] ?? 'Unknown error'));
}
$result = json_decode($response, true);
// Vérifier si l'envoi a réussi
if (isset($result['data']['status']) && $result['data']['status'] === 'error') {
$errorMessage = $result['data']['message'] ?? 'Unknown error';
$this->logger->error('Expo push notification failed', [
'user_id' => $user->getId(),
'error' => $errorMessage,
'details' => $result['data']
]);
// Si le token est invalide, le supprimer
if (str_contains($errorMessage, 'InvalidCredentials') ||
str_contains($errorMessage, 'DeviceNotRegistered')) {
$user->setDeviceToken(null);
$this->em->persist($user);
$this->em->flush();
}
return false;
}
$this->logger->info('Expo push notification sent successfully', [
'user_id' => $user->getId(),
'ticket_id' => $result['data']['id'] ?? null
]);
return $response;
} catch (\Exception $e) {
$this->logger->error('Error sending Expo push notification', [
'user_id' => $user->getId(),
'error' => $e->getMessage()
]);
return false;
}
}
public function saveNotification(User $user, $payloadData, $type) {
try {
$notification = new NotificationHistory();
$notification->setUser($user);
$notification->setTitle($payloadData['title']);
$notification->setMessage($payloadData['message']);
$notification->setCreatedAt(new \DateTime());
$notification->setSeen(false);
$notification = $this->notificationHistoryService->setNotificationObject($notification, $type);
$this->em->persist($notification);
$this->em->flush();
return $notification;
} catch (Exception $e) {
return null;
}
}
/**
* Convertit une URL en format screen pour React Native
*
* @param string $url URL à convertir
* @return string
*/
private function convertUrlToScreen(string $url): string
{
// Exemples de conversion
// /complaint/123 -> /screens/complaint/Detail
// /contract/456 -> /screens/contract/Detail
// /dashboard -> /screens/Dashboard
$url = trim($url, '/');
// Mapping des routes
$routeMapping = [
'complaint' => '/screens/complaint/Detail',
'contract' => '/screens/contract/Detail',
'payment' => '/screens/payment/List',
'dashboard' => '/screens/Dashboard',
'profil' => '/screens/account/Profil',
];
foreach ($routeMapping as $key => $screen) {
if (str_starts_with($url, $key)) {
return $screen;
}
}
// Par défaut, retourner l'URL telle quelle ou Dashboard
return $url ? '/screens/' . $url : '/screens/Dashboard';
}
}