Completed
Push — master ( 3abb65...67d89a )
by Christian
17s
created

OpenWeatherMap::buildQueryUrlParameter()   C

Complexity

Conditions 12
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 0
cts 0
cp 0
rs 6.9666
c 0
b 0
f 0
cc 12
nc 6
nop 1
crap 156

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * OpenWeatherMap-PHP-API — A PHP API to parse weather data from https://OpenWeatherMap.org.
5
 *
6
 * @license MIT
7
 *
8
 * Please see the LICENSE file distributed with this source code for further
9
 * information regarding copyright and licensing.
10
 *
11
 * Please visit the following links to read about the usage policies and the license of
12
 * OpenWeatherMap data before using this library:
13
 *
14
 * @see https://OpenWeatherMap.org/price
15
 * @see https://OpenWeatherMap.org/terms
16
 * @see https://OpenWeatherMap.org/appid
17
 */
18
19
namespace Cmfcmf;
20
21
use Cmfcmf\OpenWeatherMap\AirPollution;
22
use Cmfcmf\OpenWeatherMap\CurrentWeather;
23
use Cmfcmf\OpenWeatherMap\CurrentWeatherGroup;
24
use Cmfcmf\OpenWeatherMap\Exception as OWMException;
25
use Cmfcmf\OpenWeatherMap\NotFoundException as OWMNotFoundException;
26
use Cmfcmf\OpenWeatherMap\UVIndex;
27
use Cmfcmf\OpenWeatherMap\WeatherForecast;
28
use Psr\Cache\CacheItemPoolInterface;
29
use Psr\Http\Client\ClientInterface;
30
use Psr\Http\Message\RequestFactoryInterface;
31
32
/**
33
 * Main class for the OpenWeatherMap-PHP-API. Only use this class.
34
 *
35
 * @api
36
 */
