1 <?php 2 3 namespace App\Component\AppleSearch; 4 5 use App\Entity\AppleSearchAccount; 6 use App\Exception\AppleSearchException; 7 use Firebase\JWT\JWT; 8 use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; 9 use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; 10 use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; 11 use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 12 use Symfony\Contracts\HttpClient\HttpClientInterface; 13 14 class AppleSearchTokenBuilder 15 { 16 private const AUDIENCE = 'https://appleid.apple.com'; 17 18 private const EXPIRE = 86400 * 180; 19 20 private const ALGORITHM = 'ES256'; 21 22 private const GRANT_TYPE = 'client_credentials'; 23 24 private const SCOPE = 'searchadsorg'; 25 26 private const ENDPOINT = 'https://appleid.apple.com/auth/oauth2/token'; 27 28 public function __construct(private HttpClientInterface $client) {} 29 30 public function getToken(AppleSearchAccount $searchAccount): string 31 { 32 try { 33 $response = $this->client->request('POST', self::ENDPOINT, [ 34 'body' => [ 35 'client_id' => $searchAccount->getClientID(), 36 'client_secret' => $this->generateSecret($searchAccount), 37 'grant_type' => self::GRANT_TYPE, 38 'scope' => self::SCOPE, 39 ], 40 ]); 41 42 if (200 !== $response->getStatusCode()) { 43 throw new \Exception('Return status code: ' . $response->getStatusCode()); 44 } 45 46 return json_decode($response->getContent(), true)['access_token'] ?? ''; 47 } catch (\Exception | TransportExceptionInterface | RedirectionExceptionInterface | ServerExceptionInterface | ClientExceptionInterface $ex) { 48 throw new AppleSearchException($ex->getMessage()); 49 } 50 } 51 52 private function generateSecret(AppleSearchAccount $searchAccount): string 53 { 54 $payload = [ 55 'iss' => $searchAccount->getTeamID(), 56 'aud' => self::AUDIENCE, 57 'sub' => $searchAccount->getClientID(), 58 'lat' => \time(), 59 'exp' => \time() + self::EXPIRE, 60 ]; 61 $keyID = $searchAccount->getKeyId(); 62 63 return JWT::encode($payload, $searchAccount->getCert(), self::ALGORITHM, $keyID); 64 } 65 }
1 <?php 2 3 namespace App\Components\AppleSearch; 4 5 use App\Entity\AppleSearchAccount; 6 use App\Exception\AppleSearchException; 7 use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; 8 use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; 9 use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; 10 use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 11 use Symfony\Contracts\HttpClient\HttpClientInterface; 12 13 class AppleSearchAdClient 14 { 15 16 public const TIME_FORMAT = 'Y-m-d'; 17 18 /** 19 * @var int 20 */ 21 private $maxReplaceRequest = 5; 22 23 public function __construct( 24 private readonly HttpClientInterface $client, 25 private readonly AppleSearchTokenBuilder $tokenBuilder 26 ) {} 27 28 public function getCampaignsList(AppleSearchAccount $account): string 29 { 30 try { 31 $response = $this->client->request( 32 'GET', 33 'https://api.searchads.apple.com/api/v4/campaigns?' . http_build_query(['limit' => 100]), 34 [ 35 'auth_bearer' => $account->getToken(), 36 'headers' => [ 37 'X-AP-Context' => "orgId={$account->getOrgID()}", 38 'Content-Type' => 'application/json', 39 ], 40 ] 41 ); 42 43 if (200 === $response->getStatusCode()) { 44 return $response->getContent(); 45 } 46 if ($this->maxReplaceRequest > 0 && 401 === $response->getStatusCode()) { 47 --$this->maxReplaceRequest; 48 49 return $this->getCampaignsList($this->updateToken($account)); 50 } 51 52 throw new AppleSearchException('Response status: ' . $response->getStatusCode() . ". {$response->getContent()}"); 53 } catch (TransportExceptionInterface | ClientExceptionInterface | RedirectionExceptionInterface | ServerExceptionInterface $e) { 54 throw new AppleSearchException($e->getMessage()); 55 } 56 } 57 58 private function updateToken(AppleSearchAccount $account): AppleSearchAccount 59 { 60 $token = $this->tokenBuilder->getToken($account); 61 $account->setToken($token); 62 63 return $account; 64 } 65 }