Completed
Pull Request — master (#132)
by
unknown
10:58
created

OpenWeatherMap::getForecastUVIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 11
loc 11
ccs 0
cts 0
cp 0
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 2
1
<?php
2
/**
3
 * OpenWeatherMap-PHP-API — A php api to parse weather data from http://www.OpenWeatherMap.org .
4
 *
5
 * @license MIT
6
 *
7
 * Please see the LICENSE file distributed with this source code for further
8
 * information regarding copyright and licensing.
9
 *
10
 * Please visit the following links to read about the usage policies and the license of
11
 * OpenWeatherMap before using this class:
12
 *
13
 * @see http://www.OpenWeatherMap.org
14
 * @see http://www.OpenWeatherMap.org/terms
15
 * @see http://openweathermap.org/appid
16
 */
17
18
namespace Cmfcmf;
19
20
use Cmfcmf\OpenWeatherMap\AbstractCache;
21
use Cmfcmf\OpenWeatherMap\CurrentWeather;
22
use Cmfcmf\OpenWeatherMap\UVIndex;
23
use Cmfcmf\OpenWeatherMap\CurrentWeatherGroup;
24
use Cmfcmf\OpenWeatherMap\Exception as OWMException;
25
use Cmfcmf\OpenWeatherMap\Fetcher\CurlFetcher;
26
use Cmfcmf\OpenWeatherMap\Fetcher\FetcherInterface;
27
use Cmfcmf\OpenWeatherMap\Fetcher\FileGetContentsFetcher;
28
use Cmfcmf\OpenWeatherMap\WeatherForecast;
29
use Cmfcmf\OpenWeatherMap\WeatherHistory;
30
31
/**
32
 * Main class for the OpenWeatherMap-PHP-API. Only use this class.
33
 *
34
 * @api
35
 */
36
class OpenWeatherMap
37
{
38
    /**
39
     * The copyright notice. This is no official text, it was created by
40
     * following the guidelines at http://openweathermap.org/copyright.
41
     *
42
     * @var string $copyright
43
     */
44
    const COPYRIGHT = "Weather data from <a href=\"https://openweathermap.org\">OpenWeatherMap.org</a>";
45
46
    /**
47
     * @var string The basic api url to fetch weather data from.
48
     */
49
    private $weatherUrl = 'https://api.openweathermap.org/data/2.5/weather?';
50
51
    /**
52
     * @var string The basic api url to fetch weather group data from.
53
     */
54
    private $weatherGroupUrl = 'https://api.openweathermap.org/data/2.5/group?';
55
56
    /**
57
     * @var string The basic api url to fetch weekly forecast data from.
58
     */
59
    private $weatherHourlyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast?';
60
61
    /**
62
     * @var string The basic api url to fetch daily forecast data from.
63
     */
64
    private $weatherDailyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast/daily?';
65
66
    /**
67
     * @var string The basic api url to fetch history weather data from.
68
     */
69
    private $weatherHistoryUrl = 'https://history.openweathermap.org/data/2.5/history/city?';
70
71
    /**
72
     * @var string The basic api url to fetch uv index data from.
73
     */
74
    private $uvIndexUrl = 'https://api.openweathermap.org/data/2.5/uvi';
75
76
    /**
77
     * @var AbstractCache|bool $cache The cache to use.
78
     */
79
    private $cache = false;
80
81
    /**
82
     * @var int
83
     */
84
    private $seconds;
85
86
    /**
87
     * @var bool
88
     */
89
    private $wasCached = false;
90
91
    /**
92
     * @var FetcherInterface The url fetcher.
93
     */
94
    private $fetcher;
95
96
    /**
97
     * @var string
98
     */
99
    private $apiKey = '';
100
101
    /**
102
     * Constructs the OpenWeatherMap object.
103
     *
104
     * @param string                $apiKey  The OpenWeatherMap API key. Required and only optional for BC.
105
     * @param null|FetcherInterface $fetcher The interface to fetch the data from OpenWeatherMap. Defaults to
106
     *                                       CurlFetcher() if cURL is available. Otherwise defaults to
107
     *                                       FileGetContentsFetcher() using 'file_get_contents()'.
108
     * @param bool|string           $cache   If set to false, caching is disabled. Otherwise this must be a class
109
     *                                       extending AbstractCache. Defaults to false.
110
     * @param int $seconds                   How long weather data shall be cached. Default 10 minutes.
111
     *
112
     * @throws \Exception If $cache is neither false nor a valid callable extending Cmfcmf\OpenWeatherMap\Util\Cache.
113
     *
114
     * @api
115
     */
116
    public function __construct($apiKey = '', $fetcher = null, $cache = false, $seconds = 600)
117
    {
118
        if (!is_string($apiKey) || empty($apiKey)) {
119
            // BC
120
            $seconds = $cache !== false ? $cache : 600;
121
            $cache = $fetcher !== null ? $fetcher : false;
122
            $fetcher = $apiKey !== '' ? $apiKey : null;
123
        } else {
124
            $this->apiKey = $apiKey;
125
        }
126
127
        if ($cache !== false && !($cache instanceof AbstractCache)) {
128
            throw new \InvalidArgumentException('The cache class must implement the FetcherInterface!');
129
        }
130
        if (!is_numeric($seconds)) {
131
            throw new \InvalidArgumentException('$seconds must be numeric.');
132
        }
133
        if (!isset($fetcher)) {
134
            $fetcher = (function_exists('curl_version')) ? new CurlFetcher() : new FileGetContentsFetcher();
135
        }
136
        if ($seconds == 0) {
137
            $cache = false;
138
        }
139
140
        $this->cache = $cache;
141
        $this->seconds = $seconds;
0 ignored issues
show
Documentation Bug introduced by
It seems like $seconds can also be of type double or string. However, the property $seconds 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...
142
        $this->fetcher = $fetcher;
0 ignored issues
show
Documentation Bug introduced by
It seems like $fetcher can also be of type string. However, the property $fetcher is declared as type object<Cmfcmf\OpenWeathe...tcher\FetcherInterface>. 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...
143
    }
144
145
    /**
146
     * Sets the API Key.
147
     *
148
     * @param string $apiKey API key for the OpenWeatherMap account.
149
     *
150
     * @api
151
     */
152
    public function setApiKey($apiKey)
153
    {
154
        $this->apiKey = $apiKey;
155
    }
156
157
    /**
158
     * Returns the API Key.
159
     *
160
     * @return string
161
     *
162
     * @api
163
     */
164
    public function getApiKey()
165
    {
166
        return $this->apiKey;
167
    }
168
169
    /**
170
     * Returns the current weather at the place you specified.
171
     *
172
     * @param array|int|string $query The place to get weather information for. For possible values see below.
173
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
174
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
175
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
176
     *
177
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
178
     * @throws \InvalidArgumentException If an argument error occurs.
179
     *
180
     * @return CurrentWeather The weather object.
181
     *
182
     * There are four ways to specify the place to get weather information for:
183
     * - Use the city name: $query must be a string containing the city name.
184
     * - Use the city id: $query must be an integer containing the city id.
185
     * - Use the coordinates: $query must be an associative array containing the 'lat' and 'lon' values.
186
     * - Use the zip code: $query must be a string, prefixed with "zip:"
187
     *
188
     * Zip code may specify country. e.g., "zip:77070" (Houston, TX, US) or "zip:500001,IN" (Hyderabad, India)
189
     *
190
     * @api
191
     */
192
    public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
193
    {
194
        $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
195
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawWeatherData..., $lang, $appid, 'xml') on line 194 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseXML() 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...
196
197
        return new CurrentWeather($xml, $units);
198
    }
199
200
    /**
201
     * Returns the current weather for a group of city ids.
202
     *
203
     * @param array  $ids   The city ids to get weather information for
204
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
205
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
206
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
207
     *
208
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
209
     * @throws \InvalidArgumentException If an argument error occurs.
210
     *
211
     * @return CurrentWeatherGroup
212
     *
213
     * @api
214
     */
215
    public function getWeatherGroup($ids, $units = 'imperial', $lang = 'en', $appid = '')
216
    {
217
        $answer = $this->getRawWeatherGroupData($ids, $units, $lang, $appid);
218
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawWeatherGrou... $units, $lang, $appid) on line 217 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...
219
220
        return new CurrentWeatherGroup($json, $units);
221
    }
