Completed
Push — master ( 16cac1...c4d6ff )
by Vladimir
02:22
created

SodaDataset::getMetadata()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 2
Metric Value
c 4
b 0
f 2
dl 0
loc 12
rs 9.4285
cc 3
eloc 6
nc 2
nop 1
1
<?php
2
3
/**
4
 * This file contains the SodaDataset class
5
 *
6
 * @copyright 2015 Vladimir Jimenez
7
 * @license   https://github.com/allejo/PhpSoda/blob/master/LICENSE.md MIT
8
 */
9
10
namespace allejo\Socrata;
11
12
use allejo\Socrata\Converters\Converter;
13
use allejo\Socrata\Exceptions\InvalidResourceException;
14
use allejo\Socrata\Utilities\StringUtilities;
15
use allejo\Socrata\Utilities\UrlQuery;
16
17
/**
18
 * An object provided to interact with a Socrata dataset directly. Provides functionality for fetching the dataset, an
19
 * individual row, or updating/replacing a dataset.
20
 *
21
 * @package allejo\Socrata
22
 * @since   0.1.0
23
 */
24
class SodaDataset
25
{
26
    /**
27
     * The client with all the authentication and configuration set
28
     *
29
     * @var SodaClient
30
     */
31
    private $sodaClient;
32
33
    /**
34
     * The object used to make URL jobs for common requests
35
     *
36
     * @var UrlQuery
37
     */
38
    private $urlQuery;
39
40
    /**
41
     * The 4x4 resource ID of a dataset
42
     *
43
     * @var string
44
     */
45
    private $resourceId;
46
47
    /**
48
     * The API version of the dataset being worked with
49
     *
50
     * @var int
51
     */
52
    private $apiVersion;
53
54
    /**
55
     * The API's cached metadata
56
     *
57
     * @var array
58
     */
59
    private $metadata;
60
61
    /**
62
     * Create an object for interacting with a Socrata dataset
63
     *
64
     * @param  SodaClient $sodaClient The SodaClient with all of the authentication information and settings for access
65
     * @param  string     $resourceID The 4x4 resource ID of the dataset that will be referenced
66
     *
67
     * @throws InvalidResourceException If the given resource ID does not match the pattern of a resource ID
68
     *
69
     * @since 0.1.0
70
     */
71
    public function __construct ($sodaClient, $resourceID)
72
    {
73
        StringUtilities::validateResourceID($resourceID);
74
75
        if (!($sodaClient instanceof SodaClient))
76
        {
77
            throw new \InvalidArgumentException("The first variable is expected to be a SodaClient object");
78
        }
79
80
        $this->apiVersion = 0;
81
        $this->sodaClient = $sodaClient;
82
        $this->resourceId = $resourceID;
83
        $this->urlQuery   = new UrlQuery($this->buildResourceUrl(), $this->sodaClient->getToken(), $this->sodaClient->getEmail(), $this->sodaClient->getPassword());
84
85
        $this->urlQuery->setOAuth2Token($this->sodaClient->getOAuth2Token());
86
    }
87
88
    /**
89
     * Get the API version this dataset is using
90
     *
91
     * @since  0.1.0
92
     *
93
     * @return double The API version number
0 ignored issues
show
Documentation introduced by
Should the return type not be integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
94
     */
95
    public function getApiVersion ()
96
    {
97
        // If we don't have the API version set, send a dummy query with limit 0 since we only care about the headers
98
        if ($this->apiVersion == 0)
99
        {
100
            $soql = new SoqlQuery();
101
            $soql->limit(0);
102
103
            // When we fetch a dataset, the API version is stored
104
            $this->getDataset($soql);
105
        }
106
107
        return $this->apiVersion;
108
    }
109
110
    /**
111
     * Get the metadata of a dataset
112
     *
113
     * @param bool $forceFetch Set to true if the cached metadata for the dataset is outdata or needs to be refreshed
114
     *
115
     * @see    SodaClient::enableAssociativeArrays()
116
     * @see    SodaClient::disableAssociativeArrays()
117
     *
118
     * @since  0.1.0
119
     *
120
     * @return array The metadata as a PHP array. The array will contain associative arrays or stdClass objects from
121
     *               the decoded JSON received from the data set.
122
     */
123
    public function getMetadata ($forceFetch = false)
124
    {
125
        if (empty($this->metadata) || $forceFetch)
126
        {
127
            $metadataUrlQuery = new UrlQuery($this->buildViewUrl(), $this->sodaClient->getToken(), $this->sodaClient->getEmail(), $this->sodaClient->getPassword());
128
            $metadataUrlQuery->setOAuth2Token($this->sodaClient->getOAuth2Token());
129
130
            $this->metadata = $metadataUrlQuery->sendGet("", $this->sodaClient->associativeArrayEnabled());
0 ignored issues
show
Documentation Bug introduced by
It seems like $metadataUrlQuery->sendG...ociativeArrayEnabled()) of type * is incompatible with the declared type array of property $metadata.

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...
131
        }
132
133
        return $this->metadata;
134
    }