37
class OpenWeatherMap
38
{
39
    /**
40
     * The copyright notice. This is no official text, it was created by
41
     * following the guidelines at http://openweathermap.org/copyright.
42
     *
43
     * @var string $copyright
44
     */
45
    const COPYRIGHT = "Weather data from <a href=\"https://openweathermap.org\">OpenWeatherMap.org</a>";
46
47
    /**
48
     * @var string The basic api url to fetch weather data from.
49
     */
50
    private $weatherUrl = 'https://api.openweathermap.org/data/2.5/weather?';
51
52
    /**
53
     * @var string The basic api url to fetch weather group data from.
54
     */
55
    private $weatherGroupUrl = 'https://api.openweathermap.org/data/2.5/group?';
56
57
    /**
58
     * @var string The basic api url to fetch weekly forecast data from.
59
     */
60
    private $weatherHourlyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast?';
61
62
    /**
63
     * @var string The basic api url to fetch daily forecast data from.
64
     */
65
    private $weatherDailyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast/daily?';
66
67
    /**
68
     * @var string The basic api url to fetch uv index data from.
69
     */
70
    private $uvIndexUrl = 'https://api.openweathermap.org/data/2.5/uvi';
71
72
    /**
73
     * @var string The basic api url to fetch air pollution data from.
74
     */
75
    private $airPollutionUrl = 'https://api.openweathermap.org/pollution/v1/';
76
77
    /**
78
     * @var CacheItemPoolInterface|null $cache The cache to use.
79
     */
80
    private $cache = null;
81
82
    /**
83
     * @var int
84
     */
85
    private $ttl;
86
87
    /**
88
     * @var bool
89
     */
90
    private $wasCached = false;
91
92
    /**
93
     * @var ClientInterface
94
     */
95
    private $httpClient;
96
97
    /**
98
     * @var RequestFactoryInterface
99
     */
100
    private $httpRequestFactory;
101
102
    /**
103
     * @var string
104
     */
105
    private $apiKey = '';
106
107
    /**
108
     * Constructs the OpenWeatherMap object.
109
     *
110
     * @param string                      $apiKey             The OpenWeatherMap API key. Required.
111
     * @param ClientInterface             $httpClient         A PSR-18 compatible HTTP client implementation.
112
     * @param RequestFactoryInterface     $httpRequestFactory A PSR-17 compatbile HTTP request factory implementation.
113
     * @param null|CacheItemPoolInterface $cache              If set to null, caching is disabled. Otherwise this must be
114
     *                                                        a PSR-6 compatible cache instance.
115
     * @param int                         $ttl                How long weather data shall be cached. Defaults to 10 minutes.
116
     *                                                        Only used if $cache is not null.
117
     *
118
     * @api
119
     */
120
    public function __construct($apiKey, $httpClient, $httpRequestFactory, $cache = null, $ttl = 600)
121
    {
122
        if (!is_string($apiKey) || empty($apiKey)) {
123
            throw new \InvalidArgumentException("You must provide an API key.");
124
        }
125
126
        if (!is_numeric($ttl)) {
127
            throw new \InvalidArgumentException('$ttl must be numeric.');
128
        }
129
130
        $this->apiKey = $apiKey;
131
        $this->httpClient = $httpClient;
132
        $this->httpRequestFactory = $httpRequestFactory;
133
        $this->cache = $cache;
134
        $this->ttl = $ttl;
0 ignored issues
show
Documentation Bug introduced by
It seems like $ttl can also be of type double or string. However, the property $ttl 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...
135
    }
136
137
    /**
138
     * Sets the API Key.
139
     *
140
     * @param string $apiKey API key for the OpenWeatherMap account.
141
     *
142
     * @api
143
     */
144
    public function setApiKey($apiKey)
145
    {
146
        $this->apiKey = $apiKey;
147
    }
148
149
    /**
150
     * Returns the API Key.
151
     *
152
     * @return string
153
     *
154
     * @api
155
     */
156
    public function getApiKey()
157
    {
158
        return $this->apiKey;
159
    }
160
161
    /**
162
     * Returns the current weather at the place you specified.
163
     *
164
     * @param array|int|string $query The place to get weather information for. For possible values see below.
165
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
166
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
167
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
168
     *
169
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
170
     * @throws \InvalidArgumentException If an argument error occurs.
171
     *
172
     * @return CurrentWeather The weather object.
173
     *
174
     * There are four ways to specify the place to get weather information for:
175
     * - Use the city name: $query must be a string containing the city name.
176
     * - Use the city id: $query must be an integer containing the city id.
177
     * - Use the coordinates: $query must be an associative array containing the 'lat' and 'lon' values.
178
     * - Use the zip code: $query must be a string, prefixed with "zip:"
179
     *
180
     * Zip code may specify country. e.g., "zip:77070" (Houston, TX, US) or "zip:500001,IN" (Hyderabad, India)
181
     *
182
     * @api
183
     */
184
    public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
185
    {
186
        $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
187
        $xml = $this->parseXML($answer);
188
189
        return new CurrentWeather($xml, $units);
190
    }
191
192
    /**
193
     * Returns the current weather for a group of city ids.
194
     *
195
     * @param array  $ids   The city ids to get weather information for
196
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
197
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
198
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
199
     *
200
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
201
     * @throws \InvalidArgumentException If an argument error occurs.
202
     *
203
     * @return CurrentWeatherGroup
204
     *
205
     * @api
206
     */
207
    public function getWeatherGroup($ids, $units = 'imperial', $lang = 'en', $appid = '')
208
    {
209
        $answer = $this->getRawWeatherGroupData($ids, $units, $lang, $appid);
210
        $json = $this->parseJson($answer);
211
212
        return new CurrentWeatherGroup($json, $units);
0 ignored issues
show
Bug introduced by
It seems like $json defined by $this->parseJson($answer) on line 210 can also be of type array; however, Cmfcmf\OpenWeatherMap\Cu...herGroup::__construct() does only seem to accept object<stdClass>, 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...
213
    }
214
215
    /**
216
     * Returns the forecast for the place you specified. DANGER: Might return
217
     * fewer results than requested due to a bug in the OpenWeatherMap API!
218
     *
219
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
220
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
221
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
222
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
223
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
224
     *
225
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
226
     * @throws \InvalidArgumentException If an argument error occurs.
227
     *
228
     * @return WeatherForecast
229
     *
230
     * @api
231
     */
232
    public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
233
    {
234
        if ($days <= 5) {
235
            $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
236
        } elseif ($days <= 16) {
237
            $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
238
        } else {
239
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
240
        }
241
        $xml = $this->parseXML($answer);
242
243
        return new WeatherForecast($xml, $units, $days);
244
    }
245
246
    /**
247
     * Returns the DAILY forecast for the place you specified. DANGER: Might return
248
     * fewer results than requested due to a bug in the OpenWeatherMap API!
249
     *
250
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
251
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
252
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
253
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
254
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
255
     *
256
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
257
     * @throws \InvalidArgumentException If an argument error occurs.
258
     *
259
     * @return WeatherForecast
260
     *
261
     * @api
262
     */
263
    public function getDailyWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
264
    {
265
        if ($days > 16) {
266
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
267
        }
268
269
        $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
270
        $xml = $this->parseXML($answer);
271
        return new WeatherForecast($xml, $units, $days);
272
    }
273
274
    /**
275
     * Returns the current uv index at the location you specified.
276
     *
277
     * @param float $lat The location's latitude.
278
     * @param float $lon The location's longitude.
279
     *
280
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
281
     * @throws \InvalidArgumentException If an argument error occurs.
282
     *
283
     * @return UVIndex
284
     *
285
     * @api
286
     */
287
    public function getCurrentUVIndex($lat, $lon)
288
    {
289
        $answer = $this->getRawUVIndexData('current', $lat, $lon);
290
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData('current', $lat, $lon) on line 289 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseJson() 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...
291
292
        return new UVIndex($json);
0 ignored issues
show
Bug introduced by
It seems like $json defined by $this->parseJson($answer) on line 290 can also be of type array; however, Cmfcmf\OpenWeatherMap\UVIndex::__construct() does only seem to accept object, 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...
293
    }
294
295
    /**
296
     * Returns a forecast of the uv index at the specified location.
297
     * The optional $cnt parameter determines the number of days to forecase.
298
     * The maximum supported number of days is 8.
299
     *
300
     * @param float $lat The location's latitude.
301
     * @param float $lon The location's longitude.
302
     * @param int   $cnt Number of returned days (default to 8).
303
     *
304
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
305
     * @throws \InvalidArgumentException If an argument error occurs.
306
     *
307
     * @return UVIndex[]
308
     *
309
     * @api
310
     */
311 View Code Duplication
    public function getForecastUVIndex($lat, $lon, $cnt = 8)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
312
    {
313
        $answer = $this->getRawUVIndexData('forecast', $lat, $lon, $cnt);
314
        $data = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData...ast', $lat, $lon, $cnt) on line 313 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseJson() 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...
315
316
        return array_map(function ($entry) {
317
            return new UVIndex($entry);
318
        }, $data);
319
    }
320
321
    /**
322
     * Returns the historic uv index at the specified location.
323
     *
324
     * @param float     $lat   The location's latitude.
325
     * @param float     $lon   The location's longitude.
326
     * @param \DateTime $start Starting point of time period.
327
     * @param \DateTime $end   Final point of time period.
328
     *
329
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
330
     * @throws \InvalidArgumentException If an argument error occurs.
331
     *
332
     * @return UVIndex[]
333
     *
334
     * @api
335
     */
336 View Code Duplication
    public function getHistoricUVIndex($lat, $lon, $start, $end)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
337
    {
338
        $answer = $this->getRawUVIndexData('historic', $lat, $lon, null, $start, $end);
339
        $data = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData...on, null, $start, $end) on line 338 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseJson() 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...
340
341
        return array_map(function ($entry) {
342
            return new UVIndex($entry);
343
        }, $data);
344
    }
