Completed
Push — uv-index ( 44d314...153dea )
by Christian
02:09
created

OpenWeatherMap::getUVIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 4
crap 1
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=\"http://openweathermap.org\">OpenWeatherMap.org</a>";
45
46
    /**
47
     * @var string The basic api url to fetch weather data from.
48
     */
49
    private $weatherUrl = 'http://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 = 'http://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 = 'http://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 = 'http://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 = 'http://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 = 'http://api.openweathermap.org/v3/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 37
    public function __construct($apiKey = '', $fetcher = null, $cache = false, $seconds = 600)
117
    {
118 37
        if (!is_string($apiKey) || empty($apiKey)) {
119
            // BC
120 1
            $seconds = $cache !== false ? $cache : 600;
121 1
            $cache = $fetcher !== null ? $fetcher : false;
122 1
            $fetcher = $apiKey !== '' ? $apiKey : null;
123 1
        } else {
124 37
            $this->apiKey = $apiKey;
125
        }
126
127 37
        if ($cache !== false && !($cache instanceof AbstractCache)) {
128 1
            throw new \InvalidArgumentException('The cache class must implement the FetcherInterface!');
129
        }
130 37
        if (!is_numeric($seconds)) {
131 1
            throw new \InvalidArgumentException('$seconds must be numeric.');
132
        }
133 37
        if (!isset($fetcher)) {
134 15
            $fetcher = (function_exists('curl_version')) ? new CurlFetcher() : new FileGetContentsFetcher();
135 15
        }
136 37
        if ($seconds == 0) {
137 1
            $cache = false;
138 1
        }
139
140 37
        $this->cache = $cache;
141 37
        $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 37
        $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 37
    }
144
145
    /**
146
     * Sets the API Key.
147
     *
148
     * @param string $apiKey API key for the OpenWeatherMap account.
149
     *
150
     * @api
151
     */
152 3
    public function setApiKey($apiKey)
153
    {
154 3
        $this->apiKey = $apiKey;
155 3
    }
156
157
    /**
158
     * Returns the API Key.
159
     *
160
     * @return string
161
     *
162
     * @api
163
     */
164 5
    public function getApiKey()
165
    {
166 5
        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 three 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
     *
187
     * @api
188
     */
189 3
    public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
190
    {
191 3
        $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
192 2
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawWeatherData..., $lang, $appid, 'xml') on line 191 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...
193
194 2
        return new CurrentWeather($xml, $units);
195
    }
196
197
    /**
198
     * Returns the current weather for a group of city ids.
199
     *
200
     * @param array  $ids   The city ids to get weather information for
201
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
202
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
203
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
204
     *
205
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
206
     * @throws \InvalidArgumentException If an argument error occurs.
207
     *
208
     * @return CurrentWeatherGroup
209
     *
210
     * @api
211
     */
212 1
    public function getWeatherGroup($ids, $units = 'imperial', $lang = 'en', $appid = '')
213
    {
214 1
        $answer = $this->getRawWeatherGroupData($ids, $units, $lang, $appid);
215 1
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawWeatherGrou... $units, $lang, $appid) on line 214 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...
216
217 1
        return new CurrentWeatherGroup($json, $units);
218
    }
219
220
    /**
221
     * Returns the forecast for the place you specified. DANGER: Might return
222
     * fewer results than requested due to a bug in the OpenWeatherMap API!
223
     *
224
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
225
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
226
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
227
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
228
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
229
     *
230
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
231
     * @throws \InvalidArgumentException If an argument error occurs.
232
     *
233
     * @return WeatherForecast
234
     *
235
     * @api
236
     */
237 2
    public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
238
    {
239 2
        if ($days <= 5) {
240 1
            $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
241 2
        } elseif ($days <= 16) {
242 1
            $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
243 1
        } else {
244 1
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
245
        }
246 1
        $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...
247
248 1
        return new WeatherForecast($xml, $units, $days);
249
    }