135
136
    /**
137
     * Fetch a dataset based on a resource ID.
138
     *
139
     * @param  string|SoqlQuery $filterOrSoqlQuery A simple filter or a SoqlQuery to filter the results
140
     *
141
     * @see    SodaClient::enableAssociativeArrays()
142
     * @see    SodaClient::disableAssociativeArrays()
143
     *
144
     * @since  0.1.0
145
     *
146
     * @return array The data set as a PHP array. The array will contain associative arrays or stdClass objects from
147
     *               the decoded JSON received from the data set.
148
     */
149
    public function getDataset ($filterOrSoqlQuery = "")
150
    {
151
        $headers = array();
152
153
        if (!($filterOrSoqlQuery instanceof SoqlQuery) && StringUtilities::isNullOrEmpty($filterOrSoqlQuery))
154
        {
155
            $filterOrSoqlQuery = new SoqlQuery();
156
        }
157
158
        $dataset = $this->urlQuery->sendGet($filterOrSoqlQuery, $this->sodaClient->associativeArrayEnabled(), $headers);
0 ignored issues
show
Bug introduced by
It seems like $filterOrSoqlQuery can also be of type object<allejo\Socrata\SoqlQuery>; however, allejo\Socrata\Utilities\UrlQuery::sendGet() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
159
160
        $this->setApiVersion($headers);
0 ignored issues
show
Bug introduced by
It seems like $headers can also be of type null; however, allejo\Socrata\SodaDataset::setApiVersion() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
161
162
        return $dataset;
163
    }
164
165
    /**
166
     * Delete an individual row based on their row identifier. For deleting more than a single row, use an upsert
167
     * instead.
168
     *
169
     * @param  int|string $rowID The row identifier of the row to fetch; if no identifier is set for the dataset, the
170
     *                           internal row identifier should be used
171
     *
172
     * @link   http://dev.socrata.com/publishers/direct-row-manipulation.html#deleting-a-row Deleting a Row
173
     *
174
     * @see    SodaClient::enableAssociativeArrays()
175
     * @see    SodaClient::disableAssociativeArrays()
176
     * @see    upsert()
177
     *
178
     * @since  0.1.2
179
     *
180
     * @return mixed An object with information about the deletion. The array will contain associative arrays or
181
     *               stdClass objects from the decoded JSON received from the data set.
182
     */
183
    public function deleteRow ($rowID)
184
    {
185
        return $this->individualRow($rowID, "delete");
186
    }
187
188
    /**
189
     * Fetch an individual row from a dataset.
190
     *
191
     * @param  int|string $rowID The row identifier of the row to fetch; if no identifier is set for the dataset, the
192
     *                           internal row identifier should be used
193
     *
194
     * @link   http://dev.socrata.com/publishers/direct-row-manipulation.html#retrieving-an-individual-row  Retrieving
195
     *         An Individual Row
196
     *
197
     * @see    SodaClient::enableAssociativeArrays()
198
     * @see    SodaClient::disableAssociativeArrays()
199
     *
200
     * @since  0.1.2
201
     *
202
     * @return array The data set as a PHP array. The array will contain associative arrays or stdClass objects from
203
     *               the decoded JSON received from the data set.
204
     */