345
346
    /**
347
     * Returns air pollution data
348
     *
349
     * @param string $type One of CO, O3, SO2, and NO2.
350
     * @param string $lat The location's latitude.
351
     * @param string $lon The location's longitude.
352
     * @param string $date The date to gather data from. If you omit this parameter or supply "current", returns current data.
353
     *
354
     * @return AirPollution\COAirPollution|AirPollution\NO2AirPollution|AirPollution\O3AirPollution|AirPollution\SO2AirPollution|null The air pollution data or null if no data was found.
355
     *
356
     * We use strings as $lat and $lon, since the exact number of digits in $lat and $lon determines the search range.
357
     * For example, there is a difference between using "1.5" and "1.5000".
358
     * We also use a string for $date, since it may either be "current" or an (abbreviated) ISO 8601 timestamp like 2016Z.
359
     *
360
     * @throws OWMException|\Exception
361
     *
362
     * @api
363
     */
364
    public function getAirPollution($type, $lat, $lon, $date = "current")
365
    {
366
        $answer = $this->getRawAirPollutionData($type, $lat, $lon, $date);
367
        if ($answer === null) {
368
            return null;
369
        }
370
        $json = $this->parseJson($answer);
371
        switch ($type) {
372
            case "O3":
373
                return new AirPollution\O3AirPollution($json);
374
            case "NO2":
375
                return new AirPollution\NO2AirPollution($json);
376
            case "SO2":
377
                return new AirPollution\SO2AirPollution($json);
378
            case "CO":
379
                return new AirPollution\COAirPollution($json);
380
            default:
381
                throw new \LogicException();
382
        }
383
    }
