├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml.dist ├── rector.php ├── src └── Admitad │ └── Api │ ├── Api.php │ ├── Exception │ ├── InvalidResponseException.php │ └── InvalidSignedRequestException.php │ ├── Iterator.php │ └── Model.php └── tests └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | /composer.lock 4 | /phpunit.xml 5 | /rector.php 6 | .cache 7 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 6 | ->setUsingCache(true) 7 | ->setCacheFile('/tmp/.php_cs.cache') 8 | ->setRules([ 9 | '@PSR12' => true, 10 | '@Symfony' => true, 11 | '@PhpCsFixer' => true, 12 | 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'trim_array_spaces' => true, 15 | 'linebreak_after_opening_tag' => true, 16 | 'no_trailing_comma_in_singleline_array' => true, 17 | 'normalize_index_brace' => true, 18 | 'trailing_comma_in_multiline' => true, 19 | 'no_empty_comment' => true, 20 | 'no_leading_namespace_whitespace' => true, 21 | 'no_multiline_whitespace_around_double_arrow' => true, 22 | 'blank_line_before_statement' => [ 23 | 'statements' => [ 24 | 'break', 25 | 'continue', 26 | 'return', 27 | 'throw', 28 | 'try', 29 | 'foreach', 30 | 'if', 31 | 'switch', 32 | 'while', 33 | ], 34 | ], 35 | 'cast_spaces' => ['space' => 'single'], 36 | 'class_definition' => ['multi_line_extends_each_single_line' => true], 37 | 'concat_space' => ['spacing' => 'one'], 38 | 'no_null_property_initialization' => true, 39 | 'object_operator_without_whitespace' => true, 40 | 'include' => true, 41 | 'class_attributes_separation' => true, 42 | 'no_unused_imports' => true, 43 | 44 | /** Format phpdoc **/ 45 | 'no_empty_phpdoc' => true, 46 | 'general_phpdoc_annotation_remove' => ['annotations' => ['author']], 47 | 'phpdoc_return_self_reference' => true, 48 | 'phpdoc_add_missing_param_annotation' => true, 49 | 'phpdoc_scalar' => true, 50 | 'phpdoc_separation' => true, 51 | 'phpdoc_trim' => true, 52 | 'phpdoc_types' => true, 53 | 'phpdoc_no_empty_return' => true, 54 | 'no_blank_lines_after_phpdoc' => true, 55 | 'phpdoc_no_alias_tag' => true, 56 | 'whitespace_after_comma_in_array' => false, 57 | 'phpdoc_no_useless_inheritdoc' => true, 58 | 59 | /** Change code **/ 60 | 'no_useless_else' => true, 61 | 'no_superfluous_elseif' => true, 62 | 'native_function_casing' => true, 63 | 'modernize_types_casting' => true, 64 | 'no_mixed_echo_print' => ['use' => 'echo'], 65 | 'single_quote' => true, 66 | 'no_alias_functions' => true, 67 | 'no_php4_constructor' => true, 68 | 'dir_constant' => true, 69 | 'function_to_constant' => true, 70 | ]) 71 | ->setFinder( 72 | PhpCsFixer\Finder::create() 73 | ->exclude([ 74 | 'vendor', 75 | ]) 76 | ->in('src') 77 | ) 78 | ; 79 | 80 | return $config; 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013 admitad GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | admitad-api 2 | ================== 3 | 4 | A PHP wrapper around the Admitad API 5 | 6 | Install 7 | ------- 8 | 9 | Install http://getcomposer.org/ and run the following command: 10 | 11 | ``` 12 | php composer.phar require admitad/api dev-master 13 | ``` 14 | 15 | Examples 16 | ------- 17 | 18 | #### Request access token 19 | 20 | * By username / password 21 | 22 | ```php 23 | $api = new \Admitad\Api\Api() 24 | $response = $api->authorizeByPassword($clientId, $clientPassword, $scope, $username, $password); 25 | $result = $api->getArrayResultFromResponse($response); // or $response->getArrayResult(); 26 | ``` 27 | * OAuth2 28 | 29 | ```php 30 | // 1 step - get oauth authorization url 31 | $api = new \Admitad\Api\Api(); 32 | $authorizeUrl = $api->getAuthorizeUrl($clientId, $redirectUri, $scope); 33 | // redirect user to authorizeUrl 34 | 35 | 36 | // 2 step - request access token by OAuth2 code returned from authorization url 37 | $response = $api->requestAccessToken($clientId, $clientSecret, $code, $redirectUri); 38 | $result = $api->getArrayResultFromResponse($response); 39 | ``` 40 | * Signed Request (for applications on apps.admitad.com) 41 | 42 | ```php 43 | $api = new \Admitad\Api\Api(); 44 | $data = $api->parseSignedRequest($signedRequest, $clientSecret); 45 | // this method throws Admitad\Api\Exception\InvalidSignedRequestException when $signedRequest is invalid 46 | ``` 47 | 48 | #### Refresh token 49 | 50 | ```php 51 | $response = $api->refreshToken($clientId, $clientSecret, $refreshToken); 52 | $result = $api->getArrayResultFromResponse($response); 53 | ``` 54 | 55 | #### Methods 56 | There are 2 common methods to communicate with api: 57 | 58 | ```php 59 | $api = new \Admitad\Api\Api($accessToken); 60 | 61 | $api->get($path, $params); 62 | $api->post($path, $params); 63 | 64 | //for example 65 | $response = $api->get('/advcampaigns/', array( 66 | 'limit' => 20, 67 | 'offset' => 0 68 | )); 69 | 70 | $result = $api->getArrayResultFromResponse($response); 71 | 72 | ``` 73 | 74 | Paginated-result methods can be iterated in this way (instead of manually call methods with different offsets) 75 | 76 | ```php 77 | $iterator = $api->getIterator('/advcampaigns/', array( 78 | 'order_by' => 'id' 79 | )); 80 | 81 | foreach ($iterator as $campaign) { 82 | // do smth with campaign 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admitad/api", 3 | "type": "library", 4 | "description": "Admitad api library", 5 | "keywords": ["admitad", "api"], 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-0": { "Admitad\\Api\\": "src/" } 9 | }, 10 | "require": { 11 | "php": ">=8.1", 12 | "psr/log": "^3.0", 13 | "guzzlehttp/guzzle": "^7.8" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^11.0.9", 17 | "friendsofphp/php-cs-fixer": "*", 18 | "rector/rector": "*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 10 | __DIR__ . '/src', 11 | ]); 12 | 13 | $rectorConfig->importNames(); 14 | 15 | $rectorConfig->sets([ 16 | SetList::DEAD_CODE, 17 | SetList::EARLY_RETURN, 18 | SetList::TYPE_DECLARATION, 19 | SetList::CODE_QUALITY, 20 | SetList::CODING_STYLE, 21 | LevelSetList::UP_TO_PHP_81, 22 | ]); 23 | 24 | $rectorConfig->skip([ 25 | SpatieEnumClassToEnumRector::class, 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Admitad/Api/Api.php: -------------------------------------------------------------------------------- 1 | accessToken; 23 | } 24 | 25 | public function setAccessToken(?string $accessToken = null): static 26 | { 27 | $this->accessToken = $accessToken; 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * @throws \Exception 34 | */ 35 | public function authorizeByPassword(string $clientId, string $clientSecret, string $scope, string $username, string $password): ResponseInterface 36 | { 37 | $query = ['client_id' => $clientId, 'grant_type' => 'password', 'username' => $username, 'password' => $password, 'scope' => $scope]; 38 | 39 | $request = new Request('POST', '/token/' . http_build_query($query)); 40 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 41 | $request = $request->withHeader('Authorization', 'Basic ' . base64_encode($clientId . ':' . $clientSecret)); 42 | 43 | return $this->send($request, false); 44 | } 45 | 46 | public function getAuthorizeUrl($clientId, $redirectUri, $scope, $responseType = 'code'): string 47 | { 48 | return $this->host . '/authorize/?' . http_build_query(['client_id' => $clientId, 'redirect_uri' => $redirectUri, 'scope' => $scope, 'response_type' => $responseType]); 49 | } 50 | 51 | /** 52 | * @throws InvalidSignedRequestException 53 | */ 54 | public function parseSignedRequest(?string $signedRequest, string $clientSecret): array 55 | { 56 | if (!$signedRequest || !str_contains($signedRequest, '.')) { 57 | throw new InvalidSignedRequestException('Invalid signed request ' . $signedRequest); 58 | } 59 | 60 | [$key, $data] = explode('.', $signedRequest); 61 | 62 | $hash = hash_hmac('sha256', $data, $clientSecret); 63 | 64 | if ($hash !== $key) { 65 | throw new InvalidSignedRequestException('Invalid signed request ' . $signedRequest); 66 | } 67 | 68 | return json_decode(base64_decode($data), true); 69 | } 70 | 71 | /** 72 | * @throws \Exception 73 | */ 74 | public function requestAccessToken(string $clientId, string $clientSecret, string $code, string $redirectUri): ResponseInterface 75 | { 76 | $query = ['code' => $code, 'client_id' => $clientId, 'client_secret' => $clientSecret, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirectUri]; 77 | 78 | $request = new Request('POST', '/token/', [], http_build_query($query)); 79 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 80 | 81 | return $this->send($request, false); 82 | } 83 | 84 | /** 85 | * @throws \Exception 86 | */ 87 | public function refreshToken(string $clientId, string $clientSecret, string $refreshToken): ResponseInterface 88 | { 89 | $query = ['refresh_token' => $refreshToken, 'client_id' => $clientId, 'client_secret' => $clientSecret, 'grant_type' => 'refresh_token']; 90 | 91 | $request = new Request('POST', '/token/', [], http_build_query($query)); 92 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 93 | 94 | return $this->send($request, false); 95 | } 96 | 97 | /** 98 | * @throws \Exception 99 | */ 100 | public function send(MessageInterface|RequestInterface $request, bool $useAuth = true): ResponseInterface 101 | { 102 | if ($useAuth) { 103 | if (null === $this->accessToken) { 104 | throw new \Exception('Access token not provided'); 105 | } 106 | 107 | $request = $request->withHeader('Authorization', 'Bearer ' . $this->accessToken); 108 | } 109 | 110 | $client = $this->createClient(); 111 | 112 | try { 113 | $response = $client->send($request); 114 | } catch (GuzzleException $ex) { 115 | throw new \Exception('Operation failed: ' . $ex->getMessage()); 116 | } 117 | 118 | return $response; 119 | } 120 | 121 | /** 122 | * @throws \Exception 123 | */ 124 | public function get(string $method, array $params = []): ResponseInterface 125 | { 126 | $resource = $method . '?' . http_build_query($params); 127 | $request = new Request('GET', $resource); 128 | 129 | return $this->send($request); 130 | } 131 | 132 | public function getIterator(string $method, array $params = [], $limit = 200): Iterator 133 | { 134 | return new Iterator($this, $method, $params, $limit); 135 | } 136 | 137 | /** 138 | * @throws \Exception 139 | */ 140 | public function post(string $method, array $params = []): ?ResponseInterface 141 | { 142 | $request = new Request('POST', $method, [], http_build_query($params)); 143 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 144 | 145 | return $this->send($request); 146 | } 147 | 148 | /** 149 | * @throws \Exception 150 | */ 151 | public function me(): ResponseInterface 152 | { 153 | return $this->get('/me/'); 154 | } 155 | 156 | /** 157 | * @throws \Exception 158 | */ 159 | public function authorizeClient(string $clientId, string $clientSecret, string $scope): ResponseInterface 160 | { 161 | $query = ['client_id' => $clientId, 'scope' => $scope, 'grant_type' => 'client_credentials']; 162 | 163 | $request = new Request( 164 | 'POST', 165 | '/token/', 166 | [], 167 | http_build_query($query) 168 | ); 169 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 170 | $request = $request->withHeader('Authorization', 'Basic ' . base64_encode($clientId . ':' . $clientSecret)); 171 | 172 | return $this->send($request, false); 173 | } 174 | 175 | /** 176 | * @throws \Exception 177 | * @throws InvalidResponseException 178 | */ 179 | public function selfAuthorize(string $clientId, string $clientSecret, string $scope): static 180 | { 181 | $r = $this->authorizeClient($clientId, $clientSecret, $scope); 182 | $data = $this->getArrayResultFromResponse($r); 183 | $accessToken = $data['access_token'] ?? null; 184 | $this->setAccessToken($accessToken); 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * @throws InvalidResponseException 191 | */ 192 | public function getArrayResultFromResponse(?ResponseInterface $response = null): array 193 | { 194 | $arrayResult = []; 195 | $content = $response->getBody()->getContents(); 196 | 197 | if ('' === $content) { 198 | return []; 199 | } 200 | 201 | $arrayResult = json_decode($content, true); 202 | 203 | if (JSON_ERROR_NONE !== json_last_error()) { 204 | throw new InvalidResponseException($content); 205 | } 206 | 207 | return $arrayResult; 208 | } 209 | 210 | public function getModelFromArrayResult(?ResponseInterface $arrayResult = null): ?Model 211 | { 212 | if (null !== $arrayResult) { 213 | return new Model($arrayResult); 214 | } 215 | 216 | return null; 217 | } 218 | 219 | protected function createClient(): Client 220 | { 221 | return new Client([ 222 | 'base_uri' => $this->host, 223 | 'timeout' => 300, 224 | ]); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Admitad/Api/Exception/InvalidResponseException.php: -------------------------------------------------------------------------------- 1 | initialized) { 25 | throw new \LogicException('Rewind first'); 26 | } 27 | 28 | return current($this->results); 29 | } 30 | 31 | /** 32 | * @throws \Exception 33 | * @throws InvalidResponseException 34 | * @throws GuzzleException 35 | */ 36 | public function next(): void 37 | { 38 | if (!$this->initialized) { 39 | throw new \LogicException('Rewind first'); 40 | } 41 | 42 | if ($this->finished) { 43 | return; 44 | } 45 | 46 | ++$this->offset; 47 | 48 | if ($this->meta['count'] <= $this->offset) { 49 | $this->finished = true; 50 | 51 | return; 52 | } 53 | 54 | if (!next($this->results)) { 55 | $this->load(); 56 | } 57 | } 58 | 59 | public function key(): ?bool 60 | { 61 | if (!$this->initialized) { 62 | throw new \LogicException('Rewind first'); 63 | } 64 | 65 | if ($this->finished) { 66 | return null; 67 | } 68 | 69 | return $this->offset; 70 | } 71 | 72 | public function valid(): bool 73 | { 74 | if (!$this->initialized) { 75 | throw new \LogicException('Rewind first'); 76 | } 77 | 78 | return !$this->finished; 79 | } 80 | 81 | /** 82 | * @throws \Exception 83 | * @throws InvalidResponseException 84 | * @throws GuzzleException 85 | */ 86 | public function rewind(): void 87 | { 88 | if ($this->initialized && 0 === $this->offset) { 89 | return; 90 | } 91 | 92 | $this->offset = 0; 93 | $this->initialized = true; 94 | $this->finished = false; 95 | $this->load(); 96 | } 97 | 98 | public function count(): int 99 | { 100 | if (!$this->initialized) { 101 | throw new \LogicException('Rewind first'); 102 | } 103 | 104 | return $this->meta['count']; 105 | } 106 | 107 | /** 108 | * @throws InvalidResponseException 109 | * @throws \Exception 110 | * @throws GuzzleException 111 | */ 112 | protected function load(): void 113 | { 114 | $response = $this->api->get($this->method, array_merge($this->params, ['limit' => $this->limit, 'offset' => $this->offset])); 115 | 116 | $result = $this->api->getArrayResultFromResponse($response); 117 | 118 | $this->meta = $result['_meta'] ?: ['limit' => $this->limit, 'offset' => $this->offset, 'count' => 0]; 119 | 120 | $this->results = $result['results']->getArrayCopy() ?: []; 121 | 122 | if ($this->meta['limit'] < $this->limit) { 123 | $this->limit = $this->meta['limit']; 124 | } 125 | 126 | if (empty($this->results)) { 127 | $this->finished = true; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Admitad/Api/Model.php: -------------------------------------------------------------------------------- 1 | $value) { 11 | if (is_array($value)) { 12 | $value = new Model($value); 13 | } 14 | 15 | $this[$key] = $value; 16 | } 17 | } 18 | } 19 | 20 | public function __get($key): mixed 21 | { 22 | return $this[$key]; 23 | } 24 | 25 | public function offsetGet($key): mixed 26 | { 27 | return $this->offsetExists($key) ? parent::offsetGet($key) : null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Admitad\Api\Tests\\', __DIR__); 17 | 18 | --------------------------------------------------------------------------------