205
    public function getRow ($rowID)
206
    {
207
        return $this->individualRow($rowID, "get");
208
    }
209
210
    /**
211
     * Replace the entire dataset with the new payload provided
212
     *
213
     * Data will always be transmitted as JSON to Socrata even though different forms are accepted. In order to pass
214
     * other forms of data, you must use a Converter class that has a `toJson()` method, such as the CsvConverter.
215
     *
216
     * @param  array|Converter|JSON $payload  The data that will be upserted to the Socrata dataset as a PHP array, an
217
     *                                        instance of a Converter child class, or a JSON string
218
     *
219
     * @link   http://dev.socrata.com/publishers/replace.html Replacing a dataset with Replace
220
     *
221
     * @see    Converter
222
     * @see    CsvConverter
223
     *
224
     * @since  0.1.0
225
     *
226
     * @return mixed
227
     */
228
    public function replace ($payload)
229
    {
230
        $upsertData = $this->handleJson($payload);
231
232
        return $this->urlQuery->sendPut($upsertData, $this->sodaClient->associativeArrayEnabled());
233
    }
234
235
    /**
236
     * Create, update, and delete rows in a single operation, using their row identifiers.
237
     *
238
     * Data will always be transmitted as JSON to Socrata even though different forms are accepted. In order to pass
239
     * other forms of data, you must use a Converter class that has a `toJson()` method, such as the CsvConverter.
240
     *
241
     * @param  array|Converter|JSON $payload  The data that will be upserted to the Socrata dataset as a PHP array, an
242
     *                                        instance of a Converter child class, or a JSON string
243
     *
244
     * @link   http://dev.socrata.com/publishers/upsert.html Updating Rows in Bulk with Upsert
245
     *
246
     * @see    Converter
247
     * @see    CsvConverter
248
     *
249
     * @since  0.1.0
250
     *
251
     * @return mixed
252
     */
253
    public function upsert ($payload)
254
    {
255
        $upsertData = $this->handleJson($payload);
256
257
        return $this->urlQuery->sendPost($upsertData, $this->sodaClient->associativeArrayEnabled());
258
    }
259
260
    /**
261
     * Build the API URL that will be used to access the dataset
262
     *
263
     * @return string The apt API URL
264
     */
265
    private function buildResourceUrl ()
266
    {
267
        return $this->buildApiUrl("resource");
268
    }
269
270
    /**
271
     * Build the API URL that will be used to access the metadata for the dataset
272
     *
273
     * @return string The apt API URL
274
     */
275
    private function buildViewUrl ()
276
    {
277
        return $this->buildApiUrl("views");
278
    }
279
280
    /**
281
     * Build the URL that will be used to access the API for the respective action
282
     *
283
     * @param  string      $location    The location of where to get information from
284
     * @param  string|null $identifier  The part of the URL that will end with .json. This will either be the resource
285
     *                                  ID or it will be a row ID prepended with the resource ID
286
     *
287
     * @return string The API URL
288
     */
289
    private function buildApiUrl ($location, $identifier = NULL)
290
    {
291
        if ($identifier === NULL)
292
        {
293
            $identifier = $this->resourceId;
294
        }
295
296
        return sprintf("%s://%s/%s/%s.json", UrlQuery::DEFAULT_PROTOCOL, $this->sodaClient->getDomain(), $location, $identifier);
297
    }
298
299
    /**
300
     * Handle different forms of data to be returned in JSON format so it can be sent to Socrata.
301
     *
302
     * Data will always be transmitted as JSON to Socrata even though different forms are accepted.
303
     *
304
     * @param  array|Converter|JSON $payload  The data that will be upserted to the Socrata dataset as a PHP array, an
305
     *                                        instance of a Converter child class, or a JSON string
306
     *
307
     * @return string A JSON encoded string available to be used for UrlQuery requsts
0 ignored issues
show
Documentation introduced by
Should the return type not be array|Converter|JSON|string? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
308
     */