384
385
    /**
386
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
387
     *
388
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
389
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
390
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
391
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
392
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
393
     *
394
     * @return string Returns false on failure and the fetched data in the format you specified on success.
395
     *
396
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
397
     *
398
     * @api
399
     */
400
    public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
401
    {
402
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
403
404
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($query, ...ode, $this->weatherUrl) on line 402 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::cacheOrFetchResult() 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...
405
    }
406
407
    /**
408
     * Directly returns the JSON string returned by OpenWeatherMap for the group of current weather.
409
     * Only a JSON response format is supported for this webservice.
410
     *
411
     * @param array  $ids   The city ids to get weather information for
412
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
413
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
414
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
415
     *
416
     * @return string Returns false on failure and the fetched data in the format you specified on success.
417
     *
418
     * @api
419
     */
420
    public function getRawWeatherGroupData($ids, $units = 'imperial', $lang = 'en', $appid = '')
421
    {
422
        $url = $this->buildUrl($ids, $units, $lang, $appid, 'json', $this->weatherGroupUrl);
423
424
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($ids, $u...$this->weatherGroupUrl) on line 422 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::cacheOrFetchResult() 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...
425
    }
426
427
    /**
428
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
429
     *
430
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
431
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
432
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
433
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
434
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
435
     *
436
     * @return string Returns false on failure and the fetched data in the format you specified on success.
437
     *
438
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
439
     *
440
     * @api
441
     */
442
    public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
443
    {
444
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
445
446
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($query, ...atherHourlyForecastUrl) on line 444 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::cacheOrFetchResult() 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...
447
    }
448
449
    /**
450
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
451
     *
452
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
453
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
454
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
455
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
456
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
457
     * @param int              $cnt   How many days of forecast shall be returned? Maximum (and default): 16
458
     *
459
     * @throws \InvalidArgumentException If $cnt is higher than 16.
460
     *
461
     * @return string Returns false on failure and the fetched data in the format you specified on success.
462
     *
463
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
464
     *
465
     * @api
466
     */
