Completed
Push — develop ( 6fdb58...dee5cf )
by Oleg
04:27 queued 01:45
created

OptimizelyApiClient::sendApiRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.3332

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 5
dl 0
loc 16
ccs 4
cts 6
cp 0.6667
crap 3.3332
rs 9.4285
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
/**
11
 * Client for Optimizely REST API v2.
12
 */
13
class OptimizelyApiClient 
14
{
15
    /**
16
     * Auth credentials.
17
     * @var array
18
     */
19
    private $authCredentials = array();
20
    
21
    /**
22
     * API version.
23
     * @var string
24
     */
25
    private $apiVersion;
26
    
27
    /**
28
     * CURL handle.
29
     * @var resource 
30
     */
31
    private $curlHandle;
32
    
33
    /**
34
     * Instantiated services (used internally).
35
     * @var array
36
     */
37
    private $services = array();
38
    
39
    /**
40
     * Constructor.
41
     * @param string $authCredentials Auth credentials.
42
     * @param string $apiVersion Optional. Currently supported 'v2' only.
43
     */
44 8
    public function __construct($authCredentials, $apiVersion='v2')
45
    {
46 8
        if (!is_array($authCredentials)) {
47 1
            throw new \Exception('Auth credentials must be an array');            
48
        }
49
        
50 7
        if ($apiVersion!='v2') {
51 1
            throw new \Exception('Invalid API version passed');
52
        }
53
        
54 6
        $this->authCredentials = $authCredentials;
55 6
        $this->apiVersion = $apiVersion;
56
        
57 6
        $this->curlHandle = curl_init();
58 6
        if (!$this->curlHandle) {
59
            throw new \Exception('Error initializing CURL');
60
        }
61 6
    }
62
    
63
    /**
64
     * Returns API version (currently it is always 'v2').
65
     */
66 1
    public function getApiVersion()
67
    {
68 1
        return $this->apiVersion;
69
    }
70
    
71
    /**
72
     * Sets API version. 
73
     * @param string $apiVersion Currently, 'v2' only.
74
     */
75 1
    public function setApiVersion($apiVersion)
76
    {
77 1
        if ($apiVersion!='v2') {
78
            throw new \Exception('Invalid API version passed');
79
        }
80
        
81 1
        $this->apiVersion = $apiVersion;
82 1
    }
83
    
84
    /**
85
     * Returns auth credentials
86
     * @return array
87
     */
88 1
    public function getAuthCredentials()
89
    {
90 1
        return $this->authCredentials;
91
    }
92
    
93
    /**
94
     * Sets Auth credentials.
95
     * @param array $authCredentials
96
     */
97 1
    public function setAuthCredentials($authCredentials)
98
    {
99 1
        if (!is_array($authCredentials) || count($authCredentials)==0) {
100
            throw new \Exception('Auth credentials must be an array');            
101
        }
102
        
103 1
        $this->authCredentials = $authCredentials;
104 1
    }
105
    
106
    /**
107
     * Returns access token information as array.
108
     * @return array
109
     */
110 1
    public function getAccessToken()
111
    {
112 1
        $accessToken = array();
113 1
        if (is_array($this->authCredentials)) {
114 1
            if (isset($this->authCredentials['access_token'])) {
115 1
                $accessToken['access_token'] = $this->authCredentials['access_token'];
116
            }
117
            
118 1
            if (isset($this->authCredentials['access_token_timestamp'])) {
119
                $accessToken['access_token_timestamp'] = $this->authCredentials['access_token_timestamp'];
120
            }
121
            
122 1
            if (isset($this->authCredentials['token_type'])) {
123
                $accessToken['token_type'] = $this->authCredentials['token_type'];
124
            }
125
            
126 1
            if (isset($this->authCredentials['expires_in'])) {
127
                $accessToken['expires_in'] = $this->authCredentials['expires_in'];
128
            }
129
        }
130
        
131 1
        return $accessToken;
132
    }
133
    
134
    /**
135
     * Returns refresh token.
136
     * @return string
137
     */
138 1
    public function getRefreshToken()
139
    {
140 1
        if (is_array($this->authCredentials)) {
141 1
            if (isset($this->authCredentials['refresh_token'])) {
142
                return $this->authCredentials['refresh_token'];
143
            }
144
        }
145
        
146 1
        return null;
147
    }
148
    
149
    /**
150
     * Sends an HTTP request to the given URL and returns response in form of array. 
151
     * @param string $url The URL of Optimizely endpoint (relative, without host and API version).
152
     * @param array $queryParams The list of query parameters.
153
     * @param string $method HTTP method (GET or POST).
154
     * @param array $postData Data send in request body (only for POST method).
155
     * @param array $expectedResponseCodes List of HTTP response codes treated as success.
156
     * @return array Optimizely response in form of array.
157
     * @throws \Exception
158
     */
159 1
    public function sendApiRequest($url, $queryParams = array(), $method='GET', 
160
            $postData = array(), $expectedResponseCodes = array(200))
161
    {
162
        // If access token has expired, try to get another one with refresh token.
163 1
        if ($this->isAccessTokenExpired() && $this->getRefreshToken()!=null) {
164
            $this->getAccessTokenByRefreshToken();
165
        }
166
        
167
        // Produce absolute URL
168 1
        $url = 'https://api.optimizely.com/' . $this->apiVersion . $url;
169
        
170 1
        $result = $this->sendHttpRequest($url, $queryParams, $method, 
171
                $postData, $expectedResponseCodes);
172
        
173
        return $result;
174
    }
175
    
176
    /**
177
     * Sends an HTTP request to the given URL and returns response in form of array. 
178
     * @param string $url The URL of Optimizely endpoint.
179
     * @param array $queryParams The list of query parameters.
180
     * @param string $method HTTP method (GET or POST).
181
     * @param array $postData Data send in request body (only for POST method).
182
     * @param array $expectedResponseCodes List of HTTP response codes treated as success.
183
     * @return array Optimizely response in form of array.
184
     * @throws \Exception
185
     */
186 1
    private function sendHttpRequest($url, $queryParams = array(), $method='GET', 
187
            $postData = array(), $expectedResponseCodes = array(200))
188
    {
189
        // Check if CURL is initialized (it should have been initialized in 
190
        // constructor).
191 1
        if ($this->curlHandle==false) {
192
            throw new \Exception('CURL is not initialized', -1);
193
        }
194
        
195 1
        if ($method!='GET' && $method!='POST' && $method!='PUT' && 
196 1
            $method!='PATCH' && $method!='DELETE') {
197
            throw new \Exception('Invalid HTTP method passed: ' . $method, -1);
198
        }
199
        
200 1
        if (!isset($this->authCredentials['access_token'])) {
201
            throw new \Exception('OAuth access token is not set. You should pass ' . 
202
                    'it to the class constructor when initializing the Optimizely client.', -1);
203
        }
204
                
205
        // Append query parameters to URL.
206 1
        if (count($queryParams)!=0) {            
207
            $query = http_build_query($queryParams);
208
            $url .= '?' . $query;
209
        }
210
        
211
        $headers = array(
212 1
            "Authorization: Bearer " . $this->authCredentials['access_token'],
213 1
            "Content-Type: application/json"
214
            );
215 1
        $content = '';
216 1
        if (count($postData)!=0) {
217
            $content = json_encode($postData);            
218
        }
219 1
        $headers[] = "Content-length:" . strlen($content);            
220
        
221
        // Set HTTP options.
222 1
        curl_setopt($this->curlHandle, CURLOPT_URL, $url);
223 1
        curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, $method);        
224 1
        if (count($postData)!=0) {
225
            curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, $content);            
226
        }