222
223
    /**
224
     * Returns the forecast for the place you specified. DANGER: Might return
225
     * fewer results than requested due to a bug in the OpenWeatherMap API!
226
     *
227
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
228
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
229
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
230
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
231
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
232
     *
233
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
234
     * @throws \InvalidArgumentException If an argument error occurs.
235
     *
236
     * @return WeatherForecast
237
     *
238
     * @api
239
     */
240
    public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
241
    {
242
        if ($days <= 5) {
243
            $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
244
        } elseif ($days <= 16) {
245
            $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
246
        } else {
247
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
248
        }
249
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseXML() 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...
250
251
        return new WeatherForecast($xml, $units, $days);
252
    }
253
254
    /**
255
     * Returns the DAILY forecast for the place you specified. DANGER: Might return
256
     * fewer results than requested due to a bug in the OpenWeatherMap API!
257
     *
258
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
259
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
260
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
261
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
262
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
263
     *
264
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
265
     * @throws \InvalidArgumentException If an argument error occurs.
266
     *
267
     * @return WeatherForecast
268
     *
269
     * @api
270
     */
271
    public function getDailyWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
272
    {
273
        if ($days > 16) {
274
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
275
        }
276
277
        $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
278
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawDailyForeca..., $appid, 'xml', $days) on line 277 can also be of type boolean; however, Cmfcmf\OpenWeatherMap::parseXML() 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...
279
        return new WeatherForecast($xml, $units, $days);