467
    public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
468
    {
469
        if ($cnt > 16) {
470
            throw new \InvalidArgumentException('$cnt must be 16 or lower!');
471
        }
472
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
473
474
        return $this->cacheOrFetchResult($url);
475
    }
476
477
    /**
478
     * Directly returns the json string returned by OpenWeatherMap for the UV index data.
479
     *
480
     * @param string    $mode  The type of requested data (['historic', 'forecast', 'current']).
481
     * @param float     $lat   The location's latitude.
482
     * @param float     $lon   The location's longitude.
483
     * @param int       $cnt   Number of returned days (only allowed for 'forecast' data).
484
     * @param \DateTime $start Starting point of time period (only allowed and required for 'historic' data).
485
     * @param \DateTime $end   Final point of time period (only allowed and required for 'historic' data).
486
     *
487
     * @return bool|string Returns the fetched data.
488
     *
489
     * @api
490
     */
491
    public function getRawUVIndexData($mode, $lat, $lon, $cnt = null, $start = null, $end = null)
492
    {
493
        if (!in_array($mode, array('current', 'forecast', 'historic'), true)) {
494
            throw new \InvalidArgumentException("$mode must be one of 'historic', 'forecast', 'current'.");
495
        }
496
        if (!is_float($lat) || !is_float($lon)) {
497
            throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
498
        }
499
        if (isset($cnt) && (!is_int($cnt) || $cnt > 8 || $cnt < 1)) {
500
            throw new \InvalidArgumentException('$cnt must be an int between 1 and 8');
501
        }
502
        if (isset($start) && !$start instanceof \DateTime) {
503
            throw new \InvalidArgumentException('$start must be an instance of \DateTime');
504
        }
505
        if (isset($end) && !$end instanceof \DateTime) {
506
            throw new \InvalidArgumentException('$end must be an instance of \DateTime');
507
        }
508
        if ($mode === 'current' && (isset($start) || isset($end) || isset($cnt))) {
509
            throw new \InvalidArgumentException('Neither $start, $end, nor $cnt must be set for current data.');
510
        } elseif ($mode === 'forecast' && (isset($start) || isset($end) || !isset($cnt))) {
511
            throw new \InvalidArgumentException('$cnt needs to be set and both $start and $end must not be set for forecast data.');
512
        } elseif ($mode === 'historic' && (!isset($start) || !isset($end) || isset($cnt))) {
513
            throw new \InvalidArgumentException('Both $start and $end need to be set and $cnt must not be set for historic data.');
514
        }
515
516
        $url = $this->buildUVIndexUrl($mode, $lat, $lon, $cnt, $start, $end);
517
        return $this->cacheOrFetchResult($url);
518
    }
519
520
    /**
521
     * Fetch raw air pollution data
522
     *
523
     * @param  string $type One of CO, O3, SO2, and NO2.
524
     * @param  string $lat  The location's latitude.
525
     * @param  string $lon  The location's longitude.
526
     * @param  string $date The date to gather data from. If you omit this parameter or supply "current", returns current data.
527
     *
528
     * @return string|null The air pollution data or null if no data was found.
529
     *
530
     * We use strings as $lat and $lon, since the exact number of digits in $lat and $lon determines the search range.
531
     * For example, there is a difference between using "1.5" and "1.5000".
532
     * We also use a string for $date, since it may either be "current" or an (abbreviated) ISO 8601 timestamp like 2016Z.
533
     *
534
     * @api
535
     */
536
    public function getRawAirPollutionData($type, $lat, $lon, $date = "current")