227 1
        curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
228 1
        curl_setopt($this->curlHandle, CURLOPT_HEADER, false);        
229 1
        curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, $headers);
230
        
231
        // Execute HTTP request and get response.
232 1
        $result = curl_exec($this->curlHandle);
233 1
        if ($result === false) {
234
            $code = curl_errno($this->curlHandle);
235
            $error = curl_error($this->curlHandle);
236
            throw new \Exception("Failed to send HTTP request $method '$url', the error code was $code, error message was: '$error'", -1);
237
        }        
238
        
239
        // Check HTTP response code.
240 1
        $info = curl_getinfo($this->curlHandle);
241 1
        if (!in_array($info['http_code'], $expectedResponseCodes)) {
242 1
            throw new \Exception('Unexpected HTTP response code: ' . $info['http_code'] . 
243 1
                    '. Request was ' . $method . ' "' . $url . '". Response was "' . $result . '"',
244 1
                    $info['http_code']);
245
        }
246
        
247
        // JSON-decode response.
248
        $decodedResult = json_decode($result, true);
249
        if (!is_array($decodedResult)) {
250
            throw new \Exception('Could not JSON-decode the Optimizely response. Request was ' . 
251
                    $method . ' "' . $url . '". The response was: "' . $result . '"',
252
                    $info['http_code']);
253
        }
