Completed
Push — master ( 7b1361...074054 )
by Oleg
02:24
created

OptimizelyApiClient.php ➔ curl_reset()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @abstract API client for Optimizely.
4
 * @author Oleg Krivtsov <[email protected]>
5
 * @date 03 October 2016
6
 * @copyright (c) 2016, Web Marketing ROI
7
 */
8
namespace WebMarketingROI\OptimizelyPHP;
9
10
use WebMarketingROI\OptimizelyPHP\Exception;
11
use WebMarketingROI\OptimizelyPHP\Result;
12
13
/**
14
 * Client for Optimizely REST API v2.
15
 */
16
class OptimizelyApiClient 
17
{    
0 ignored issues
show
Coding Style introduced by
The opening class brace should be on a newline by itself.
Loading history...
18
    /**
19
     * Auth credentials.
20
     * @var array
21
     */
22
    private $authCredentials = array();
23
    
24
    /**
25
     * API version.
26
     * @var string
27
     */
28
    private $apiVersion;
29
    
30
    /**
31
     * CURL handle.
32
     * @var resource 
33
     */
34
    private $curlHandle;
35
    
36
    /**
37
     * Debugging information. Typically contains the last HTTP request/response.
38
     * @var type 
39
     */
40
    private $diagnosticsInfo = array();
41
    
42
    /**
43
     * Instantiated services (used internally).
44
     * @var array
45
     */
46
    private $services = array();
47
    
48
    /**
49
     * Constructor.
50
     * @param array $authCredentials Auth credentials.
51
     * @param string $apiVersion Optional. Currently supported 'v2' only.     
52
     */
53 13
    public function __construct($authCredentials, $apiVersion='v2')
54
    {
55 13
        if (!is_array($authCredentials)) {
56 2
            throw new Exception('Auth credentials must be an array');            
57
        }
58
        
59 11
        if ($apiVersion!='v2') {
60 1
            throw new Exception('Invalid API version passed');
61
        }
62
        
63 10
        $this->setAuthCredentials($authCredentials);
64 9
        $this->setApiVersion($apiVersion);
65
        
66 9
        $this->curlHandle = curl_init();
67 9
        if (!$this->curlHandle) {
68
            throw new Exception('Error initializing CURL',
69
                    Exception::CODE_CURL_ERROR);
70
        }
71 9
    }
72
    
73
    /**
74
     * Returns API version (currently it is always 'v2').
75
     * @return string
76
     */
77 1
    public function getApiVersion()
78
    {
79 1
        return $this->apiVersion;
80
    }
81
    
82
    /**
83
     * Sets API version. 
84
     * @param string $apiVersion Currently, 'v2' only.
85
     */
86 9
    public function setApiVersion($apiVersion)
87
    {
88 9
        if ($apiVersion!='v2') {
89 1
            throw new Exception('Invalid API version passed');
90
        }
91
        
92 9
        $this->apiVersion = $apiVersion;
93 9
    }
94
    
95
    /**
96
     * Returns auth credentials
97
     * @return array
98
     */
99 1
    public function getAuthCredentials()
100
    {
101 1
        return $this->authCredentials;
102
    }
103
    
104
    /**
105
     * Sets Auth credentials.
106
     * @param array $authCredentials
107
     */
108 10
    public function setAuthCredentials($authCredentials)
109
    {
110 10
        if (!is_array($authCredentials) || count($authCredentials)==0) {
111 1
            throw new Exception('Auth credentials must be an non-empty array');            
112
        }
113
        
114 9
        $this->authCredentials = $authCredentials;
115 9
    }
116
    
117
    /**
118
     * Returns access token information as array.
119
     * @return array
120
     */
121 1
    public function getAccessToken()
122
    {
123 1
        $accessToken = array();
124 1
        if (is_array($this->authCredentials)) {
125 1
            if (isset($this->authCredentials['access_token'])) {
126 1
                $accessToken['access_token'] = $this->authCredentials['access_token'];
127 1
            }
128
            
129 1
            if (isset($this->authCredentials['access_token_timestamp'])) {
130 1
                $accessToken['access_token_timestamp'] = $this->authCredentials['access_token_timestamp'];
131 1
            }
132
            
133 1
            if (isset($this->authCredentials['token_type'])) {
134 1
                $accessToken['token_type'] = $this->authCredentials['token_type'];
135 1
            }
136
            
137 1
            if (isset($this->authCredentials['expires_in'])) {
138 1
                $accessToken['expires_in'] = $this->authCredentials['expires_in'];
139 1
            }
140 1
        }
141
        
142 1
        return $accessToken;
143
    }
144
    
145
    /**
146
     * Returns refresh token.
147
     * @return string
148
     */
149 2
    public function getRefreshToken()
150
    {
151 2
        if (is_array($this->authCredentials)) {
152 2
            if (isset($this->authCredentials['refresh_token'])) {
153 1
                return $this->authCredentials['refresh_token'];
154
            }
155 1
        }
156
        
157 1
        return null;
158
    }
159
    
160
    /**
161
     * Sends an HTTP request to the given URL and returns response in form of array. 
162
     * @param string $url The URL of Optimizely endpoint (relative, without host and API version).
163
     * @param array $queryParams The list of query parameters.
164
     * @param string $method HTTP method (GET or POST).
165
     * @param array $postData Data send in request body (only for POST method).
166
     * @return array Optimizely response in form of array.
167
     * @throws Exception
168
     */
169 2
    public function sendApiRequest($url, $queryParams = array(), $method='GET', 
170
            $postData = array())
171
    {
172
        // If access token has expired, try to get another one with refresh token.
173 2
        if ($this->isAccessTokenExpired() && $this->getRefreshToken()!=null) {
174 1
            $this->getAccessTokenByRefreshToken();
175
        }
176
        
177
        // Produce absolute URL
178 1
        $url = 'https://api.optimizely.com/' . $this->apiVersion . $url;
179
        
180 1
        $result = $this->sendHttpRequest($url, $queryParams, $method, $postData);
181
        
182
        return $result;
183
    }
184
    
185
    /**
186
     * Sends an HTTP request to the given URL and returns response in form of array. 
187
     * @param string $url The URL of Optimizely endpoint.
188
     * @param array $queryParams The list of query parameters.
189
     * @param string $method HTTP method (GET or POST).
190
     * @param array $postData Data send in request body (only for POST method).
191
     * @return array Optimizely response in form of array.
192
     * @throws Exception
193
     */
194 2
    private function sendHttpRequest($url, $queryParams = array(), $method='GET', 
195
            $postData = array())
196
    {
197
        // Reset diagnostics info.
198 2
        $this->diagnosticsInfo = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type object<WebMarketingROI\OptimizelyPHP\type> of property $diagnosticsInfo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
199
        
200
        // Check if CURL is initialized (it should have been initialized in 
201
        // constructor).
202 2
        if ($this->curlHandle==false) {
203
            throw new Exception('CURL is not initialized', 
204
                    Exception::CODE_CURL_ERROR);
205
        }
206
        
207 2
        if ($method!='GET' && $method!='POST' && $method!='PUT' && 
208 2
            $method!='PATCH' && $method!='DELETE') {
209
            throw new Exception('Invalid HTTP method passed: ' . $method);
210
        }
211
        
212 2
        if (!isset($this->authCredentials['access_token'])) {
213
            throw new Exception('OAuth access token is not set. You should pass ' . 
214
                    'it to the class constructor when initializing the Optimizely client.');
215
        }
216
                
217
        // Append query parameters to URL.
218 2
        if (count($queryParams)!=0) {            
219
            $query = http_build_query($queryParams);
220
            $url .= '?' . $query;
221
        }
222
        
223
        $headers = array(
224 2
            "Authorization: Bearer " . $this->authCredentials['access_token'],
225
            "Content-Type: application/json"
226 2
            );
227 2
        $content = '';
228 2
        if (count($postData)!=0) {
229
            $content = json_encode($postData);            
230
        }
231 2
        $headers[] = "Content-length:" . strlen($content);            
232
        
233
        // Set HTTP options.
234 2
        if (!function_exists('curl_reset'))
235 2
            $this->curlHandle = curl_init();            
236
        else
237 2
            curl_reset($this->curlHandle);
238
        
239 2
        curl_setopt($this->curlHandle, CURLOPT_URL, $url);
240 2
        curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, $method);        
241 2
        if (count($postData)!=0) {
242
            curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, $content);            
243
        }