537
    {
538
        if (!in_array($type, ["CO", "O3", "SO2", "NO2"])) {
539
            throw new \InvalidArgumentException('Invalid $type received.');
540
        }
541
        if (!is_string($lat) || !is_string($lon) || !is_string($date)) {
542
            throw new \InvalidArgumentException('$lat, $lon and $date all must be strings.');
543
        }
544
545
        $url = $this->airPollutionUrl . strtolower($type) . "/$lat,$lon/$date.json?appid=" . $this->apiKey;
546
547
        try {
548
            return $this->cacheOrFetchResult($url);
549
        } catch (OWMNotFoundException $e) {
550
            return null;
551
        }
552
    }
553
554
    /**
555
     * Returns whether or not the last result was fetched from the cache.
556
     *
557
     * @return bool true if last result was fetched from cache, false otherwise.
558
     */
559
    public function wasCached()
560
    {
561
        return $this->wasCached;
562
    }
563
564
    /**
565
     * Fetches the result or delivers a cached version of the result.
566
     *
567
     * @param string $url
568
     *
569
     * @return string
570
     */
571
    private function cacheOrFetchResult($url)
572
    {
573
        if ($this->cache !== null) {
574
            $key = str_replace(
575
                ["{", "}", "(", ")", "/", "\\", "@", ":"],
576
                ["_", "_", "_", "_", "_", "_",  "_", "_"],
577
                $url);
578
            $item = $this->cache->getItem($key);
579
            if ($item->isHit()) {
580
                $this->wasCached = true;
581
                return $item->get();
582
            }
583
        }
584
585
        $response = $this->httpClient->sendRequest($this->httpRequestFactory->createRequest("GET", $url));
586
        $result = $response->getBody()->getContents();
587
        if ($response->getStatusCode() !== 200) {
588
            if ($result === "{\"message\":\"not found\"}\n" && $response->getStatusCode() === 404) {
589
                throw new OWMNotFoundException();
590
            }
591
            throw new OWMException('OpenWeatherMap returned a response with status code ' . $response->getStatusCode() . ' and the following content `'. $result . '`');
592
        }
593
594
        if ($this->cache !== null) {
595
            $item->set($result);
0 ignored issues
show
Bug introduced by
The variable $item does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
596
            $item->expiresAfter($this->ttl);
597
            $this->cache->save($item);
598
        }
599
        $this->wasCached = false;
600
601
        return $result;
602
    }
603
604
    /**
605
     * Build the url to fetch weather data from.
606
     *
607
     * @param        $query
608
     * @param        $units
609
     * @param        $lang
610
     * @param        $appid
611
     * @param        $mode
612
     * @param string $url   The url to prepend.
613
     *
614
     * @return bool|string The fetched url, false on failure.
615
     */
616
    private function buildUrl($query, $units, $lang, $appid, $mode, $url)
617
    {
618
        $queryUrl = $this->buildQueryUrlParameter($query);
619
620
        $url = $url."$queryUrl&units=$units&lang=$lang&mode=$mode&APPID=";
621
        $url .= empty($appid) ? $this->apiKey : $appid;
622
623
        return $url;
624
    }
625
626
    /**
627
     * @param string             $mode          The type of requested data.
628
     * @param float              $lat           The location's latitude.
629
     * @param float              $lon           The location's longitude.
630
     * @param int                $cnt           Number of returned days.
631
     * @param \DateTime          $start         Starting point of time period.
632
     * @param \DateTime          $end           Final point of time period.
633
     *
634
     * @return string
635
     */
636
    private function buildUVIndexUrl($mode, $lat, $lon, $cnt = null, \DateTime $start = null, \DateTime $end = null)