250
251
    /**
252
     * Returns the DAILY forecast for the place you specified. DANGER: Might return
253
     * fewer results than requested due to a bug in the OpenWeatherMap API!
254
     *
255
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
256
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
257
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
258
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
259
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
260
     *
261
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
262
     * @throws \InvalidArgumentException If an argument error occurs.
263
     *
264
     * @return WeatherForecast
265
     *
266
     * @api
267
     */
268 2
    public function getDailyWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
269
    {
270 2
        if ($days > 16) {
271 1
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
272
        }
273
274 1
        $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
275 1
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawDailyForeca..., $appid, 'xml', $days) on line 274 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...
276 1
        return new WeatherForecast($xml, $units, $days);
277
    }
278
279
    /**
280
     * Returns the weather history for the place you specified.
281
     *
282
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
283
     * @param \DateTime        $start
284
     * @param int              $endOrCount
285
     * @param string           $type       Can either be 'tick', 'hour' or 'day'.
286
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
287
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
288
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
289
     *
290
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
291
     * @throws \InvalidArgumentException If an argument error occurs.
292
     *
293
     * @return WeatherHistory
294
     *
295
     * @api
296
     */
297 3
    public function getWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
298
    {
299 3 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...
300 1
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
301
        }
302
303 2
        $xml = json_decode($this->getRawWeatherHistory($query, $start, $endOrCount, $type, $units, $lang, $appid), true);
304
305 2
        if ($xml['cod'] != 200) {
306 2
            throw new OWMException($xml['message'], $xml['cod']);
307
        }
308
309
        return new WeatherHistory($xml, $query);
310
    }
311
312
    /**
313
     * Returns the current uv index at the location you specified.
314
     *
315
     * @param float              $lat           The location's latitude.
316
     * @param float              $lon           The location's longitude.
317
     *
318
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
319
     * @throws \InvalidArgumentException If an argument error occurs.
320
     *
321
     * @return UVIndex The uvi object.
322
     *
323
     * @api
324
     */
325 1
    public function getCurrentUVIndex($lat, $lon)
326
    {
327 1
        $answer = $this->getRawCurrentUVIndexData($lat, $lon);
328 1
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawCurrentUVIndexData($lat, $lon) on line 327 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...
329
330 1
        return new UVIndex($json);
331
    }
332
333
    /**
334
     * Returns the uv index at date, time and location you specified.
335
     *
336
     * @param float              $lat           The location's latitude.
337
     * @param float              $lon           The location's longitude.
338
     * @param \DateTimeInterface $dateTime      The date and time to request data for.
339
     * @param string             $timePrecision This decides about the timespan OWM will look for the uv index. The tighter
340
     *                                          the timespan, the less likely it is to get a result. Can be 'year', 'month',
341
     *                                          'day', 'hour', 'minute' or 'second', defaults to 'day'.
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 1
    public function getUVIndex($lat, $lon, $dateTime, $timePrecision = 'day')
351
    {
352 1
        $answer = $this->getRawUVIndexData($lat, $lon, $dateTime, $timePrecision);
353 1
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData...teTime, $timePrecision) 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 1
        return new UVIndex($json);
356
    }
357
358
    /**
359
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
360
     *
361
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
362
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
363
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
364
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
365
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
366
     *
367
     * @return string Returns false on failure and the fetched data in the format you specified on success.
368
     *
369
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
370
     *
371
     * @api
372
     */
373 4
    public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
374
    {
375 4
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
376
377 3
        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 375 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...
378
    }
379
380
    /**
381
     * Directly returns the JSON string returned by OpenWeatherMap for the group of current weather.
382
     * Only a JSON response format is supported for this webservice.
383
     *
384
     * @param array  $ids   The city ids to get weather information for
385
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
386
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
387
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
388
     *
389
     * @return string Returns false on failure and the fetched data in the format you specified on success.
390
     *
391
     * @api
392
     */
393 1
    public function getRawWeatherGroupData($ids, $units = 'imperial', $lang = 'en', $appid = '')
394
    {
395 1
        $url = $this->buildUrl($ids, $units, $lang, $appid, 'json', $this->weatherGroupUrl);
396
397 1
        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 395 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...
398
    }