280
    }
281
282
    /**
283
     * Returns the weather history for the place you specified.
284
     *
285
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
286
     * @param \DateTime        $start
287
     * @param int              $endOrCount
288
     * @param string           $type       Can either be 'tick', 'hour' or 'day'.
289
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
290
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
291
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
292
     *
293
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
294
     * @throws \InvalidArgumentException If an argument error occurs.
295
     *
296
     * @return WeatherHistory
297
     *
298
     * @api
299
     */
300
    public function getWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
301
    {
302 View Code Duplication
        if (!in_array($type, array('tick', 'hour', 'day'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
304
        }
305
306
        $xml = json_decode($this->getRawWeatherHistory($query, $start, $endOrCount, $type, $units, $lang, $appid), true);
307
308
        if ($xml['cod'] != 200) {
309
            throw new OWMException($xml['message'], $xml['cod']);
310
        }
311
312
        return new WeatherHistory($xml, $query);
313
    }
314
315
    /**
316
     * Returns the current uv index at the location you specified.
317
     *
318
     * @param float              $lat           The location's latitude.
319
     * @param float              $lon           The location's longitude.
320
     *
321
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
322
     * @throws \InvalidArgumentException If an argument error occurs.
323
     *
324
     * @return UVIndex The uvi object.
325
     *
326
     * @api
327
     */
328
    public function getCurrentUVIndex($lat, $lon)
329
    {
330
        $answer = $this->getRawUVIndexData('current', $lat, $lon);
331
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData('current', $lat, $lon) on line 330 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...
332
333
        return new UVIndex($json);
334
    }
335
336
    /**
337
     * Returns the current uv index at the location you specified.
338
     *
339
     * @param float              $lat           The location's latitude.
340
     * @param float              $lon           The location's longitude.
341
     * @param int                $cnt           Number of returned days.
342
     *
343
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
344
     * @throws \InvalidArgumentException If an argument error occurs.
345
     *
346
     * @return UVIndex The uvi object.
347
     *
348
     * @api
349
     */
350 View Code Duplication
    public function getForecastUVIndex($lat, $lon, $cnt)
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...
351
    {
352
        $answer = $this->getRawUVIndexData('forecast', $lat, $lon, $cnt);
353
        $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 352 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...
354
355
        $mapData = function ($entry) {
356
            return new UVIndex($entry);
357
        };
358
359
        return array_map($mapData, $data);
360
    }
361
362
    /**
363
     * Returns the current uv index at the location you specified.
364
     *
365
     * @param float              $lat           The location's latitude.
366
     * @param float              $lon           The location's longitude.
367
     * @param int                $cnt           Number of returned days.
368
     * @param \DateTime          $start         Starting point of time period.
369
     * @param \DateTime          $end           Final point of time period.
370
     *
371
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
372
     * @throws \InvalidArgumentException If an argument error occurs.
373
     *
374
     * @return UVIndex The uvi object.
375
     *
376
     * @api
377
     */
378 View Code Duplication
    public function getHistoryUVIndex($lat, $lon, $cnt, $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...
379
    {
380
        $answer = $this->getRawUVIndexData('history', $lat, $lon, $cnt, $start, $end);
381
        $data = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData...on, $cnt, $start, $end) on line 380 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...
382
383
        $mapData = function ($entry) {
384
            return new UVIndex($entry);
385
        };
386
387
        return array_map($mapData, $data);
388
    }
389
390
    /**
391
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
392
     *
393
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
394
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
395
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
396
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
397
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
398
     *
399
     * @return string Returns false on failure and the fetched data in the format you specified on success.
400
     *
401
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
402
     *
403
     * @api
404
     */
405
    public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
406
    {
407
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
408
409
        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 407 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...
410
    }
411
412
    /**
413
     * Directly returns the JSON string returned by OpenWeatherMap for the group of current weather.
414
     * Only a JSON response format is supported for this webservice.
415
     *
416
     * @param array  $ids   The city ids to get weather information for
417
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
418
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
419
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
420
     *
421
     * @return string Returns false on failure and the fetched data in the format you specified on success.
422
     *
423
     * @api
424
     */
425
    public function getRawWeatherGroupData($ids, $units = 'imperial', $lang = 'en', $appid = '')
426
    {
427
        $url = $this->buildUrl($ids, $units, $lang, $appid, 'json', $this->weatherGroupUrl);
428
429
        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 427 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...
430
    }
431
432
    /**
433
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
434
     *
435
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
436
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
437
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
438
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
439
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
440
     *
441
     * @return string Returns false on failure and the fetched data in the format you specified on success.
442
     *
443
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
444
     *
445
     * @api
446
     */
447
    public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
448
    {
449
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
450
451
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($query, ...atherHourlyForecastUrl) on line 449 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...
452
    }
453
454
    /**
455
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
456
     *
457
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
458
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
459
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
460
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
461
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
462
     * @param int              $cnt   How many days of forecast shall be returned? Maximum (and default): 16
463
     *
464
     * @throws \InvalidArgumentException If $cnt is higher than 16.
465
     *
466
     * @return string Returns false on failure and the fetched data in the format you specified on success.
467
     *
468
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
469
     *
470
     * @api
471
     */
472
    public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
473
    {
474
        if ($cnt > 16) {
475
            throw new \InvalidArgumentException('$cnt must be 16 or lower!');
476
        }
477
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
478
479
        return $this->cacheOrFetchResult($url);
480
    }
481
482
    /**
483
     * Directly returns the json string returned by OpenWeatherMap for the weather history.
484
     *
485
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
486
     * @param \DateTime        $start      The \DateTime object of the date to get the first weather information from.
487
     * @param \DateTime|int    $endOrCount Can be either a \DateTime object representing the end of the period to
488
     *                                     receive weather history data for or an integer counting the number of
489
     *                                     reports requested.
490
     * @param string           $type       The period of the weather history requested. Can be either be either "tick",
491
     *                                     "hour" or "day".
492
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
493
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
494
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
495
     *
496
     * @throws \InvalidArgumentException
497
     *
498
     * @return string Returns false on failure and the fetched data in the format you specified on success.
499
     *
500
     * Warning If an error occurred, OpenWeatherMap ALWAYS returns data in json format.
501
     *
502
     * @api
503
     */
504
    public function getRawWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
505
    {
506 View Code Duplication
        if (!in_array($type, array('tick', 'hour', 'day'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
507
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
508
        }
509
510
        $url = $this->buildUrl($query, $units, $lang, $appid, 'json', $this->weatherHistoryUrl);
511
        $url .= "&type=$type&start={$start->format('U')}";
512
        if ($endOrCount instanceof \DateTime) {
513
            $url .= "&end={$endOrCount->format('U')}";
514
        } elseif (is_numeric($endOrCount) && $endOrCount > 0) {
515
            $url .= "&cnt=$endOrCount";
516
        } else {
517
            throw new \InvalidArgumentException('$endOrCount must be either a \DateTime or a positive integer.');
518
        }
519
520
        return $this->cacheOrFetchResult($url);
521
    }
522
523
    /**
524
     * Directly returns the json string returned by OpenWeatherMap for the UV index data.
525
     *
526
     * @param string             $mode          The type of requested data.
527
     * @param float              $lat           The location's latitude.
528
     * @param float              $lon           The location's longitude.
529
     * @param int                $cnt           Number of returned days.
530
     * @param \DateTime          $start         Starting point of time period.
531
     * @param \DateTime          $end           Final point of time period.
532
     *
533
     * @return bool|string Returns the fetched data.
534
     *
535
     * @api
536
     */
537
    public function getRawUVIndexData($mode, $lat, $lon, $cnt = null, $start = null, $end = null)
538
    {
539
        if (!is_float($lat) || !is_float($lon)) {
540
            throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
541
        }
542
        if (isset($cnt) && !is_int($cnt)) {
543
            throw new \InvalidArgumentException('$cnt must be an int');
544
        }
545
        if (isset($start) && !$start instanceof \DateTime) {
546
            throw new \InvalidArgumentException('$start must be an instance of \DateTime');
547
        }
548
        if (isset($end) && !$end instanceof \DateTime) {
549
            throw new \InvalidArgumentException('$end must be an instance of \DateTime');
550
        }
551
552
        $url = $this->buildUVIndexUrl($mode, $lat, $lon, $cnt, $start, $end);
553
554
        return $this->cacheOrFetchResult($url);
555
    }
556
557
    /**
558
     * Returns whether or not the last result was fetched from the cache.
559
     *
560
     * @return bool true if last result was fetched from cache, false otherwise.
561
     */
562
    public function wasCached()
563
    {
564
        return $this->wasCached;
565
    }
566
567
    /**
568
     * @deprecated Use {@link self::getRawWeatherData()} instead.
569
     */
570
    public function getRawData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
571
    {
572
        return $this->getRawWeatherData($query, $units, $lang, $appid, $mode);
573
    }
574
575
    /**
576
     * Fetches the result or delivers a cached version of the result.
577
     *
578
     * @param string $url
579
     *
580
     * @return string
581
     */
582
    private function cacheOrFetchResult($url)
583
    {
584
        if ($this->cache !== false) {
585
            /** @var AbstractCache $cache */
586
            $cache = $this->cache;
587
            $cache->setSeconds($this->seconds);
588
589
            if ($cache->isCached($url)) {
590
                $this->wasCached = true;
591
                return $cache->getCached($url);
592
            }
593
594
            $result = $this->fetcher->fetch($url);
595
            $cache->setCached($url, $result);
596
        } else {
597
            $result = $this->fetcher->fetch($url);
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
        switch ($mode) {
639
            case 'history':
640
                $requestMode = '/history';
641
                break;
642
            case 'forecast':
643
                $requestMode = '/forecast';
644
                break;
645
            case 'current':
646
            default:
647
                $requestMode = '';
648
        }
649
650
        $params = [
651
            'appid' => $this->apiKey,
652
            'lat' => $lat,
653
            'lon' => $lon,
654
        ];
655
656
        if (!is_int($cnt)) {
657
            $params['cnt'] = $cnt;
658
        }
659
660
        if (!is_int($start)) {
661
            $params['start'] = $start;
662
        }
663
664
        if (!is_int($end)) {
665
            $params['end'] = $end;
666
        }
667
668
        return sprintf($this->uvIndexUrl . '%s?%s', $requestMode, http_build_query($params));
669
    }
670
671
    /**
672
     * Builds the query string for the url.
673
     *
674
     * @param mixed $query
675
     *
676
     * @return string The built query string for the url.
677
     *
678
     * @throws \InvalidArgumentException If the query parameter is invalid.
679
     */
680
    private function buildQueryUrlParameter($query)
681
    {
682
        switch ($query) {
683
            case is_array($query) && isset($query['lat']) && isset($query['lon']) && is_numeric($query['lat']) && is_numeric($query['lon']):
684
                return "lat={$query['lat']}&lon={$query['lon']}";
685
            case is_array($query) && is_numeric($query[0]):
686
                return 'id='.implode(',', $query);
687
            case is_numeric($query):
688
                return "id=$query";
689
            case is_string($query) && strpos($query, 'zip:') === 0:
690
                $subQuery = str_replace('zip:', '', $query);
691
                return 'zip='.urlencode($subQuery);
692
            case is_string($query):
693
                return 'q='.urlencode($query);
694
            default:
695
                throw new \InvalidArgumentException('Error: $query has the wrong format. See the documentation of OpenWeatherMap::getWeather() to read about valid formats.');
696
        }
697
    }
698
699
    /**
700
     * @param string $answer The content returned by OpenWeatherMap.
701
     *
702
     * @return \SimpleXMLElement
703
     * @throws OWMException If the content isn't valid XML.
704
     */
705
    private function parseXML($answer)
706
    {
707
        // Disable default error handling of SimpleXML (Do not throw E_WARNINGs).
708
        libxml_use_internal_errors(true);
709
        libxml_clear_errors();
710
        try {
711
            return new \SimpleXMLElement($answer);
712
        } catch (\Exception $e) {
713
            // Invalid xml format. This happens in case OpenWeatherMap returns an error.
714
            // OpenWeatherMap always uses json for errors, even if one specifies xml as format.
715
            $error = json_decode($answer, true);
716
            if (isset($error['message'])) {
717
                throw new OWMException($error['message'], isset($error['cod']) ? $error['cod'] : 0);
718
            } else {
719
                throw new OWMException('Unknown fatal error: OpenWeatherMap returned the following json object: ' . $answer);
720
            }
721
        }
722
    }
723
724
    /**
725
     * @param string $answer The content returned by OpenWeatherMap.
726
     *
727
     * @return \stdClass
728
     * @throws OWMException If the content isn't valid JSON.
729
     */
730
    private function parseJson($answer)
731
    {
732
        $json = json_decode($answer);
733
        if (json_last_error() !== JSON_ERROR_NONE) {
734
            throw new OWMException('OpenWeatherMap returned an invalid json object. JSON error was: ' . $this->json_last_error_msg());
735
        }
736
        if (isset($json->message)) {
737
            throw new OWMException('An error occurred: '. $json->message);
738
        }
739
740
        return $json;
741
    }
742
743
    private function json_last_error_msg()
744
    {
745
        if (function_exists('json_last_error_msg')) {
746
            return json_last_error_msg();
747
        }
748
749
        static $ERRORS = array(
750
            JSON_ERROR_NONE => 'No error',
751
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
752
            JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
753
            JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
754
            JSON_ERROR_SYNTAX => 'Syntax error',
755
            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
756
        );
757
758
        $error = json_last_error();
759
        return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error';
760
    }
761
}
762