src/Service/LinkedInApiService.php line 43

Open in your IDE?
  1. <?php
  2. // src/Service/LinkedInApiService.php
  3. namespace App\Service;
  4. use App\Repository\LinkedInTokenRepository;
  5. use GuzzleHttp\Client;
  6. use Symfony\Component\Security\Core\Security;
  7. class LinkedInApiService
  8. {
  9.     private Client $client;
  10.     private LinkedInTokenRepository $linkedInTokenRepository;
  11.     private Security $security;
  12.     private array $mimeTypes = [];
  13.     public function __construct(
  14.         LinkedInTokenRepository $linkedInTokenRepository,
  15.         Security $security
  16.     ) {
  17.         $this->client = new Client();
  18.         $this->linkedInTokenRepository $linkedInTokenRepository;
  19.         $this->security $security;
  20.     }
  21.     private function getAccessToken(string $_site)
  22.     {
  23.         $user $this->security->getUser();
  24.         if (!$user) {
  25.             throw new \RuntimeException('User not logged in.');
  26.         }
  27.         $linkedInToken $this->linkedInTokenRepository->findValidTokenByUser($user,$_site);
  28.         if (!$linkedInToken) {
  29.             throw new \RuntimeException('No valid LinkedIn token found for the user.');
  30.         }
  31.         return $linkedInToken;
  32.     }
  33.     public function publishPost(string $textstring $type, array $mediaUrls = [], string $_site): array
  34.     {
  35.         $accessToken $this->getAccessToken($_site);
  36.         $uploadedMedia $this->upload($type$mediaUrls,$_site);
  37.     
  38.         // Construire les données du post
  39.         $postData = [
  40.             'author' => 'urn:li:organization:' $accessToken->getLinkedInOrganizationId(),
  41.             'lifecycleState' => 'PUBLISHED',
  42.             'specificContent' => [
  43.                 'com.linkedin.ugc.ShareContent' => [
  44.                     'shareCommentary' => ['text' => $this->cleanHtmlForLinkedIn($text)],
  45.                     'shareMediaCategory' => $type,
  46.                     'media' => $uploadedMedia// Utilisation des URNs retournés par upload
  47.                 ],
  48.             ],
  49.             'visibility' => ['com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC'],
  50.         ];
  51.     
  52.         try {
  53.             $response $this->client->post('https://api.linkedin.com/v2/ugcPosts', [
  54.                 'headers' => [
  55.                     'Authorization' => "Bearer {$accessToken->getToken()}",
  56.                     'Content-Type' => 'application/json',
  57.                     'X-Restli-Protocol-Version' => '2.0.0',
  58.                 ],
  59.                 'json' => $postData,
  60.             ]);
  61.     
  62.             $linkedInId json_decode($response->getBody()->getContents(), true);
  63.             return  $linkedInId;
  64.         } catch (\GuzzleHttp\Exception\ClientException $e) {
  65.             return [
  66.                 'error' => true,
  67.                 'message' => $e->getResponse()->getBody()->getContents(),
  68.             ];
  69.         }
  70.     }
  71.     public function updatePost(string $existingLinkedInIdstring $textstring $type, array $mediaUrls = [], string $_site): array
  72.     {
  73.         $this->deletePost($existingLinkedInId,$_site);
  74.         return $this->publishPost($text$type$mediaUrls$_site);
  75.     }
  76.     public function deletePost(string $linkedInIdstring $_site): bool
  77.     {
  78.         $accessToken $this->getAccessToken($_site);
  79.         $urn rawurlencode($linkedInId);
  80.         $url "https://api.linkedin.com/rest/posts/{$urn}";
  81.         $response $this->client->delete($url, [
  82.                 'headers' => [
  83.                 'Authorization' => "Bearer {$accessToken->getToken()}",
  84.                 'X-Restli-Protocol-Version' => '2.0.0',
  85.                 'LinkedIn-Version' => '202306',
  86.             ],
  87.         ]);
  88.         return $response->getStatusCode() === 204;
  89.     }
  90.     public function upload(string $type, array $mediaUrls = [], string $_site): array
  91.     {
  92.         $accessToken $this->getAccessToken($_site);
  93.         $uploadedMedia = [];
  94.     
  95.         foreach ($mediaUrls as $mediaUrl) {
  96.             // Step 1: Check if the type is a document
  97.             // Handle other media types (images, videos) using the existing logic
  98.             $recipe $this->getRecipeByMediaCategory($type);
  99.             $mimeType mime_content_type($mediaUrl);
  100.             // Register upload for images/videos
  101.             $response $this->client->request('POST''https://api.linkedin.com/v2/assets?action=registerUpload', [
  102.                 'headers' => [
  103.                     'Authorization' => "Bearer {$accessToken->getToken()}",
  104.                     'Content-Type' => 'application/json',
  105.                     'X-Restli-Protocol-Version' => '2.0.0',
  106.                 ],
  107.                 'json' => [
  108.                     'registerUploadRequest' => [
  109.                         'owner' => 'urn:li:organization:' $accessToken->getLinkedInOrganizationId(),
  110.                         'recipes' => [$recipe],
  111.                         'serviceRelationships' => [
  112.                             [
  113.                                 'identifier' => 'urn:li:userGeneratedContent',
  114.                                 'relationshipType' => 'OWNER',
  115.                             ],
  116.                         ],
  117.                     ],
  118.                 ],
  119.             ]);
  120.             $data json_decode($response->getBody()->getContents(), true);
  121.             if (!isset($data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'])) {
  122.                 throw new \RuntimeException('uploadUrl is missing in LinkedIn API response: ' json_encode($data));
  123.             }
  124.             $uploadUrl $data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'];
  125.             $asset $data['value']['asset']; // URN of the media
  126.             // Upload the file
  127.             $this->client->request('PUT'$uploadUrl, [
  128.                 'headers' => [
  129.                     'Authorization' => "Bearer {$accessToken->getToken()}",
  130.                     'Content-Type' => $mimeType,
  131.                 ],
  132.                 'body' => file_get_contents($mediaUrl),
  133.             ]);
  134.             $uploadedMedia[] = [
  135.                 'status' => 'READY',
  136.                 'media' => $asset// Use the asset URN
  137.             ];
  138.         }
  139.     
  140.         return $uploadedMedia;
  141.     }
  142.     private function getRecipeByMediaCategory(string $categoryType): string
  143.     {
  144.         switch ($categoryType) {
  145.             case 'IMAGE':
  146.                 return 'urn:li:digitalmediaRecipe:feedshare-image';
  147.             case 'VIDEO':
  148.                 return 'urn:li:digitalmediaRecipe:feedshare-video';
  149.             default:
  150.                 throw new \RuntimeException("Unsupported media type: $categoryType");
  151.         }
  152.     }
  153.     function cleanHtmlForLinkedIn(string $html): string
  154.     {
  155.         // 1. Remplacer les balises <p> par des doubles sauts de ligne
  156.         $html preg_replace('#<p>(.*?)</p>#s'"$1\n\n"$html);
  157.         // 2. Remplacer les balises <br> par des sauts de ligne
  158.         $html preg_replace('#<br\s*/?>#i'"\n"$html);
  159.         // 3. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
  160.         $html preg_replace('#<b>(.*?)</b>#s'"$1"$html); // Supprimer <ul>
  161.         // 4. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
  162.         $html preg_replace('#<i>(.*?)</i>#s'"$1"$html); // Supprimer <ul>
  163.         // 5. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
  164.         $html preg_replace('#<ul>(.*?)</ul>#s'"$1"$html); // Supprimer <ul>
  165.         $html preg_replace('#<li>(.*?)</li>#s'"- $1\n"$html); // Préfixer les <li> avec un tiret
  166.         // 6. Remplacer les balises <a> par leur texte avec l'URL entre parenthèses
  167.         $html preg_replace_callback('#<a\s+href=["\'](.*?)["\'].*?>(.*?)</a>#s', function ($matches) {
  168.             return $matches[2] . ' (' $matches[1] . ')';
  169.         }, $html);
  170.         // 7. Supprimer les balises spécifiques aux émojis <span class="ql-emojiblot">
  171.         $html preg_replace('#<span\s+class=["\']ql-emojiblot["\'].*?>\s*<span[^>]*>(.*?)</span>\s*</span>#s''$1'$html);
  172.         $html preg_replace('#</span>#i'''$html);
  173.         // 8. Supprimer les lignes vides multiples
  174.         $html preg_replace("/\n{3,}/""\n\n"trim($html)); 
  175.         return $html;
  176.     }
  177. }