Completed
Push — master ( ec36d3...7b1361 )
by Oleg
02:43
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
        {
236
            function curl_reset(&$ch)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
237
            {
238
                $ch = curl_init();
239
            }
240
        }
241
        
242 2
        curl_reset($this->curlHandle);
243 2
        curl_setopt($this->curlHandle, CURLOPT_URL, $url);
244 2
        curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, $method);        
245 2
        if (count($postData)!=0) {
246
            curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, $content);            
247
        }
248 2
        curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
249 2
        curl_setopt($this->curlHandle, CURLOPT_HEADER, true);        
250 2
        curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, $headers);
251 2
        curl_setopt($this->curlHandle, CURLINFO_HEADER_OUT, true);
252
        
253
        // Save diagnostics info.
254 2
        $this->diagnosticsInfo['request']['method'] = $method;
255 2
        $this->diagnosticsInfo['request']['url'] = $url;
256 2
        $this->diagnosticsInfo['request']['headers'] = $headers;
257 2
        $this->diagnosticsInfo['request']['content'] = $content;        
258
        
259
        // Execute HTTP request and get response.
260 2
        $result = curl_exec($this->curlHandle);                        
261 2
        if ($result === false) {
262
            $code = curl_errno($this->curlHandle);
263
            $error = curl_error($this->curlHandle);
264
            throw new Exception("Failed to send HTTP request $method '$url', " . 
265
                    "the error code was $code, error message was: '$error'", 
266
                    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...
267
        }        
268
        
269
        // Split headers and body
270 2
        $headerSize = curl_getinfo($this->curlHandle, CURLINFO_HEADER_SIZE);
271 2
        $headers = substr($result, 0, $headerSize);
272 2
        $body = substr($result, $headerSize);
273
274
        // Parse headers
275 2
        $headers = explode("\n", $headers);
276 2
        $parsedHeaders = array();
277 2
        foreach ($headers as $i=>$header) {
278 2
            if ($i==0)
279 2
                continue; // Skip first line (http code).
280 2
            $pos = strpos($header, ':');
281 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...
282 2
                $headerName = trim(strtolower(substr($header, 0, $pos)));
283 2
                $headerValue = trim(substr($header, $pos+1));
284 2
                $parsedHeaders[$headerName] = $headerValue;
285 2
            }
286 2
        }
287
        
288
        // Get HTTP code
289 2
        $info = curl_getinfo($this->curlHandle);    
290 2
        $httpCode = $info['http_code'];
291
        
292
        // Save diagnostics info.
293 2
        $this->diagnosticsInfo['response']['http_code'] = $httpCode;
294 2
        $this->diagnosticsInfo['response']['headers'] = $headers;
295 2
        $this->diagnosticsInfo['response']['content'] = $body;                       
296
        
297
        // Determine if we have rate limiting headers
298 2
        $rateLimit = null;
299 2
        $rateLimitRemaining = null;
300 2
        $rateLimitReset = null;
301 2
        if (isset($parsedHeaders['x-ratelimit-limit'])) {
302
            $rateLimit = $parsedHeaders['x-ratelimit-limit'];
303
        }
304
        
305 2
        if (isset($parsedHeaders['x-ratelimit-remaining'])) {
306
            $rateLimitRemaining = $parsedHeaders['x-ratelimit-remaining'];
307
        }
308
        
309 2
        if (isset($parsedHeaders['x-ratelimit-reset'])) {
310
            $rateLimitReset = $parsedHeaders['x-ratelimit-reset'];
311
        }        
312
        
313
        // JSON-decode payload.
314 2
        $decodedPayload = json_decode($body, true);
315 2
        if ($decodedPayload===false) {
316
            throw new Exception('Could not JSON-decode the Optimizely API response. Request was ' . 
317
                    $method . ' "' . $url . '". The response was: "' . $body . '"',
318
                    Exception::CODE_API_ERROR, array('http_code'=>$httpCode));
319
        }