244 2
        curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
245 2
        curl_setopt($this->curlHandle, CURLOPT_HEADER, true);        
246 2
        curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, $headers);
247 2
        curl_setopt($this->curlHandle, CURLINFO_HEADER_OUT, true);
248
        
249
        // Save diagnostics info.
250 2
        $this->diagnosticsInfo['request']['method'] = $method;
251 2
        $this->diagnosticsInfo['request']['url'] = $url;
252 2
        $this->diagnosticsInfo['request']['headers'] = $headers;
253 2
        $this->diagnosticsInfo['request']['content'] = $content;        
254
        
255
        // Execute HTTP request and get response.
256 2
        $result = curl_exec($this->curlHandle);                        
257 2
        if ($result === false) {
258
            $code = curl_errno($this->curlHandle);
259
            $error = curl_error($this->curlHandle);
260
            throw new Exception("Failed to send HTTP request $method '$url', " . 
261
                    "the error code was $code, error message was: '$error'", 
262
                    Exception::CODE_CURL_ERROR, $code, $error);
0 ignored issues
show
Documentation introduced by
$code is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to Exception::__construct() has too many arguments starting with $error.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
263
        }        
264
        
265
        // Split headers and body
266 2
        $headerSize = curl_getinfo($this->curlHandle, CURLINFO_HEADER_SIZE);
