Completed
Pull Request — v3 (#135)
by Christian
30:58 queued 15:52
created

OpenWeatherMap   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 716
Duplicated Lines 3.35 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 86
lcom 1
cbo 12
dl 24
loc 716
rs 1.884
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A buildUrl() 0 9 2
A buildUVIndexUrl() 0 27 4
C buildQueryUrlParameter() 0 18 12
A parseXML() 0 18 4
A parseJson() 0 13 3
A json_last_error_msg() 0 18 3
A __construct() 0 16 4
A setApiKey() 0 4 1
A getApiKey() 0 4 1
A getWeather() 0 7 1
A getWeatherGroup() 0 7 1
A getWeatherForecast() 0 13 3
A getDailyWeatherForecast() 0 10 2
A getWeatherHistory() 3 14 3
A getCurrentUVIndex() 0 7 1
A getForecastUVIndex() 9 9 1
A getHistoricUVIndex() 9 9 1
A getRawWeatherData() 0 6 1
A getRawWeatherGroupData() 0 6 1
A getRawHourlyForecastData() 0 6 1
A getRawDailyForecastData() 0 9 2
A getRawWeatherHistory() 3 18 5
D getRawUVIndexData() 0 28 24
A wasCached() 0 4 1
A cacheOrFetchResult() 0 28 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OpenWeatherMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OpenWeatherMap, and based on these observations, apply Extract Interface, too.

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\CurrentWeather;
21
use Cmfcmf\OpenWeatherMap\UVIndex;
22
use Cmfcmf\OpenWeatherMap\CurrentWeatherGroup;
23
use Cmfcmf\OpenWeatherMap\Exception as OWMException;
24
use Cmfcmf\OpenWeatherMap\WeatherForecast;
25
use Cmfcmf\OpenWeatherMap\WeatherHistory;
26
use Psr\Cache\CacheItemPoolInterface;
27
use Psr\Http\Client\ClientInterface;
28
use Psr\Http\Message\RequestFactoryInterface;
29
30
/**
31
 * Main class for the OpenWeatherMap-PHP-API. Only use this class.
32
 *
33
 * @api
34
 */
35
class OpenWeatherMap
36
{
37
    /**
38
     * The copyright notice. This is no official text, it was created by
39
     * following the guidelines at http://openweathermap.org/copyright.
40
     *
41
     * @var string $copyright
42
     */
43
    const COPYRIGHT = "Weather data from <a href=\"https://openweathermap.org\">OpenWeatherMap.org</a>";
44
45
    /**
46
     * @var string The basic api url to fetch weather data from.
47
     */
48
    private $weatherUrl = 'https://api.openweathermap.org/data/2.5/weather?';
49
50
    /**
51
     * @var string The basic api url to fetch weather group data from.
52
     */
53
    private $weatherGroupUrl = 'https://api.openweathermap.org/data/2.5/group?';
54
55
    /**
56
     * @var string The basic api url to fetch weekly forecast data from.
57
     */
58
    private $weatherHourlyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast?';
59
60
    /**
61
     * @var string The basic api url to fetch daily forecast data from.
62
     */
63
    private $weatherDailyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast/daily?';
64
65
    /**
66
     * @var string The basic api url to fetch history weather data from.
67
     */
68
    private $weatherHistoryUrl = 'https://history.openweathermap.org/data/2.5/history/city?';
69
70
    /**
71
     * @var string The basic api url to fetch uv index data from.
72
     */
73
    private $uvIndexUrl = 'https://api.openweathermap.org/data/2.5/uvi';
74
75
    /**
76
     * @var CacheItemPoolInterface|null $cache The cache to use.
77
     */
78
    private $cache = null;
79
80
    /**
81
     * @var int
82
     */
83
    private $ttl;
84
85
    /**
86
     * @var bool
87
     */
88
    private $wasCached = false;
89
90
    /**
91
     * @var ClientInterface
92
     */
93
    private $httpClient;
94
95
    /**
96
     * @var RequestFactoryInterface
97
     */
98
    private $httpRequestFactory;
99
100
    /**
101
     * @var string
102
     */
103
    private $apiKey = '';
104
105
    /**
106
     * Constructs the OpenWeatherMap object.
107
     *
108
     * @param string                      $apiKey             The OpenWeatherMap API key. Required.
109
     * @param ClientInterface             $httpClient         A PSR-18 compatible HTTP client implementation.
110
     * @param RequestFactoryInterface     $httpRequestFactory A PSR-17 compatbile HTTP request factory implementation.
111
     * @param null|CacheItemPoolInterface $cache              If set to null, caching is disabled. Otherwise this must be
112
     *                                                        a PSR 16-compatible cache instance.
113
     * @param int                         $ttl                How long weather data shall be cached. Defaults to 10 minutes.
114
     *                                                        Only used if $cache is not null.
115
     *
116
     * @api
117
     */
118
    public function __construct($apiKey, $httpClient, $httpRequestFactory, $cache = null, $ttl = 600)
119
    {
120
        if (!is_string($apiKey) || empty($apiKey)) {
121
            throw new \InvalidArgumentException("You must provide an API key.");
122
        }
123
124
        if (!is_numeric($ttl)) {
125
            throw new \InvalidArgumentException('$ttl must be numeric.');
126
        }
127
128
        $this->apiKey = $apiKey;
129
        $this->httpClient = $httpClient;
130
        $this->httpRequestFactory = $httpRequestFactory;
131
        $this->cache = $cache;
132
        $this->ttl = $ttl;
0 ignored issues
show
Documentation Bug introduced by
It seems like $ttl can also be of type double or string. However, the property $ttl is declared as type integer. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
133
    }
134
135
    /**
136
     * Sets the API Key.
137
     *
138
     * @param string $apiKey API key for the OpenWeatherMap account.
139
     *
140
     * @api
141
     */
142
    public function setApiKey($apiKey)
143
    {
144
        $this->apiKey = $apiKey;
145
    }
146
147
    /**
148
     * Returns the API Key.
149
     *
150
     * @return string
151
     *
152
     * @api
153
     */
154
    public function getApiKey()
155
    {
156
        return $this->apiKey;
157
    }
158
159
    /**
160
     * Returns the current weather at the place you specified.
161
     *
162
     * @param array|int|string $query The place to get weather information for. For possible values see below.
163
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
164
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
165
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
166
     *
167
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
168
     * @throws \InvalidArgumentException If an argument error occurs.
169
     *
170
     * @return CurrentWeather The weather object.
171
     *
172
     * There are four ways to specify the place to get weather information for:
173
     * - Use the city name: $query must be a string containing the city name.
174
     * - Use the city id: $query must be an integer containing the city id.
175
     * - Use the coordinates: $query must be an associative array containing the 'lat' and 'lon' values.
176
     * - Use the zip code: $query must be a string, prefixed with "zip:"
177
     *
178
     * Zip code may specify country. e.g., "zip:77070" (Houston, TX, US) or "zip:500001,IN" (Hyderabad, India)
179
     *
180
     * @api
181
     */
182
    public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
183
    {
184
        $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
185
        $xml = $this->parseXML($answer);
186
187
        return new CurrentWeather($xml, $units);
188
    }
189
190
    /**
191
     * Returns the current weather for a group of city ids.
192
     *
193
     * @param array  $ids   The city ids to get weather information for
194
     * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
195
     * @param string $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
196
     * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
197
     *
198
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
199
     * @throws \InvalidArgumentException If an argument error occurs.
200
     *
201
     * @return CurrentWeatherGroup
202
     *
203
     * @api
204
     */
205
    public function getWeatherGroup($ids, $units = 'imperial', $lang = 'en', $appid = '')
206
    {
207
        $answer = $this->getRawWeatherGroupData($ids, $units, $lang, $appid);
208
        $json = $this->parseJson($answer);
209
210
        return new CurrentWeatherGroup($json, $units);
0 ignored issues
show
Bug introduced by
It seems like $json defined by $this->parseJson($answer) on line 208 can also be of type array; however, Cmfcmf\OpenWeatherMap\Cu...herGroup::__construct() does only seem to accept object<stdClass>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
211
    }
212
213
    /**
214
     * Returns the forecast for the place you specified. DANGER: Might return
215
     * fewer results than requested due to a bug in the OpenWeatherMap API!
216
     *
217
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
218
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
219
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
220
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
221
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
222
     *
223
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
224
     * @throws \InvalidArgumentException If an argument error occurs.
225
     *
226
     * @return WeatherForecast
227
     *
228
     * @api
229
     */
230
    public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
231
    {
232
        if ($days <= 5) {
233
            $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
234
        } elseif ($days <= 16) {
235
            $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
236
        } else {
237
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
238
        }
239
        $xml = $this->parseXML($answer);
240
241
        return new WeatherForecast($xml, $units, $days);
242
    }
243
244
    /**
245
     * Returns the DAILY forecast for the place you specified. DANGER: Might return
246
     * fewer results than requested due to a bug in the OpenWeatherMap API!
247
     *
248
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
249
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
250
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
251
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
252
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
253
     *
254
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
255
     * @throws \InvalidArgumentException If an argument error occurs.
256
     *
257
     * @return WeatherForecast
258
     *
259
     * @api
260
     */
261
    public function getDailyWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
262
    {
263
        if ($days > 16) {
264
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
265
        }
266
267
        $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
268
        $xml = $this->parseXML($answer);
269
        return new WeatherForecast($xml, $units, $days);
270
    }
271
272
    /**
273
     * Returns the weather history for the place you specified.
274
     *
275
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
276
     * @param \DateTime        $start
277
     * @param int              $endOrCount
278
     * @param string           $type       Can either be 'tick', 'hour' or 'day'.
279
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
280
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
281
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
282
     *
283
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
284
     * @throws \InvalidArgumentException If an argument error occurs.
285
     *
286
     * @return WeatherHistory
287
     *
288
     * @api
289
     */
290
    public function getWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
291
    {
292 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...
293
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
294
        }
295
296
        $xml = json_decode($this->getRawWeatherHistory($query, $start, $endOrCount, $type, $units, $lang, $appid), true);
297
298
        if ($xml['cod'] != 200) {
299
            throw new OWMException($xml['message'], $xml['cod']);
300
        }
301
302
        return new WeatherHistory($xml, $query);
303
    }
304
305
    /**
306
     * Returns the current uv index at the location you specified.
307
     *
308
     * @param float $lat The location's latitude.
309
     * @param float $lon The location's longitude.
310
     *
311
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
312
     * @throws \InvalidArgumentException If an argument error occurs.
313
     *
314
     * @return UVIndex
315
     *
316
     * @api
317
     */
318
    public function getCurrentUVIndex($lat, $lon)
319
    {
320
        $answer = $this->getRawUVIndexData('current', $lat, $lon);
321
        $json = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData('current', $lat, $lon) on line 320 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...
322
323
        return new UVIndex($json);
0 ignored issues
show
Bug introduced by
It seems like $json defined by $this->parseJson($answer) on line 321 can also be of type array; however, Cmfcmf\OpenWeatherMap\UVIndex::__construct() does only seem to accept object, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
324
    }
325
326
    /**
327
     * Returns a forecast of the uv index at the specified location.
328
     * The optional $cnt parameter determines the number of days to forecase.
329
     * The maximum supported number of days is 8.
330
     *
331
     * @param float $lat The location's latitude.
332
     * @param float $lon The location's longitude.
333
     * @param int   $cnt Number of returned days (default to 8).
334
     *
335
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
336
     * @throws \InvalidArgumentException If an argument error occurs.
337
     *
338
     * @return UVIndex[]
339
     *
340
     * @api
341
     */
342 View Code Duplication
    public function getForecastUVIndex($lat, $lon, $cnt = 8)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
343
    {
344
        $answer = $this->getRawUVIndexData('forecast', $lat, $lon, $cnt);
345
        $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 344 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...
346
347
        return array_map(function ($entry) {
348
            return new UVIndex($entry);
349
        }, $data);
350
    }
351
352
    /**
353
     * Returns the historic uv index at the specified location.
354
     *
355
     * @param float     $lat   The location's latitude.
356
     * @param float     $lon   The location's longitude.
357
     * @param \DateTime $start Starting point of time period.
358
     * @param \DateTime $end   Final point of time period.
359
     *
360
     * @throws OpenWeatherMap\Exception  If OpenWeatherMap returns an error.
361
     * @throws \InvalidArgumentException If an argument error occurs.
362
     *
363
     * @return UVIndex[]
364
     *
365
     * @api
366
     */
367 View Code Duplication
    public function getHistoricUVIndex($lat, $lon, $start, $end)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
368
    {
369
        $answer = $this->getRawUVIndexData('historic', $lat, $lon, null, $start, $end);
370
        $data = $this->parseJson($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawUVIndexData...on, null, $start, $end) on line 369 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...
371
372
        return array_map(function ($entry) {
373
            return new UVIndex($entry);
374
        }, $data);
375
    }
376
377
    /**
378
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
379
     *
380
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
381
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
382
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
383
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
384
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
385
     *
386
     * @return string Returns false on failure and the fetched data in the format you specified on success.
387
     *
388
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
389
     *
390
     * @api
391
     */
392
    public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
393
    {
394
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
395
396
        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 394 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...
397
    }
398
399
    /**
400
     * Directly returns the JSON string returned by OpenWeatherMap for the group of current weather.
401
     * Only a JSON response format is supported for this webservice.
402
     *
403
     * @param array  $ids   The city ids to get weather information for
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
     *
408
     * @return string Returns false on failure and the fetched data in the format you specified on success.
409
     *
410
     * @api
411
     */
412
    public function getRawWeatherGroupData($ids, $units = 'imperial', $lang = 'en', $appid = '')
413
    {
414
        $url = $this->buildUrl($ids, $units, $lang, $appid, 'json', $this->weatherGroupUrl);
415
416
        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 414 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...
417
    }
418
419
    /**
420
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
421
     *
422
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
423
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
424
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
425
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
426
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
427
     *
428
     * @return string Returns false on failure and the fetched data in the format you specified on success.
429
     *
430
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
431
     *
432
     * @api
433
     */
434
    public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
435
    {
436
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
437
438
        return $this->cacheOrFetchResult($url);
0 ignored issues
show
Bug introduced by
It seems like $url defined by $this->buildUrl($query, ...atherHourlyForecastUrl) on line 436 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...
439
    }
440
441
    /**
442
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
443
     *
444
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
445
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
446
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
447
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
448
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
449
     * @param int              $cnt   How many days of forecast shall be returned? Maximum (and default): 16
450
     *
451
     * @throws \InvalidArgumentException If $cnt is higher than 16.
452
     *
453
     * @return string Returns false on failure and the fetched data in the format you specified on success.
454
     *
455
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
456
     *
457
     * @api
458
     */
459
    public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
460
    {
461
        if ($cnt > 16) {
462
            throw new \InvalidArgumentException('$cnt must be 16 or lower!');
463
        }
464
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
465
466
        return $this->cacheOrFetchResult($url);
467
    }
468
469
    /**
470
     * Directly returns the json string returned by OpenWeatherMap for the weather history.
471
     *
472
     * @param array|int|string $query      The place to get weather information for. For possible values see ::getWeather.
473
     * @param \DateTime        $start      The \DateTime object of the date to get the first weather information from.
474
     * @param \DateTime|int    $endOrCount Can be either a \DateTime object representing the end of the period to
475
     *                                     receive weather history data for or an integer counting the number of
476
     *                                     reports requested.
477
     * @param string           $type       The period of the weather history requested. Can be either be either "tick",
478
     *                                     "hour" or "day".
479
     * @param string           $units      Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
480
     * @param string           $lang       The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
481
     * @param string           $appid      Your app id, default ''. See http://openweathermap.org/appid for more details.
482
     *
483
     * @throws \InvalidArgumentException
484
     *
485
     * @return string Returns false on failure and the fetched data in the format you specified on success.
486
     *
487
     * Warning If an error occurred, OpenWeatherMap ALWAYS returns data in json format.
488
     *
489
     * @api
490
     */
491
    public function getRawWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
492
    {
493 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...
494
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
495
        }
496
497
        $url = $this->buildUrl($query, $units, $lang, $appid, 'json', $this->weatherHistoryUrl);
498
        $url .= "&type=$type&start={$start->format('U')}";
499
        if ($endOrCount instanceof \DateTime) {
500
            $url .= "&end={$endOrCount->format('U')}";
501
        } elseif (is_numeric($endOrCount) && $endOrCount > 0) {
502
            $url .= "&cnt=$endOrCount";
503
        } else {
504
            throw new \InvalidArgumentException('$endOrCount must be either a \DateTime or a positive integer.');
505
        }
506
507
        return $this->cacheOrFetchResult($url);
508
    }