320
        
321
        // Check HTTP response code.
322 2
        if ($httpCode<200 || $httpCode>299) {
323
            
324 2
            if (!isset($decodedPayload['message']) || 
325 1
                !isset($decodedPayload['code']) ||
326 2
                !isset($decodedPayload['uuid'])) {
327 1
                throw new Exception('Optimizely API responded with error code ' . $httpCode . 
328 1
                    '. Request was ' . $method . ' "' . $url . '". Response was "' . $body . '"',
329 1
                    Exception::CODE_API_ERROR, array(
330 1
                        'http_code' => $httpCode,
331 1
                        'rate_limit' => $rateLimit,
332 1
                        'rate_limit_remaining' => $rateLimitRemaining,
333 1
                        'rate_limit_reset' => $rateLimitReset,
334 1
                    ));
335
            }
336
            
337 1
            throw new Exception($decodedPayload['message'], Exception::CODE_API_ERROR, 
338
                    array(
339 1
                        'http_code'=>$decodedPayload['code'], 
340 1
                        'uuid'=>$decodedPayload['uuid'],
341 1
                        'rate_limit' => $rateLimit,
342 1
                        'rate_limit_remaining' => $rateLimitRemaining,
343
                        'rate_limit_reset' => $rateLimitReset
344 1
                    ));
345
        }        
346
        
347
        // Create Result object
348
        $result = new Result($decodedPayload, $httpCode);
349
        $result->setRateLimit($rateLimit);
350
        $result->setRateLimitRemaining($rateLimitRemaining);
351
        $result->setRateLimitReset($rateLimitReset);
352
        
353
        // Determine if we have prev/next/last page headers.
354
        if (isset($parsedHeaders['link']))
355
        {
356
            // Parse LINK header
357
            $matched = preg_match_all('/<(.+)>;\s+rel=(\w+)(,|\z)/U', 
358
                    $parsedHeaders['link'], $matches, PREG_SET_ORDER);
359
            if (!$matched) {
360
                throw new Exception('Error parsing LINK header: ' . 
361
                        $parsedHeaders['link'], Exception::CODE_API_ERROR, $httpCode);
362
            }
363
            
364
            foreach ($matches as $match) {
365
                
366
                $url = $match[1];
367
                $rel = $match[2];
368
                
369
                $matched = preg_match('/page=(\d+)/U', $url, $pageMatches);
370 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...
371
                    throw new Exception('Error extracting page argument while parsing LINK header: ' . 
372
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
373
                            array('http_code'=>$httpCode));
374
                }
375
                
376
                $pageNumber = $pageMatches[1];
377
                
378
                if ($rel=='prev') {
379
                    $result->setPrevPage($pageNumber);
380
                } else if ($rel=='next') {
381
                    $result->setNextPage($pageNumber);
382 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...
383
                    $result->setLastPage($pageNumber);
384
                } else {
385
                    throw new Exception('Unexpected rel argument while parsing LINK header: ' . 
386
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
387
                            array('http_code'=>$httpCode));
388
                }
389
            }
390
        }        
391
                
392
        // Return the result.
393
        return $result;
394
    }
395
    
396
    /**
397
     * Determines whether the access token has expired or not. Returns true if 
398
     * token has expired; false if token is valid.
399
     * @return boolean
400
     */
401 2
    private function isAccessTokenExpired() 
402
    {
403 2
        if(!isset($this->authCredentials['access_token'])) {
404
            return true; // We do not have access token.
405
        }
406
        
407 2
        if (!isset($this->authCredentials['expires_in']) || 
408 2
            !isset($this->authCredentials['access_token_timestamp'])) {
409 1
            return true; // Assume it has expired, since we can't tell for sure.
410
        } 
411
        
412 1
        $expiresIn = $this->authCredentials['expires_in'];
413 1
        $timestamp = $this->authCredentials['access_token_timestamp'];
414
        
415 1
        if ($timestamp + $expiresIn < time()) {
416
            // Access token has expired.
417 1
            return true;
418
        }
419
        
420
        // Access token is valid.
421
        return false;
422
    }