399
400
    /**
401
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
402
     *
403
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
404
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
405
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
406
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
407
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
408
     *
409
     * @return string Returns false on failure and the fetched data in the format you specified on success.
410
     *
411
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
412
     *
413
     * @api
414
     */
415 1
    public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
416
    {
417 1
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
418
419 1
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($query, ...atherHourlyForecastUrl) on line 417 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...
420
    }
421
422
    /**
423
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
424
     *
425
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
426
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
427
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
428
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
429
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
430
     * @param int              $cnt   How many days of forecast shall be returned? Maximum (and default): 16
431
     *
432
     * @throws \InvalidArgumentException If $cnt is higher than 16.
433
     *
434
     * @return string Returns false on failure and the fetched data in the format you specified on success.
435
     *
436
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
437
     *
438
     * @api
439
     */
440 3
    public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
441
    {
442 3
        if ($cnt > 16) {
443 1
            throw new \InvalidArgumentException('$cnt must be 16 or lower!');
444
        }
445 2
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
446
447 2
        return $this->cacheOrFetchResult($url);
448
    }
449
450
    /**
451
     * Directly returns the json string returned by OpenWeatherMap for the weather history.
452
     *
453
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
454
     * @param \DateTime        $start      The \DateTime object of the date to get the first weather information from.
455
     * @param \DateTime|int    $endOrCount Can be either a \DateTime object representing the end of the period to
456
     *                                     receive weather history data for or an integer counting the number of
457
     *                                     reports requested.
458
     * @param string           $type       The period of the weather history requested. Can be either be either "tick",
459
     *                                     "hour" or "day".
460
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
461
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
462
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
463
     *
464
     * @throws \InvalidArgumentException
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 occurred, OpenWeatherMap ALWAYS returns data in json format.
469
     *
470
     * @api
471
     */
472 4
    public function getRawWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
473
    {
474 4 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...
475 1
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
476
        }
477
478 3
        $url = $this->buildUrl($query, $units, $lang, $appid, 'json', $this->weatherHistoryUrl);
479 3
        $url .= "&type=$type&start={$start->format('U')}";
480 3
        if ($endOrCount instanceof \DateTime) {
481 1
            $url .= "&end={$endOrCount->format('U')}";
482 3
        } elseif (is_numeric($endOrCount) && $endOrCount > 0) {
483 1
            $url .= "&cnt=$endOrCount";
484 1
        } else {
485 1
            throw new \InvalidArgumentException('$endOrCount must be either a \DateTime or a positive integer.');
486
        }
487
488 2
        return $this->cacheOrFetchResult($url);
489
    }
490
491
    /**
492
     * Directly returns the json string returned by OpenWeatherMap for the current UV index data.
493
     *
494
     * @param float $lat                   The location's latitude.
495
     * @param float $lon                   The location's longitude.
496
     *
497
     * @return bool|string Returns the fetched data.
498
     *
499
     * @api
500
     */
501 5
    public function getRawCurrentUVIndexData($lat, $lon)
502
    {
503 5
        if (!$this->apiKey) {
504 1
            throw new \RuntimeException('Before using this method, you must set the api key using ->setApiKey()');
505
        }
506 4
        if (!is_float($lat) || !is_float($lon)) {
507 3
            throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
508
        }
509 1
        $url = $this->buildUVIndexUrl($lat, $lon);
510
511 1
        return $this->cacheOrFetchResult($url);
512
    }
513
514
    /**
515
     * Directly returns the json string returned by OpenWeatherMap for the UV index data.
516
     *
517
     * @param float $lat                   The location's latitude.
518
     * @param float $lon                   The location's longitude.
519
     * @param \DateTimeInterface $dateTime The date and time to request data for.
520
     * @param string $timePrecision        This decides about the timespan OWM will look for the uv index. The tighter
521
     *                                     the timespan, the less likely it is to get a result. Can be 'year', 'month',
522
     *                                     'day', 'hour', 'minute' or 'second', defaults to 'day'.
523
     *
524
     * @return bool|string Returns the fetched data.
525
     *
526
     * @api
527
     */