254
        
255
        // Return the response in form of array.
256
        return $decodedResult;
257
    }
258
    
259
    /**
260
     * Determines whether the access token has expired or not. Returns true if 
261
     * token has expired; false if token is valid.
262
     * @return boolean
263
     */
264 1
    private function isAccessTokenExpired() 
265
    {
266 1
        if(!isset($this->authCredentials['access_token'])) {
267
            return true; // We do not have access token.
268
        }
269
        
270 1
        if (!isset($this->authCredentials['expires_in']) || 
271 1
            !isset($this->authCredentials['access_token_timestamp'])) {
272 1
            return true; // Assume it has expired, since we can't tell for sure.
273
        } 
274
        
275
        $expiresIn = $this->authCredentials['expires_in'];
276
        $timestamp = $this->authCredentials['access_token_timestamp'];
277
        
278
        if ($timestamp + $expiresIn < time()) {
279
            // Access token has expired.
280
            return true;
281
        }
282
        
283
        // Access token is valid.
284
        return false;
285
    }
286
    
287
    /**
288
     * This method retrieves the access token by refresh token.
289
     */
290
    private function getAccessTokenByRefreshToken()
291
    {
292
        if (!isset($this->authCredentials['client_id']))
293
            throw new \Exception('OAuth 2.0 client ID is not set');
294
        
295
        if (!isset($this->authCredentials['client_secret']))
296
            throw new \Exception('OAuth 2.0 client secret is not set');
297
        
298
        if (!isset($this->authCredentials['refresh_token']))
299
            throw new \Exception('Refresh token is not set');
300
        
301
        $clientId = $this->authCredentials['client_id'];
302
        $clientSecret = $this->authCredentials['client_secret'];
303
        $refreshToken = $this->authCredentials['refresh_token'];
304
        
305
        $url = "https://app.optimizely.com/oauth2/token?refresh_token=$refreshToken&client_id=$clientId&client_secret=$clientSecret&grant_type=refresh_token";
306
        
307
        $response = $this->sendHttpRequest($url, array(), 'POST');
308
        
309
        if (!isset($response['access_token'])) {
310
            throw new \Exception('Not found access token in response. Request URL was "' . $url. '". Response was "' . print_r($response, true). '"');
311
        }
312
        
313
        $this->authCredentials['access_token'] = $response['access_token'];
314
        $this->authCredentials['token_type'] = $response['token_type'];
315
        $this->authCredentials['expires_in'] = $response['expires_in'];
316
        $this->authCredentials['access_token_timestamp'] = time(); 
317
    }
318
    
319
    /**
320
     * Provides access to API services (experiments, campaigns, etc.)
321
     */
322 1
    public function __call($name, $arguments)
323
    {
324
        $allowedServiceNames = array(
325 1
            'audiences',
326
            'campaigns',
327
            'events',
328
            'experiments',
329
            'pages',
330
            'projects'
331
        );
332
        
333
        // Check if the service name is valid
334 1
        if (!in_array($name, $allowedServiceNames)) {
335
            throw new \Exception("Unexpected service name: $name");
336
        }
337
        
338
        // Check if such service already instantiated
339 1
        if (isset($this->services[$this->apiVersion][$name])) {
340
            $service = $this->services[$this->apiVersion][$name];
341
        } else {
342
            // Instantiate the service
343 1
            $apiVersion = $this->apiVersion;
344 1
            $serviceName = ucwords($name);
345 1
            $className = "\\WebMarketingROI\\OptimizelyPHP\\Service\\$apiVersion\\$serviceName";
346 1
            $service = new $className($this); 
347 1
            $this->services[$apiVersion][$name] = $service;
348
        }
349
350 1
        return $service;
351
    }
352
}