Completed
Push — master ( da3286...1b063a )
by Vladimir
04:02
created

UrlQuery::buildQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 2
1
<?php
2
3
/**
4
 * This file contains the UrlQuery class which is a wrapper for cURL
5
 *
6
 * @copyright 2015 Vladimir Jimenez
7
 * @license   https://github.com/allejo/PhpSoda/blob/master/LICENSE.md MIT
8
 */
9
10
namespace allejo\Socrata\Utilities;
11
12
use allejo\Socrata\Exceptions\CurlException;
13
use allejo\Socrata\Exceptions\HttpException;
14
use allejo\Socrata\Exceptions\SodaException;
15
16
/**
17
 * A wrapper class for working with cURL requests.
18
 *
19
 * This class configures cURL with all of the appropriate authentication information and proper cURL configuration for
20
 * processing requests.
21
 *
22
 * There's no need to access this class outside of this library as the appropriate functionality is properly wrapped in
23
 * the SodaDataset class.
24
 *
25
 * @package allejo\Socrata\Utilities
26
 * @since   0.1.0
27
 */
28
class UrlQuery
29
{
30
    /**
31
     * The default protocol the Soda API expects
32
     */
33
    const DEFAULT_PROTOCOL = "https";
34
35
    /**
36
     * The API endpoint that will be used in all requests
37
     *
38
     * @var string
39
     */
40
    private $url;
41
42
    /**
43
     * The cURL object this class is a wrapper for
44
     *
45
     * @var resource
46
     */
47
    private $cURL;
48
49
    /**
50
     * The Socrata API token
51
     *
52
     * @var string
53
     */
54
    private $token;
55
56
    /**
57
     * HTTP headers sent in all requests
58
     *
59
     * @var string[]
60
     */
61
    private $headers;
62
63
    /**
64
     * The OAuth 2.0 token sent in all requests
65
     *
66
     * @var string
67
     */
68
    private $oAuth2Token;
69
70
    /**
71
     * Configure all of the authentication needed for cURL requests and the API endpoint
72
     *
73
     * **Note** If OAuth 2.0 is used for authentication, do not give values to the $email and $password parameters;
74
     * instead, use the `setOAuth2Token()` function. An API token will still be required to bypass throttling.
75
     *
76
     * @param string $url      The API endpoint this instance will be calling
77
     * @param string $token    The API token used in order to bypass throttling
78
     * @param string $email    The email address of the user being authenticated through Basic Authentication
79
     * @param string $password The password for the user being authenticated through Basic Authentication
80
     *
81
     * @see   setOAuth2Token
82
     *
83
     * @since 0.1.0
84
     */
85
    public function __construct ($url, $token = "", $email = "", $password = "")
86
    {
87
        $this->url   = $url;
88
        $this->token = $token;
89
        $this->cURL  = curl_init();
90
91
        // Build up the headers we'll need to pass
92
        $this->headers = array(
93
                             'Accept: application/json',
94
                             'Content-type: application/json',
95
                             'X-App-Token: ' . $this->token
96
                         );
97
98
        $this->configureCurl($email, $password);
99
    }
100
101
    /**
102
     * Clean up after ourselves; clean up the cURL object.
103
     */
104
    public function __destruct ()
105
    {
106
        curl_close($this->cURL);
107
    }
108
109
    /**
110
     * Set the OAuth 2.0 token that requests will be using. This function does **not** retrieve a token, it simply uses
111
     * the existing token and sends it as authentication.
112
     *
113
     * @param string $token The OAuth 2.0 token used in requests
114
     *
115
     * @since 0.1.2
116
     */
117
    public function setOAuth2Token ($token)
118
    {
119
        if (!StringUtilities::isNullOrEmpty($token))
120
        {
121
            $this->oAuth2Token = $token;
122
            $this->headers[]   = "Authorization: OAuth " . $this->oAuth2Token;
123
        }
124
    }
125
126
    /**
127
     * Send a GET request
128
     *
129
     * @param  string $params           The GET parameters to be appended to the API endpoint
130
     * @param  bool   $associativeArray When true, the returned data will be associative arrays; otherwise, it'll be an
131
     *                                  StdClass object.
132
     * @param  array  $headers          An array where the return HTTP headers will be stored
0 ignored issues
show
Documentation introduced by
Should the type for parameter $headers not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
133
     *
134
     * @see    SodaClient::enableAssociativeArrays
135
     *
136
     * @since  0.1.0
137
     *
138
     * @return mixed  An associative array matching the returned JSON result or an StdClass object
139
     */
140
    public function sendGet ($params, $associativeArray, &$headers = NULL)
141
    {
142
        if (is_array($params))
143
        {
144
            $parameters = self::formatParameters($params);
145
            $full_url   = self::buildQuery($this->url, $parameters);
146
        }
147
        else if (!empty($params))
148
        {
149
            $full_url = $this->url . "?" . $params;
150
        }
151
        else
152
        {
153
            $full_url = $this->url;
154
        }
155
156
        curl_setopt($this->cURL, CURLOPT_URL, $full_url);
157
158
        return $this->handleQuery($associativeArray, $headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by parameter $headers on line 140 can also be of type null; however, allejo\Socrata\Utilities\UrlQuery::handleQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
159
    }
160
161
    /**
162
     * Send a POST request
163
     *
164
     * @param  string $dataAsJson       The data that will be sent to Socrata as JSON
165
     * @param  bool   $associativeArray When true, the returned data will be associative arrays; otherwise, it'll be an
166
     *                                  StdClass object.
167
     * @param  array  $headers          An array where the return HTTP headers will be stored
0 ignored issues
show
Documentation introduced by
Should the type for parameter $headers not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
168
     *
169
     * @see    SodaClient::enableAssociativeArrays
170
     *
171
     * @since  0.1.0
172
     *
173
     * @return mixed  An associative array matching the returned JSON result or an StdClass object
174
     */
175
    public function sendPost ($dataAsJson, $associativeArray, &$headers = NULL)
176
    {
177
        $this->setPostFields($dataAsJson);
178
179
        curl_setopt_array($this->cURL, array(
180
            CURLOPT_POST => true,
181
            CURLOPT_CUSTOMREQUEST => "POST"
182
        ));
183
184
        return $this->handleQuery($associativeArray, $headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by parameter $headers on line 175 can also be of type null; however, allejo\Socrata\Utilities\UrlQuery::handleQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
185
    }
186
187
    /**
188
     * Send a PUT request
189
     *
190
     * @param  string $dataAsJson       The data that will be sent to Socrata as JSON
191
     * @param  bool   $associativeArray When true, the returned data will be associative arrays; otherwise, it'll be an
192
     *                                  StdClass object.
193
     * @param  array  $headers          An array where the return HTTP headers will be stored
0 ignored issues
show
Documentation introduced by
Should the type for parameter $headers not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
194
     *
195
     * @see    SodaClient::enableAssociativeArrays
196
     *
197
     * @since  0.1.0
198
     *
199
     * @return mixed  An associative array matching the returned JSON result or an StdClass object
200
     */
201
    public function sendPut ($dataAsJson, $associativeArray, &$headers = NULL)
202
    {
203
        $this->setPostFields($dataAsJson);
204
205
        curl_setopt($this->cURL, CURLOPT_CUSTOMREQUEST, "PUT");
206
207
        return $this->handleQuery($associativeArray, $headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by parameter $headers on line 201 can also be of type null; however, allejo\Socrata\Utilities\UrlQuery::handleQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
208
    }
209
210
    /**
211
     * Send a DELETE request
212
     *
213
     * @param  bool   $associativeArray When true, the returned data will be associative arrays; otherwise, it'll be an
214
     *                                  StdClass object.
215
     * @param  array  $headers          An array where the return HTTP headers will be stored
0 ignored issues
show
Documentation introduced by
Should the type for parameter $headers not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
216
     *
217
     * @see    SodaClient::enableAssociativeArrays
218
     *
219
     * @since  0.1.2
220
     *
221
     * @return mixed  An associative array matching the returned JSON result or an StdClass object
222
     */
223
    public function sendDelete ($associativeArray, &$headers = NULL)
224
    {
225
        curl_setopt($this->cURL, CURLOPT_CUSTOMREQUEST, "DELETE");
226
227
        $this->handleQuery($associativeArray, $headers, true);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by parameter $headers on line 223 can also be of type null; however, allejo\Socrata\Utilities\UrlQuery::handleQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
228
    }
229
230
    /**
231
     * Set the POST fields that will be submitted in the cURL request
232
     *
233
     * @param string $dataAsJson The data that will be sent to Socrata as JSON
234
     *
235
     * @since 0.1.0
236
     */
237
    private function setPostFields ($dataAsJson)
238
    {
239
        curl_setopt($this->cURL, CURLOPT_POSTFIELDS, $dataAsJson);
240
    }
241
242
    /**
243
     * Handle the execution of the cURL request. This function will also save the returned HTTP headers and handle them
244
     * appropriately.
245
     *
246
     * @param  bool  $associativeArray When true, the returned data will be associative arrays; otherwise, it'll be an
247
     *                                 StdClass object.
248
     * @param  array $headers          The reference to the array where the returned HTTP headers will be stored
249
     * @param  bool  $ignoreReturn     True if the returned body should be ignored
250
     *
251
     * @since  0.1.0
252
     *
253
     * @throws \allejo\Socrata\Exceptions\CurlException If cURL is misconfigured or encounters an error
254
     * @throws \allejo\Socrata\Exceptions\HttpException An HTTP status of something other 200 is returned
255
     * @throws \allejo\Socrata\Exceptions\SodaException A SODA API error is returned
256
     *
257
     * @return mixed|NULL
258
     */
259
    private function handleQuery ($associativeArray, &$headers, $ignoreReturn = false)
260
    {
261
        $result = $this->executeCurl();
262
263
        // Ignore "100 Continue" headers
264
        $continueHeader = "HTTP/1.1 100 Continue\r\n\r\n";
265
266
        if (strpos($result, $continueHeader) === 0)
267
        {
268
            $result = str_replace($continueHeader, '', $result);
269
        }
270
271
        list($header, $body) = explode("\r\n\r\n", $result, 2);
272
273
        $this->saveHeaders($header, $headers);
274
275
        if ($ignoreReturn)
276
        {
277
            return NULL;
278
        }
279
280
        $resultArray = $this->handleResponseBody($body, $result);
281
282
        return ($associativeArray) ? $resultArray : json_decode($body, false);
283
    }
284
285
    /**
286
     * Configure the cURL instance and its credentials for Basic Authentication that this instance will be working with
287
     *
288
     * @param string $email    The email for the user with Basic Authentication
289
     * @param string $password The password for the user with Basic Authentication
290
     *
291
     * @since 0.1.0
292
     */
293
    private function configureCurl ($email, $password)
294
    {
295
        curl_setopt_array($this->cURL, array(
296
            CURLOPT_URL => $this->url,
297
            CURLOPT_HEADER => true,
298
            CURLOPT_HTTPHEADER => $this->headers,
299
            CURLOPT_RETURNTRANSFER => true
300
        ));
301
302
        if (!StringUtilities::isNullOrEmpty($email) && !StringUtilities::isNullOrEmpty($password))
303
        {
304
            curl_setopt_array($this->cURL, array(
305
                CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
306
                CURLOPT_USERPWD => $email . ":" . $password
307
            ));
308
        }
309
    }
310
311
    /**
312
     * Execute the finalized cURL object that has already been configured
313
     *
314
     * @since  0.1.0
315
     *
316
     * @throws \allejo\Socrata\Exceptions\CurlException If cURL is misconfigured or encounters an error
317
     *
318
     * @return mixed
319
     */
320
    private function executeCurl ()
321
    {
322
        $result = curl_exec($this->cURL);
323
324
        if (!$result)
325
        {
326
            throw new CurlException($this->cURL);
327
        }
328
329
        return $result;
330
    }
331
332
    /**
333
     * Check for unexpected errors or SODA API errors
334
     *
335
     * @param  string $body   The body of the response
336
     * @param  string $result The unfiltered result cURL received
337
     *
338
     * @since  0.1.0
339
     *
340
     * @throws \allejo\Socrata\Exceptions\HttpException If the $body returned was not a JSON object
341
     * @throws \allejo\Socrata\Exceptions\SodaException The returned JSON object in the $body was a SODA API error
342
     *
343
     * @return mixed An associative array of the decoded JSON response
344
     */
345
    private function handleResponseBody ($body, $result)
346
    {
347
        // We somehow got a server error from Socrata without a JSON object with details
348
        if (!StringUtilities::isJson($body))
349
        {
350
            $httpCode = curl_getinfo($this->cURL, CURLINFO_HTTP_CODE);
351
352
            throw new HttpException($httpCode, $result);
353
        }
354
355
        $resultArray = json_decode($body, true);
356
357
        // We got an error JSON object back from Socrata
358
        if (array_key_exists('error', $resultArray) && $resultArray['error'])
359
        {
360
            throw new SodaException($resultArray);
361
        }
362
363
        return $resultArray;
364
    }
365
366
    /**
367
     * Handle the returned HTTP headers and save them into an array
368
     *
369
     * @param string $header  The returned HTTP headers
370
     * @param array  $headers The reference to the array where our headers will be saved
371
     *
372
     * @since 0.1.0
373
     */
374
    private function saveHeaders ($header, &$headers)
375
    {
376
        if ($headers === NULL)
377
        {
378
            return;
379
        }
380
381
        $header       = explode("\r\n", $header);
382
        $headers      = array();
383
        $headerLength = count($header);
384
385
        // The 1st element is the HTTP code, so we can safely skip it
386
        for ($i = 1; $i < $headerLength; $i++)
387
        {
388
            list($key, $val) = explode(":", $header[$i]);
389
            $headers[$key] = trim($val);
390
        }
391
    }
392
393
    /**
394
     * Build a URL with GET parameters formatted into the URL
395
     *
396
     * @param string  $url    The base URL
397
     * @param array   $params The GET parameters that need to be appended to the base URL
398
     *
399
     * @since 0.1.0
400
     *
401
     * @return string A URL with GET parameters
402
     */
403
    private static function buildQuery ($url, $params = array())
404
    {
405
        $full_url = $url;
406
407
        if (count($params) > 0)
408
        {
409
            $full_url .= "?" . implode("&", $params);
410
        }
411
412
        return $full_url;
413
    }
414
415
    /**
416
     * Format an array into a URL encoded values to be submitted in cURL requests
417
     *
418
     * **Input**
419
     *
420
     * ```php
421
     * array(
422
     *     "foo"   => "bar",
423
     *     "param" => "value"
424
     * )
425
     * ```
426
     *
427
     * **Output**
428
     *
429
     * ```php
430
     * array(
431
     *     "foo=bar",
432
     *     "param=value"
433
     * )
434
     * ```
435
     *
436
     * @param  array    $params An array containing parameter names as keys and parameter values as values in the array.
437
     *
438
     * @return string[]         A URL encoded and combined array of GET parameters to be sent
439
     */
440
    private static function formatParameters ($params)
441
    {
442
        $parameters = array();
443
444
        foreach ($params as $key => $value)
445
        {
446
            $parameters[] = rawurlencode($key) . "=" . rawurlencode($value);
447
        }
448
449
        return $parameters;
450
    }
451
}
452