528 5
    public function getRawUVIndexData($lat, $lon, $dateTime, $timePrecision = 'day')
529
    {
530 5
        if (!$this->apiKey) {
531 1
            throw new \RuntimeException('Before using this method, you must set the api key using ->setApiKey()');
532
        }
533 4
        if (!is_float($lat) || !is_float($lon)) {
534 2
            throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
535
        }
536 2
        if (interface_exists('DateTimeInterface') && !$dateTime instanceof \DateTimeInterface || !$dateTime instanceof \DateTime) {
537 1
            throw new \InvalidArgumentException('$dateTime must be an instance of \DateTime or \DateTimeInterface');
538
        }
539 1
        $url = $this->buildUVIndexUrl($lat, $lon, $dateTime, $timePrecision);
540
541 1
        return $this->cacheOrFetchResult($url);
542
    }
543
544
    /**
545
     * Returns whether or not the last result was fetched from the cache.
546
     *
547
     * @return bool true if last result was fetched from cache, false otherwise.
548
     */
549 1
    public function wasCached()
550
    {
551 1
        return $this->wasCached;
552
    }
553
554
    /**
555
     * @deprecated Use {@link self::getRawWeatherData()} instead.
556
     */
557
    public function getRawData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
558
    {
559
        return $this->getRawWeatherData($query, $units, $lang, $appid, $mode);
560
    }
561
562
    /**
563
     * Fetches the result or delivers a cached version of the result.
564
     *
565
     * @param string $url
566
     *
567
     * @return string
568
     */
569 10
    private function cacheOrFetchResult($url)
570
    {
571 10
        if ($this->cache !== false) {
572
            /** @var AbstractCache $cache */
573 1
            $cache = $this->cache;
574 1
            $cache->setSeconds($this->seconds);
575 1
            if ($cache->isCached($url)) {
576 1
                $this->wasCached = true;
577 1
                return $cache->getCached($url);
578
            }
579 1
            $result = $this->fetcher->fetch($url);
580 1
            $cache->setCached($url, $result);
581 1
        } else {
582 9
            $result = $this->fetcher->fetch($url);
583
        }
584 10
        $this->wasCached = false;
585
586 10
        return $result;
587
    }
588
589
    /**
590
     * Build the url to fetch weather data from.
591
     *
592
     * @param        $query
593
     * @param        $units
594
     * @param        $lang
595
     * @param        $appid
596
     * @param        $mode
597
     * @param string $url   The url to prepend.
598
     *
599
     * @return bool|string The fetched url, false on failure.
600
     */
601 10
    private function buildUrl($query, $units, $lang, $appid, $mode, $url)
602
    {
603 10
        $queryUrl = $this->buildQueryUrlParameter($query);
604
605 9
        $url = $url."$queryUrl&units=$units&lang=$lang&mode=$mode&APPID=";
606 9
        $url .= empty($appid) ? $this->apiKey : $appid;
607
608 9
        return $url;
609
    }
610
611
    /**
612
     * @param float                        $lat
613
     * @param float                        $lon
614
     * @param \DateTime|\DateTimeImmutable $dateTime
615
     * @param string                       $timePrecision
616
     *
617
     * @return string
618
     */
619 2
    private function buildUVIndexUrl($lat, $lon, $dateTime = null, $timePrecision = null)