637
    {
638
        $params = array(
639
            'appid' => $this->apiKey,
640
            'lat' => $lat,
641
            'lon' => $lon,
642
        );
643
644
        switch ($mode) {
645
            case 'historic':
646
                $requestMode = '/history';
647
                $params['start'] = $start->format('U');
0 ignored issues
show
Bug introduced by
It seems like $start is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
648
                $params['end'] = $end->format('U');
0 ignored issues
show
Bug introduced by
It seems like $end is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
649
                break;
650
            case 'forecast':
651
                $requestMode = '/forecast';
652
                $params['cnt'] = $cnt;
653
                break;
654
            case 'current':
655
                $requestMode = '';
656
                break;
657
            default:
658
                throw new \InvalidArgumentException("Invalid mode $mode for uv index url");
659
        }
660
661
        return sprintf($this->uvIndexUrl . '%s?%s', $requestMode, http_build_query($params));
662
    }
663
664
    /**
665
     * Builds the query string for the url.
666
     *
667
     * @param mixed $query
668
     *
669
     * @return string The built query string for the url.
670
     *
671
     * @throws \InvalidArgumentException If the query parameter is invalid.
672
     */
673
    private function buildQueryUrlParameter($query)
674
    {
675
        switch ($query) {
676
            case is_array($query) && isset($query['lat']) && isset($query['lon']) && is_numeric($query['lat']) && is_numeric($query['lon']):
677
                return "lat={$query['lat']}&lon={$query['lon']}";
678
            case is_array($query) && is_numeric($query[0]):
679
                return 'id='.implode(',', $query);
680
            case is_numeric($query):
681
                return "id=$query";
682
            case is_string($query) && strpos($query, 'zip:') === 0:
683
                $subQuery = str_replace('zip:', '', $query);
684
                return 'zip='.urlencode($subQuery);
685
            case is_string($query):
686
                return 'q='.urlencode($query);
687
            default:
688
                throw new \InvalidArgumentException('Error: $query has the wrong format. See the documentation of OpenWeatherMap::getWeather() to read about valid formats.');
689
        }
690
    }
691
692
    /**
693
     * @param string $answer The content returned by OpenWeatherMap.
694
     *
695
     * @return \SimpleXMLElement
696
     * @throws OWMException If the content isn't valid XML.
697
     */
698
    private function parseXML($answer)
699
    {
700
        // Disable default error handling of SimpleXML (Do not throw E_WARNINGs).
701
        libxml_use_internal_errors(true);
702
        libxml_clear_errors();
703
        try {
704
            return new \SimpleXMLElement($answer);
705
        } catch (\Exception $e) {
706
            // Invalid xml format. This happens in case OpenWeatherMap returns an error.
707
            // OpenWeatherMap always uses json for errors, even if one specifies xml as format.
708
            $error = json_decode($answer, true);
709
            if (isset($error['message'])) {
710
                throw new OWMException($error['message'], isset($error['cod']) ? $error['cod'] : 0);
711
            } else {
712
                throw new OWMException('Unknown fatal error: OpenWeatherMap returned the following json object: ' . $answer);
713
            }
714
        }
715
    }
716
717
    /**
718
     * @param string $answer The content returned by OpenWeatherMap.
719
     *
720
     * @return \stdClass|array
721
     * @throws OWMException If the content isn't valid JSON.
722
     */
723
    private function parseJson($answer)
724
    {
725
        $json = json_decode($answer);
726
        if (json_last_error() !== JSON_ERROR_NONE) {
727
            throw new OWMException('OpenWeatherMap returned an invalid json object. JSON error was: "' .
728
                $this->json_last_error_msg() . '". The retrieved json was: ' . $answer);
729
        }
730
        if (isset($json->message)) {
731
            throw new OWMException('An error occurred: '. $json->message);
732
        }
733
734
        return $json;
735
    }
736
737
    private function json_last_error_msg()
738
    {
739
        if (function_exists('json_last_error_msg')) {
740
            return json_last_error_msg();
741
        }
742
743
        static $ERRORS = array(
744
            JSON_ERROR_NONE => 'No error',
745
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
746
            JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
747
            JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
748
            JSON_ERROR_SYNTAX => 'Syntax error',
749
            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
750
        );
751
752
        $error = json_last_error();
753
        return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error';
754
    }
755
}
756