509
510
    /**
511
     * Directly returns the json string returned by OpenWeatherMap for the UV index data.
512
     *
513
     * @param string    $mode  The type of requested data (['historic', 'forecast', 'current']).
514
     * @param float     $lat   The location's latitude.
515
     * @param float     $lon   The location's longitude.
516
     * @param int       $cnt   Number of returned days (only allowed for 'forecast' data).
517
     * @param \DateTime $start Starting point of time period (only allowed and required for 'historic' data).
518
     * @param \DateTime $end   Final point of time period (only allowed and required for 'historic' data).
519
     *
520
     * @return bool|string Returns the fetched data.
521
     *
522
     * @api
523
     */
524
    public function getRawUVIndexData($mode, $lat, $lon, $cnt = null, $start = null, $end = null)
525
    {
526
        if (!in_array($mode, array('current', 'forecast', 'historic'), true)) {
527
            throw new \InvalidArgumentException("$mode must be one of 'historic', 'forecast', 'current'.");
528
        }
529
        if (!is_float($lat) || !is_float($lon)) {
530
            throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
531
        }
532
        if (isset($cnt) && (!is_int($cnt) || $cnt > 8 || $cnt < 1)) {
533
            throw new \InvalidArgumentException('$cnt must be an int between 1 and 8');
534
        }
535
        if (isset($start) && !$start instanceof \DateTime) {
536
            throw new \InvalidArgumentException('$start must be an instance of \DateTime');
537
        }
538
        if (isset($end) && !$end instanceof \DateTime) {
539
            throw new \InvalidArgumentException('$end must be an instance of \DateTime');
540
        }
541
        if ($mode === 'current' && (isset($start) || isset($end) || isset($cnt))) {
542
            throw new \InvalidArgumentException('Neither $start, $end, nor $cnt must be set for current data.');
543
        } elseif ($mode === 'forecast' && (isset($start) || isset($end) || !isset($cnt))) {
544
            throw new \InvalidArgumentException('$cnt needs to be set and both $start and $end must not be set for forecast data.');
545
        } elseif ($mode === 'historic' && (!isset($start) || !isset($end) || isset($cnt))) {
546
            throw new \InvalidArgumentException('Both $start and $end need to be set and $cnt must not be set for historic data.');
547
        }
548
549
        $url = $this->buildUVIndexUrl($mode, $lat, $lon, $cnt, $start, $end);
550
        return $this->cacheOrFetchResult($url);
551
    }
552
553
    /**
554
     * Returns whether or not the last result was fetched from the cache.
555
     *
556
     * @return bool true if last result was fetched from cache, false otherwise.
557
     */
558
    public function wasCached()
559
    {
560
        return $this->wasCached;
561
    }
562
563
    /**
564
     * Fetches the result or delivers a cached version of the result.
565
     *
566
     * @param string $url
567
     *
568
     * @return string
569
     */
570
    private function cacheOrFetchResult($url)
571
    {
572
        if ($this->cache !== null) {
573
            $key = str_replace(
574
                ["{", "}", "(", ")", "/", "\\", "@", ":"],
575
                ["_", "_", "_", "_", "_", "_",  "_", "_"],
576
                $url);
577
            $item = $this->cache->getItem($key);
578
            if ($item->isHit()) {
579
                $this->wasCached = true;
580
                return $item->get();
581
            }
582
        }
583
584
        $result = $this->httpClient
585
            ->sendRequest($this->httpRequestFactory->createRequest("GET", $url))
586
            ->getBody()
587
            ->getContents();
588
589
        if ($this->cache !== null) {
590
            $item->set($result);
0 ignored issues
show
Bug introduced by
The variable $item does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

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

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