620
    {
621 2
        if ($dateTime !== null) {
622 1
            $format = '\Z';
623
            switch ($timePrecision) {
624
                /** @noinspection PhpMissingBreakStatementInspection */
625 1
                case 'second':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
626 1
                    $format = ':s' . $format;
627
                /** @noinspection PhpMissingBreakStatementInspection */
628 1
                case 'minute':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
629 1
                    $format = ':i' . $format;
630
                /** @noinspection PhpMissingBreakStatementInspection */
631 1
                case 'hour':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
632 1
                    $format = '\TH' . $format;
633
                /** @noinspection PhpMissingBreakStatementInspection */
634 1
                case 'day':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
635 1
                    $format = '-d' . $format;
636
                /** @noinspection PhpMissingBreakStatementInspection */
637 1
                case 'month':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
638 1
                    $format = '-m' . $format;
639 1
                case 'year':
640 1
                    $format = 'Y' . $format;
641 1
                    break;
642
                default:
643
                    throw new \InvalidArgumentException('$timePrecision is invalid.');
644
            }
645
            // OWM only accepts UTC timezones.
646 1
            $dateTime->setTimezone(new \DateTimeZone('UTC'));
647 1
            $dateTime = $dateTime->format($format);
648 1
        } else {
649 1
            $dateTime = 'current';
650
        }
651
652 2
        return sprintf($this->uvIndexUrl . '/%s,%s/%s.json?appid=%s', $lat, $lon, $dateTime, $this->apiKey);
653
    }
654
655
    /**
656
     * Builds the query string for the url.
657
     *
658
     * @param mixed $query
659
     *
660
     * @return string The built query string for the url.
661
     *
662
     * @throws \InvalidArgumentException If the query parameter is invalid.
663
     */
664 10
    private function buildQueryUrlParameter($query)
665
    {
666
        switch ($query) {
667 10
            case is_array($query) && isset($query['lat']) && isset($query['lon']) && is_numeric($query['lat']) && is_numeric($query['lon']):
668 1
                return "lat={$query['lat']}&lon={$query['lon']}";
669 10
            case is_array($query) && is_numeric($query[0]):
670 2
                return 'id='.implode(',', $query);
671 8
            case is_numeric($query):
672
                return "id=$query";
673 8
            case is_string($query):
674 7
                return 'q='.urlencode($query);
675 1
            default:
676 1
                throw new \InvalidArgumentException('Error: $query has the wrong format. See the documentation of OpenWeatherMap::getWeather() to read about valid formats.');
677 1
        }
678
    }
679
680
    /**
681
     * @param string $answer The content returned by OpenWeatherMap.
682
     *
683
     * @return \SimpleXMLElement
684
     * @throws OWMException If the content isn't valid XML.
685
     */
686 6
    private function parseXML($answer)
687
    {
688
        // Disable default error handling of SimpleXML (Do not throw E_WARNINGs).
689 6
        libxml_use_internal_errors(true);
690 6
        libxml_clear_errors();
691
        try {
692 6
            return new \SimpleXMLElement($answer);
693 2
        } catch (\Exception $e) {
694
            // Invalid xml format. This happens in case OpenWeatherMap returns an error.
695
            // OpenWeatherMap always uses json for errors, even if one specifies xml as format.
696 2
            $error = json_decode($answer, true);
697 2
            if (isset($error['message'])) {
698 1
                throw new OWMException($error['message'], isset($error['cod']) ? $error['cod'] : 0);
699
            } else {
700 1
                throw new OWMException('Unknown fatal error: OpenWeatherMap returned the following json object: ' . $answer);
701
            }
702
        }
703
    }
704
705
    /**
706
     * @param string $answer The content returned by OpenWeatherMap.
707
     *
708
     * @return \stdClass
709
     * @throws OWMException If the content isn't valid JSON.
710
     */
711 4
    private function parseJson($answer)
712
    {
713 4
        $json = json_decode($answer);
714 4
        if (json_last_error() !== JSON_ERROR_NONE) {
715 1
            throw new OWMException('OpenWeatherMap returned an invalid json object. JSON error was: ' . $this->json_last_error_msg());
716
        }
717 3
        if (isset($json->message)) {
718 1
            throw new OWMException('An error occurred: '. $json->message);
719
        }
720
721 3
        return $json;
722
    }
723
724 1
    private function json_last_error_msg()
725
    {
726 1
        if (function_exists('json_last_error_msg')) {
727 1
            return json_last_error_msg();
728
        }
729
730
        static $ERRORS = array(
731
            JSON_ERROR_NONE => 'No error',
732
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
733
            JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
734
            JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
735
            JSON_ERROR_SYNTAX => 'Syntax error',
736
            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
737
        );
738
739
        $error = json_last_error();
740
        return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error';
741
    }
742
}
743