267 2
        $headers = substr($result, 0, $headerSize);
268 2
        $body = substr($result, $headerSize);
269
270
        // Parse headers
271 2
        $headers = explode("\n", $headers);
272 2
        $parsedHeaders = array();
273 2
        foreach ($headers as $i=>$header) {
274 2
            if ($i==0)
275 2
                continue; // Skip first line (http code).
276 2
            $pos = strpos($header, ':');
277 2
            if ($pos!=false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $pos of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
278 2
                $headerName = trim(strtolower(substr($header, 0, $pos)));
279 2
                $headerValue = trim(substr($header, $pos+1));
280 2
                $parsedHeaders[$headerName] = $headerValue;
281 2
            }
282 2
        }
283
        
284
        // Get HTTP code
285 2
        $info = curl_getinfo($this->curlHandle);    
286 2
        $httpCode = $info['http_code'];
287
        
288
        // Save diagnostics info.
289 2
        $this->diagnosticsInfo['response']['http_code'] = $httpCode;
290 2
        $this->diagnosticsInfo['response']['headers'] = $headers;
291 2
        $this->diagnosticsInfo['response']['content'] = $body;                       
292
        
293
        // Determine if we have rate limiting headers
294 2
        $rateLimit = null;
295 2
        $rateLimitRemaining = null;
296 2
        $rateLimitReset = null;
297 2
        if (isset($parsedHeaders['x-ratelimit-limit'])) {
298
            $rateLimit = $parsedHeaders['x-ratelimit-limit'];
299
        }
300
        
301 2
        if (isset($parsedHeaders['x-ratelimit-remaining'])) {
302
            $rateLimitRemaining = $parsedHeaders['x-ratelimit-remaining'];
303
        }
304
        
305 2
        if (isset($parsedHeaders['x-ratelimit-reset'])) {
306
            $rateLimitReset = $parsedHeaders['x-ratelimit-reset'];
307
        }        
308
        
309
        // JSON-decode payload.
310 2
        $decodedPayload = json_decode($body, true);
311 2
        if ($decodedPayload===false) {
312
            throw new Exception('Could not JSON-decode the Optimizely API response. Request was ' . 
313
                    $method . ' "' . $url . '". The response was: "' . $body . '"',
314
                    Exception::CODE_API_ERROR, array('http_code'=>$httpCode));
315
        }
