src/EventSubscriber/AlertForecastReportTwitterEventSubscriber.php line 19

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Service\ReportModerationService;
  4. use App\Service\TwitterService;
  5. use Pimcore\Event\Model\DataObjectEvent;
  6. use Pimcore\Log\ApplicationLogger;
  7. use Pimcore\Model\DataObject\Report;
  8. class AlertForecastReportTwitterEventSubscriber
  9. {
  10.     public function __construct(
  11.         private TwitterService $twitterService,
  12.         private ApplicationLogger $logger,
  13.     ) {
  14.     }
  15.     public function onAlertForecastReportPublished(DataObjectEvent $event): void
  16.     {
  17.         $report $event->getObject();
  18.         if (!$report instanceof Report) {
  19.             return;
  20.         }
  21.         if (!$this->shouldPostToTwitter($report)) {
  22.             return;
  23.         }
  24.         $baseUrl rtrim((string) ($_ENV['PUBLIC_URL'] ?? NCM_PUBLIC_PORTAL_URL), '/');
  25.         $reportId $report->getId();
  26.         $publicReportUrlEn $baseUrl '/en/weather/weather-reports/' $reportId;
  27.         $publicReportUrlAr $baseUrl '/ar/weather/weather-reports/' $reportId;
  28.         $tweetMessage $this->buildTweetMessage($report$publicReportUrlEn$publicReportUrlAr);
  29.         try {
  30.             $tweet $this->postTweetWithOptionalMedia($report$tweetMessage);
  31.             $this->logger->info('Alert forecast report Twitter post response: ' json_encode($tweet));
  32.             if (!empty($tweet['tweetId'])) {
  33.                 $report->setTwitterId($tweet['tweetId']);
  34.             }
  35.             if (isset($tweet['data'])) {
  36.                 $report->setTwitterLog($tweet['data']);
  37.             }
  38.             // Persist the tweet id without $report->save() (which would re-dispatch postUpdate
  39.             // and re-enter the email subscriber). Do NOT stamp deliveredVersion here - the email
  40.             // subscriber owns that, so stamping it here would suppress the email on the same publish.
  41.             ReportModerationService::persistReportColumns($report, [
  42.                 'twitterId' => $report->getTwitterId(),
  43.                 'twitterLog' => $report->getTwitterLog(),
  44.             ]);
  45.         } catch (\Throwable $e) {
  46.             $this->logger->error('Alert forecast report Twitter post failed: ' $e->getMessage());
  47.         }
  48.     }
  49.     private function buildTweetMessage(Report $reportstring $publicReportUrlEnstring $publicReportUrlAr): string
  50.     {
  51.         $titleEn trim((string) $report->getReportTitle('en'));
  52.         $titleAr trim((string) $report->getReportTitle('ar'));
  53.         $typeEn trim((string) ($report->getReportType()?->getName('en') ?? ''));
  54.         $typeAr trim((string) ($report->getReportType()?->getName('ar') ?? ''));
  55.         $twitterDescription $this->buildTwitterDescriptionBlock($report);
  56.         $trimOrder array_values(array_filter([
  57.             $twitterDescription !== '' $twitterDescription null,
  58.             $this->buildTitleBlock($titleEn$titleAr) ?: null,
  59.             $this->buildTypeBlock($typeEn$typeAr) ?: null,
  60.         ]));
  61.         $linkBlock $this->buildLinkBlock($publicReportUrlAr$publicReportUrlEn);
  62.         return $this->fitTweetLength($trimOrder$linkBlock);
  63.     }
  64.     private function buildTwitterDescriptionBlock(Report $report): string
  65.     {
  66.         return trim((string) $report->getTwitterDescription());
  67.     }
  68.     private function buildTitleBlock(string $titleEnstring $titleAr): string
  69.     {
  70.         $lines = [];
  71.         if ($titleEn !== '') {
  72.             $lines[] = '⚠️ ' $titleEn;
  73.         }
  74.         if ($titleAr !== '' && $titleAr !== $titleEn) {
  75.             $lines[] = $titleAr;
  76.         }
  77.         return implode("\n"$lines);
  78.     }
  79.     private function buildTypeBlock(string $typeEnstring $typeAr): string
  80.     {
  81.         if ($typeEn === '' && $typeAr === '') {
  82.             return '';
  83.         }
  84.         if ($typeEn !== '' && $typeAr !== '' && $typeEn !== $typeAr) {
  85.             return '📋 ' $typeEn ' | ' $typeAr;
  86.         }
  87.         return '📋 ' . ($typeEn !== '' $typeEn $typeAr);
  88.     }
  89.     private function buildLinkBlock(string $publicReportUrlArstring $publicReportUrlEn): string
  90.     {
  91.         $lines = [];
  92.         if ($publicReportUrlAr !== '') {
  93.             $lines[] = "🔗 عربي\n" $publicReportUrlAr;
  94.         }
  95.         if ($publicReportUrlEn !== '') {
  96.             $lines[] = "🔗 EN\n" $publicReportUrlEn;
  97.         }
  98.         return implode("\n"$lines);
  99.     }
  100.     /**
  101.      * Trims sections in order (twitterDescription → titles → type) while keeping the link block intact.
  102.      *
  103.      * @param string[] $bodySections
  104.      */
  105.     private function fitTweetLength(array $bodySectionsstring $linkBlockint $maxLength 280): string
  106.     {
  107.         if ($linkBlock === '') {
  108.             return $this->truncateToTweetLength(implode("\n\n"$bodySections), $maxLength);
  109.         }
  110.         $sectionIndex 0;
  111.         $safety 500;
  112.         while ($safety-- > 0) {
  113.             $body implode("\n\n"array_filter($bodySections, static fn (string $section): bool => $section !== ''));
  114.             $message $body === '' $linkBlock $body "\n\n" $linkBlock;
  115.             if ($this->getTweetLength($message) <= $maxLength) {
  116.                 return $message;
  117.             }
  118.             if ($sectionIndex >= count($bodySections)) {
  119.                 return $linkBlock;
  120.             }
  121.             if ($bodySections[$sectionIndex] === '') {
  122.                 ++$sectionIndex;
  123.                 continue;
  124.             }
  125.             $currentLength mb_strlen($bodySections[$sectionIndex]);
  126.             if ($currentLength <= 40) {
  127.                 $bodySections[$sectionIndex] = '';
  128.                 ++$sectionIndex;
  129.                 continue;
  130.             }
  131.             $bodySections[$sectionIndex] = $this->truncateText($bodySections[$sectionIndex], $currentLength 30);
  132.         }
  133.         return $linkBlock;
  134.     }
  135.     private function truncateToTweetLength(string $textint $maxLength): string
  136.     {
  137.         if ($this->getTweetLength($text) <= $maxLength) {
  138.             return $text;
  139.         }
  140.         $length mb_strlen($text);
  141.         while ($length && $this->getTweetLength($this->truncateText($text$length)) > $maxLength) {
  142.             $length -= 10;
  143.         }
  144.         return $this->truncateText($text$length);
  145.     }
  146.     private function truncateText(string $textint $maxLength): string
  147.     {
  148.         if ($maxLength <= 0) {
  149.             return '';
  150.         }
  151.         if (mb_strlen($text) <= $maxLength) {
  152.             return $text;
  153.         }
  154.         if ($maxLength === 1) {
  155.             return '…';
  156.         }
  157.         return rtrim(mb_substr($text0$maxLength 1)) . '…';
  158.     }
  159.     private function getTweetLength(string $text): int
  160.     {
  161.         $length mb_strlen($text);
  162.         if (preg_match_all('#https?://\S+#ui'$text$matches)) {
  163.             foreach ($matches[0] as $url) {
  164.                 $length -= mb_strlen($url);
  165.                 $length += 23;
  166.             }
  167.         }
  168.         return $length;
  169.     }
  170.     private function postTweetWithOptionalMedia(Report $reportstring $tweetMessage): array
  171.     {
  172.         $mediaPaths $this->resolveTwitterMediaPaths($report);
  173.         if ($mediaPaths === []) {
  174.             return $this->twitterService->postTweet($tweetMessage);
  175.         }
  176.         // Prefer video when present (Twitter does not allow mixing video + images).
  177.         foreach ($mediaPaths as $mediaPath) {
  178.             $mimeType mime_content_type($mediaPath) ?: '';
  179.             if (str_starts_with($mimeType'video/')) {
  180.                 return $this->twitterService->uploadVideo($mediaPath$tweetMessage);
  181.             }
  182.         }
  183.         $imagePaths = [];
  184.         foreach ($mediaPaths as $mediaPath) {
  185.             $mimeType mime_content_type($mediaPath) ?: '';
  186.             if (str_starts_with($mimeType'image/')) {
  187.                 $imagePaths[] = $mediaPath;
  188.             }
  189.         }
  190.         if ($imagePaths !== []) {
  191.             return $this->twitterService->uploadMedia($imagePaths$tweetMessage);
  192.         }
  193.         $this->logger->warning('Alert forecast Twitter attachment is not an image or video; posting text only.', [
  194.             'reportId' => $report->getId(),
  195.         ]);
  196.         return $this->twitterService->postTweet($tweetMessage);
  197.     }
  198.     /**
  199.      * @return string[] Readable local media paths (report media first, then ReportType xPostImage).
  200.      */
  201.     private function resolveTwitterMediaPaths(Report $report): array
  202.     {
  203.         $paths = [];
  204.         $seen = [];
  205.         $primaryAsset $report->getTwitterImage() ?? $report->getAsset();
  206.         foreach ([$primaryAsset$report->getReportType()?->getXPostImage()] as $asset) {
  207.             if (!$asset) {
  208.                 continue;
  209.             }
  210.             $path PIMCORE_PROJECT_ROOT '/public/var/assets' $asset->getPath() . $asset->getFilename();
  211.             if (!is_readable($path) || isset($seen[$path])) {
  212.                 continue;
  213.             }
  214.             $seen[$path] = true;
  215.             $paths[] = $path;
  216.         }
  217.         return $paths;
  218.     }
  219.     private function shouldPostToTwitter($report): bool
  220.     {
  221.         return ($report instanceof Report) &&
  222.             $report->getDraftOf() === null &&
  223.             ReportModerationService::isReportApproved($report) &&
  224.             $report->isPublished(true) &&
  225.             $report->getForTwitterChannel() == true &&
  226.             $report->getReportType()?->getReportKey() == ALERT_FORECAST_REPORT_TYPE_KEY &&
  227.             empty($report->getTwitterId()) &&
  228.             empty($report->getTwitterLog());
  229.     }
  230. }