309
    private function handleJson ($payload)
310
    {
311
        $uploadData = $payload;
312
313
        if (is_array($payload))
314
        {
315
            $uploadData = json_encode($payload);
316
        }
317
        else if ($payload instanceof Converter)
318
        {
319
            $uploadData = $payload->toJson();
320
        }
321
        else if (!StringUtilities::isJson($payload))
322
        {
323
            throw new \InvalidArgumentException("The given data is not valid JSON");
324
        }
325
326
        return $uploadData;
327
    }
328
329
    /**
330
     * Interact with an individual row. Either to retrieve it or to delete it; both actions use the same API endpoint
331
     * with the exception of what type of request is sent.
332
     *
333
     * @param  string $rowID  The 4x4 resource ID of the dataset to work with
334
     * @param  string $method Either `get` or `delete`
335
     *
336
     * @return mixed
337
     */
338
    private function individualRow ($rowID, $method)
339
    {
340
        $headers = array();
341
342
        // For a single row, the format is the `resourceID/rowID.json`, so we'll use that as the "location" of the Api URL
343
        $apiEndPoint = $this->buildApiUrl("resource", $this->resourceId . "/" . $rowID);
344
345
        $urlQuery = new UrlQuery($apiEndPoint, $this->sodaClient->getToken(), $this->sodaClient->getEmail(), $this->sodaClient->getPassword());
346
        $urlQuery->setOAuth2Token($this->sodaClient->getOAuth2Token());
347
348
        $result = $this->sendIndividualRequest($urlQuery, $method, $this->sodaClient->associativeArrayEnabled(), $headers);
349
350
        $this->setApiVersion($headers);
351
352
        return $result;
353
    }
354
355
    /**
356
     * Send the appropriate request header based on the method that's required
357
     *
358
     * @param UrlQuery $urlQuery          The object for the API endpoint
359
     * @param string   $method            Either `get` or `delete`
360
     * @param bool     $associativeArrays Whether or not to return the information as an associative array
361
     * @param array    $headers           An array with the cURL headers received
362
     *
363
     * @return mixed
364
     */
365
    private function sendIndividualRequest ($urlQuery, $method, $associativeArrays, &$headers)
366
    {
367
        if ($method === "get")
368
        {
369
            return $urlQuery->sendGet("", $associativeArrays, $headers);
370
        }
371
        else if ($method === "delete")
372
        {
373
            return $urlQuery->sendDelete($associativeArrays, $headers);
374
        }
375
376
        throw new \InvalidArgumentException("Invalid request method");
377
    }
378
379
    /**
380
     * Determine and save the API version if it does not exist for easy access later
381
     *
382
     * @param array $headers An array with the cURL headers received
383
     */
384
    private function setApiVersion ($headers)
385
    {
386
        // Only set the API version number if it hasn't been set yet
387
        if ($this->apiVersion == 0)
388
        {
389
            $this->apiVersion = $this->parseApiVersion($headers);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->parseApiVersion($headers) can also be of type double. However, the property $apiVersion is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
390
        }
391
    }
392
393
    /**
394
     * Determine the version number of the API this dataset is using
395
     *
396
     * @param  array  $responseHeaders An array with the cURL headers received
397
     *
398
     * @return double The Socrata API version number this dataset uses
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
399
     */
400
    private function parseApiVersion ($responseHeaders)
401
    {
402
        // A header that's unique to the legacy API
403
        if (array_key_exists('X-SODA2-Legacy-Types', $responseHeaders) && $responseHeaders['X-SODA2-Legacy-Types'])
404
        {
405
            return 1;
406
        }
407
408
        // A header that's unique to the new API
409
        if (array_key_exists('X-SODA2-Truth-Last-Modified', $responseHeaders))
410
        {
411
            if (empty($this->metadata))
412
            {
413
                $this->getMetadata();
414
            }
415
416
            if ($this->metadata['newBackend'])
417
            {
418
                return 2.1;
419
            }
420
421
            return 2;
422
        }
423
424
        return 0;
425
    }
426
}
427