316
        
317
        // Check HTTP response code.
318 2
        if ($httpCode<200 || $httpCode>299) {
319
            
320 2
            if (!isset($decodedPayload['message']) || 
321 1
                !isset($decodedPayload['code']) ||
322 2
                !isset($decodedPayload['uuid'])) {
323 1
                throw new Exception('Optimizely API responded with error code ' . $httpCode . 
324 1
                    '. Request was ' . $method . ' "' . $url . '". Response was "' . $body . '"',
325 1
                    Exception::CODE_API_ERROR, array(
326 1
                        'http_code' => $httpCode,
327 1
                        'rate_limit' => $rateLimit,
328 1
                        'rate_limit_remaining' => $rateLimitRemaining,
329 1
                        'rate_limit_reset' => $rateLimitReset,
330 1
                    ));
331
            }
332
            
333 1
            throw new Exception($decodedPayload['message'], Exception::CODE_API_ERROR, 
334
                    array(
335 1
                        'http_code'=>$decodedPayload['code'], 
336 1
                        'uuid'=>$decodedPayload['uuid'],
337 1
                        'rate_limit' => $rateLimit,
338 1
                        'rate_limit_remaining' => $rateLimitRemaining,
339
                        'rate_limit_reset' => $rateLimitReset
340 1
                    ));
341
        }        
342
        
343
        // Create Result object
344
        $result = new Result($decodedPayload, $httpCode);
345
        $result->setRateLimit($rateLimit);
346
        $result->setRateLimitRemaining($rateLimitRemaining);
347
        $result->setRateLimitReset($rateLimitReset);
348
        
349
        // Determine if we have prev/next/last page headers.
350
        if (isset($parsedHeaders['link']))
351
        {
352
            // Parse LINK header
353
            $matched = preg_match_all('/<(.+)>;\s+rel=(\w+)(,|\z)/U', 
354
                    $parsedHeaders['link'], $matches, PREG_SET_ORDER);
355
            if (!$matched) {
356
                throw new Exception('Error parsing LINK header: ' . 
357
                        $parsedHeaders['link'], Exception::CODE_API_ERROR, $httpCode);
358
            }
359
            
360
            foreach ($matches as $match) {
361
                
362
                $url = $match[1];
363
                $rel = $match[2];
364
                
365
                $matched = preg_match('/page=(\d+)/U', $url, $pageMatches);
366 View Code Duplication
                if (!$matched || count($pageMatches)!=2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
                    throw new Exception('Error extracting page argument while parsing LINK header: ' . 
368
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
369
                            array('http_code'=>$httpCode));
370
                }
371
                
372
                $pageNumber = $pageMatches[1];
373
                
374
                if ($rel=='prev') {
375
                    $result->setPrevPage($pageNumber);
376
                } else if ($rel=='next') {
377
                    $result->setNextPage($pageNumber);
378 View Code Duplication
                } else if ($rel=='last') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
379
                    $result->setLastPage($pageNumber);
380
                } else {
381
                    throw new Exception('Unexpected rel argument while parsing LINK header: ' . 
382
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
383
                            array('http_code'=>$httpCode));
384
                }
385
            }
386
        }        
387
                
388
        // Return the result.
389
        return $result;
390
    }
391
    
392
    /**
393
     * Determines whether the access token has expired or not. Returns true if 
394
     * token has expired; false if token is valid.
395
     * @return boolean
396
     */
397 2
    private function isAccessTokenExpired() 
398
    {
399 2
        if(!isset($this->authCredentials['access_token'])) {
400
            return true; // We do not have access token.
401
        }
402
        
403 2
        if (!isset($this->authCredentials['expires_in']) || 
404 2
            !isset($this->authCredentials['access_token_timestamp'])) {
405 1
            return true; // Assume it has expired, since we can't tell for sure.
406
        } 
407
        
408 1
        $expiresIn = $this->authCredentials['expires_in'];
409 1
        $timestamp = $this->authCredentials['access_token_timestamp'];
410
        
411 1
        if ($timestamp + $expiresIn < time()) {
412
            // Access token has expired.
413 1
            return true;
414
        }
415
        
416
        // Access token is valid.
417
        return false;
418
    }