423
    
424
    /**
425
     * This method retrieves the access token by refresh token.
426
     * @return array
427
     * @throw Exception
428
     */
429 1
    private function getAccessTokenByRefreshToken()
430
    {
431 1
        if (!isset($this->authCredentials['client_id']))
432 1
            throw new Exception('OAuth 2.0 client ID is not set');
433
        
434 1
        if (!isset($this->authCredentials['client_secret']))
435 1
            throw new Exception('OAuth 2.0 client secret is not set');
436
        
437 1
        if (!isset($this->authCredentials['refresh_token']))
438 1
            throw new Exception('Refresh token is not set');
439
        
440 1
        $clientId = $this->authCredentials['client_id'];
441 1
        $clientSecret = $this->authCredentials['client_secret'];
442 1
        $refreshToken = $this->authCredentials['refresh_token'];
443
        
444 1
        $url = "https://app.optimizely.com/oauth2/token?refresh_token=$refreshToken" . 
445 1
                "&client_id=$clientId&client_secret=$clientSecret&grant_type=refresh_token";
446
        
447 1
        $response = $this->sendHttpRequest($url, array(), 'POST');
448
        $decodedJsonData = $response->getDecodedJsonData();
449
        
450
        if (!isset($decodedJsonData['access_token'])) {
451
            throw new Exception('Not found access token in response. Request URL was "' . 
452
                    $url. '". Response was "' . print_r(json_encode($decodedJsonData), true). '"',
453
                    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...
454
        }
455
        
456
        $this->authCredentials['access_token'] = $decodedJsonData['access_token'];
457
        $this->authCredentials['token_type'] = $decodedJsonData['token_type'];
458
        $this->authCredentials['expires_in'] = $decodedJsonData['expires_in'];
459
        $this->authCredentials['access_token_timestamp'] = time(); 
460
    }
461
    
462
    /**
463
     * Provides access to API services (experiments, campaigns, etc.)
464
     * @method Audiences audiences()
465
     * @method Campaigns campaigns()
466
     * @method Events events()
467
     * @method Experiment experiments()
468
     * @method Pages pages()
469
     * @method Projects projects()
470
     */
471 1
    public function __call($name, $arguments)
472
    {
473
        $allowedServiceNames = array(
474 1
            'audiences',
475 1
            'campaigns',
476 1
            'events',
477 1
            'experiments',
478 1
            'pages',
479
            'projects'
480 1
        );
481
        
482
        // Check if the service name is valid
483 1
        if (!in_array($name, $allowedServiceNames)) {
484
            throw new Exception("Unexpected service name: $name");
485
        }
486
        
487
        // Check if such service already instantiated
488 1
        if (isset($this->services[$this->apiVersion][$name])) {
489
            $service = $this->services[$this->apiVersion][$name];
490
        } else {
491
            // Instantiate the service
492 1
            $apiVersion = $this->apiVersion;
493 1
            $serviceName = ucwords($name);
494 1
            $className = "\\WebMarketingROI\\OptimizelyPHP\\Service\\$apiVersion\\$serviceName";
495 1
            $service = new $className($this); 
496 1
            $this->services[$apiVersion][$name] = $service;
497
        }
498
499 1
        return $service;
500
    }
501
    
502
    /**
503
     * Returns last HTTP request/response information (for diagnostics/debugging
504
     * purposes):
505
     *   - request
506
     *     - method
507
     *     - headers
508
     *     - content
509
     *   - response
510
     *     - http_code
511
     *     - headers
512
     *     - content
513
     * @return array
514
     */
515 1
    public function getDiagnosticsInfo()
516
    {
517 1
        return $this->diagnosticsInfo;
518
    }
519
}