419
    
420
    /**
421
     * This method retrieves the access token by refresh token.
422
     * @return array
423
     * @throw Exception
424
     */
425 1
    private function getAccessTokenByRefreshToken()
426
    {
427 1
        if (!isset($this->authCredentials['client_id']))
428 1
            throw new Exception('OAuth 2.0 client ID is not set');
429
        
430 1
        if (!isset($this->authCredentials['client_secret']))
431 1
            throw new Exception('OAuth 2.0 client secret is not set');
432
        
433 1
        if (!isset($this->authCredentials['refresh_token']))
434 1
            throw new Exception('Refresh token is not set');
435
        
436 1
        $clientId = $this->authCredentials['client_id'];
437 1
        $clientSecret = $this->authCredentials['client_secret'];
438 1
        $refreshToken = $this->authCredentials['refresh_token'];
439
        
440 1
        $url = "https://app.optimizely.com/oauth2/token?refresh_token=$refreshToken" . 
441 1
                "&client_id=$clientId&client_secret=$clientSecret&grant_type=refresh_token";
442
        
443 1
        $response = $this->sendHttpRequest($url, array(), 'POST');
444
        $decodedJsonData = $response->getDecodedJsonData();
445
        
446
        if (!isset($decodedJsonData['access_token'])) {
447
            throw new Exception('Not found access token in response. Request URL was "' . 
448
                    $url. '". Response was "' . print_r(json_encode($decodedJsonData), true). '"',
449
                    Exception::CODE_API_ERROR, $response->getHttpCode());
0 ignored issues
show
Documentation introduced by
$response->getHttpCode() is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
450
        }
451
        
452
        $this->authCredentials['access_token'] = $decodedJsonData['access_token'];
453
        $this->authCredentials['token_type'] = $decodedJsonData['token_type'];
454
        $this->authCredentials['expires_in'] = $decodedJsonData['expires_in'];
455
        $this->authCredentials['access_token_timestamp'] = time(); 
456
    }
457
    
458
    /**
459
     * Provides access to API services (experiments, campaigns, etc.)
460
     * @method Audiences audiences()
461
     * @method Campaigns campaigns()
462
     * @method Events events()
463
     * @method Experiment experiments()
464
     * @method Pages pages()
465
     * @method Projects projects()
466
     */
467 1
    public function __call($name, $arguments)
468
    {
469
        $allowedServiceNames = array(
470 1
            'audiences',
471 1
            'campaigns',
472 1
            'events',
473 1
            'experiments',
474 1
            'pages',
475
            'projects'
476 1
        );
477
        
478
        // Check if the service name is valid
479 1
        if (!in_array($name, $allowedServiceNames)) {
480
            throw new Exception("Unexpected service name: $name");
481
        }
482
        
483
        // Check if such service already instantiated
484 1
        if (isset($this->services[$this->apiVersion][$name])) {
485
            $service = $this->services[$this->apiVersion][$name];
486
        } else {
487
            // Instantiate the service
488 1
            $apiVersion = $this->apiVersion;
489 1
            $serviceName = ucwords($name);
490 1
            $className = "\\WebMarketingROI\\OptimizelyPHP\\Service\\$apiVersion\\$serviceName";
491 1
            $service = new $className($this); 
492 1
            $this->services[$apiVersion][$name] = $service;
493
        }
494
495 1
        return $service;
496
    }
497
    
498
    /**
499
     * Returns last HTTP request/response information (for diagnostics/debugging
500
     * purposes):
501
     *   - request
502
     *     - method
503
     *     - headers
504
     *     - content
505
     *   - response
506
     *     - http_code
507
     *     - headers
508
     *     - content
509
     * @return array
510
     */
511 1
    public function getDiagnosticsInfo()
512
    {
513 1
        return $this->diagnosticsInfo;
514
    }
515
}