Completed
Push — OpenWeatherMap-PHP-Api-17 ( 0a6ed3...bbab65 )
by Christian
02:10
created

OpenWeatherMap::parseXML()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
ccs 0
cts 15
cp 0
rs 9.4285
cc 3
eloc 11
nc 3
nop 1
crap 12
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\Exception as OWMException;
23
use Cmfcmf\OpenWeatherMap\Fetcher\CurlFetcher;
24
use Cmfcmf\OpenWeatherMap\Fetcher\FetcherInterface;
25
use Cmfcmf\OpenWeatherMap\Fetcher\FileGetContentsFetcher;
26
use Cmfcmf\OpenWeatherMap\WeatherForecast;
27
use Cmfcmf\OpenWeatherMap\WeatherHistory;
28
29
/**
30
 * Main class for the OpenWeatherMap-PHP-API. Only use this class.
31
 *
32
 * @api
33
 */
34
class OpenWeatherMap
35
{
36
    /**
37
     * The copyright notice. This is no official text, this hint was created
38
     * following to http://openweathermap.org/copyright.
39
     *
40
     * @var string $copyright
41
     */
42
    const COPYRIGHT = "Weather data from <a href=\"http://www.openweathermap.org\">OpenWeatherMap.org</a>";
43
44
    /**
45
     * @var string $weatherUrl The basic api url to fetch weather data from.
46
     */
47
    private $weatherUrl = "http://api.openweathermap.org/data/2.5/weather?";
48
49
    /**
50
     * @var string $url The basic api url to fetch weekly forecast data from.
51
     */
52
    private $weatherHourlyForecastUrl = "http://api.openweathermap.org/data/2.5/forecast?";
53
54
    /**
55
     * @var string $url The basic api url to fetch daily forecast data from.
56
     */
57
    private $weatherDailyForecastUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?";
58
59
    /**
60
     * @var string $url The basic api url to fetch history weather data from.
61
     */
62
    private $weatherHistoryUrl = "http://api.openweathermap.org/data/2.5/history/city?";
63
64
    /**
65
     * @var AbstractCache|bool $cacheClass The cache class.
66
     */
67
    private $cacheClass = false;
68
69
    /**
70
     * @var int
71
     */
72
    private $seconds;
73
74
    /**
75
     * @var bool
76
     */
77
    private $wasCached = false;
78
    
79
    /**
80
     * @var FetcherInterface The url fetcher.
81
     */
82
    
83
    private $fetcher;
84
85
    /**
86
     * Constructs the OpenWeatherMap object.
87
     *
88
     * @param null|FetcherInterface $fetcher    The interface to fetch the data from OpenWeatherMap. Defaults to
89
     *                                          CurlFetcher() if cURL is available. Otherwise defaults to
90
     *                                          FileGetContentsFetcher() using 'file_get_contents()'.
91
     * @param bool|string           $cacheClass If set to false, caching is disabled. Otherwise this must be a class
92
     *                                          extending AbstractCache. Defaults to false.
93
     * @param int                   $seconds    How long weather data shall be cached. Default 10 minutes.
94
     *
95
     * @throws \Exception If $cache is neither false nor a valid callable extending Cmfcmf\OpenWeatherMap\Util\Cache.
96
     *
97
     * @api
98
     */
99
    public function __construct($fetcher = null, $cacheClass = false, $seconds = 600)
100
    {
101
        if ($cacheClass !== false && !($cacheClass instanceof AbstractCache)) {
102
            throw new \Exception("The cache class must implement the FetcherInterface!");
103
        }
104
        if (!is_numeric($seconds)) {
105
            throw new \Exception("\$seconds must be numeric.");
106
        }
107
        if (!isset($fetcher)) {
108
            $fetcher = (function_exists('curl_version')) ? new CurlFetcher() : new FileGetContentsFetcher();
109
        }
110
        if ($seconds == 0) {
111
            $cacheClass = false;
112
        }
113
114
        $this->cacheClass = $cacheClass;
115
        $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...
116
        $this->fetcher = $fetcher;
117
    }
118
119
    /**
120
     * Returns the current weather at the place you specified as an object.
121
     *
122
     * @param array|int|string $query The place to get weather information for. For possible values see below.
123
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
124
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
125
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
126
     *
127
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
128
     * @throws \InvalidArgumentException If an argument error occurs.
129
     *
130
     * @return CurrentWeather The weather object.
131
     *
132
     * There are three ways to specify the place to get weather information for:
133
     * - Use the city name: $query must be a string containing the city name.
134
     * - Use the city id: $query must be an integer containing the city id.
135
     * - Use the coordinates: $query must be an associative array containing the 'lat' and 'lon' values.
136
     *
137
     * @api
138
     */
139
    public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
140
    {
141
        $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
142
        $xml = $this->parseXML($answer);
0 ignored issues
show
Bug introduced by
It seems like $answer defined by $this->getRawWeatherData..., $lang, $appid, 'xml') on line 141 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...
143
144
        return new CurrentWeather($xml, $units);
145
    }
146
147
    /**
148
     * Returns the current weather at the place you specified as an object.
149
     *
150
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
151
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
152
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
153
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
154
     * @param int              $days  For how much days you want to get a forecast. Default 1, maximum: 16.
155
     *
156
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
157
     * @throws \InvalidArgumentException If an argument error occurs.
158
     *
159
     * @return WeatherForecast
160
     *
161
     * @api
162
     */
163
    public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
164
    {
165
        if ($days <= 5) {
166
            $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
167
        } elseif ($days <= 16) {
168
            $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
169
        } else {
170
            throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
171
        }
172
        $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...
173
174
        return new WeatherForecast($xml, $units, $days);
175
    }
176
177
    /**
178
     * Returns the weather history for the place you specified as an object.
179
     *
180
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
181
     * @param \DateTime        $start
182
     * @param int              $endOrCount
183
     * @param string           $type
184
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
185
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
186
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
187
     *
188
     * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
189
     * @throws \InvalidArgumentException If an argument error occurs.
190
     *
191
     * @return WeatherHistory
192
     *
193
     * @api
194
     */
195
    public function getWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
196
    {
197 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...
198
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
199
        }
200
201
        $xml = json_decode($this->getRawWeatherHistory($query, $start, $endOrCount, $type, $units, $lang, $appid), true);
202
203
        if ($xml['cod'] != 200) {
204
            throw new OWMException($xml['message'], $xml['cod']);
205
        }
206
207
        return new WeatherHistory($xml, $query);
208
    }
209
210
    /**
211
     * @deprecated Use {@link self::getRawWeatherData()} instead.
212
     */
213
    public function getRawData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
214
    {
215
        return $this->getRawWeatherData($query, $units, $lang, $appid, $mode);
216
    }
217
218
    /**
219
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
220
     *
221
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
222
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
223
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
224
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
225
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
226
     *
227
     * @return string Returns false on failure and the fetched data in the format you specified on success.
228
     *
229
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
230
     *
231
     * @api
232
     */
233
    public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
234
    {
235
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
236
237
        return $this->cacheOrFetchResult($url);
238
    }
239
240
    /**
241
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
242
     *
243
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
244
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
245
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
246
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
247
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
248
     *
249
     * @return string Returns false on failure and the fetched data in the format you specified on success.
250
     *
251
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
252
     *
253
     * @api
254
     */
255
    public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
256
    {
257
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
258
259
        return $this->cacheOrFetchResult($url);
260
    }
261
262
    /**
263
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
264
     *
265
     * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
266
     * @param string           $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
267
     * @param string           $lang  The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
268
     * @param string           $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
269
     * @param string           $mode  The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
270
     * @param int              $cnt   How many days of forecast shall be returned? Maximum (and default): 16
271
     *
272
     * @throws \InvalidArgumentException If $cnt is higher than 16.
273
     * @return string Returns false on failure and the fetched data in the format you specified on success.
274
     *
275
     * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
276
     *
277
     * @api
278
     */
279
    public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
280
    {
281
        if ($cnt > 16) {
282
            throw new \InvalidArgumentException('$cnt must be 16 or lower!');
283
        }
284
        $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
285
286
        return $this->cacheOrFetchResult($url);
287
    }
288
289
    /**
290
     * Directly returns the xml/json/html string returned by OpenWeatherMap for the weather history.
291
     *
292
     * @param array|int|string $query           The place to get weather information for. For possible values see ::getWeather.
293
     * @param \DateTime        $start           The \DateTime object of the date to get the first weather information from.
294
     * @param \DateTime|int    $endOrCount      Can be either a \DateTime object representing the end of the period to
295
     *                                          receive weather history data for or an integer counting the number of
296
     *                                          reports requested.
297
     * @param string           $type            The period of the weather history requested. Can be either be either "tick",
298
     *                                          "hour" or "day".
299
     * @param string           $units           Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
300
     * @param string           $lang            The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
301
     * @param string           $appid           Your app id, default ''. See http://openweathermap.org/appid for more details.
302
     *
303
     * @throws \InvalidArgumentException
304
     *
305
     * @return string Returns false on failure and the fetched data in the format you specified on success.
306
     *
307
     * Warning If an error occurred, OpenWeatherMap ALWAYS returns data in json format.
308
     *
309
     * @api
310
     */
311
    public function getRawWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
312
    {
313 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...
314
            throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
315
        }
316
317
        $queryUrl = $this->weatherHistoryUrl . $this->buildQueryUrlParameter($query) . "&start={$start->format('U')}";
318
319
        if ($endOrCount instanceof \DateTime) {
320
            $queryUrl .= "&end={$endOrCount->format('U')}";
321
        } elseif (is_numeric($endOrCount) && $endOrCount > 0) {
322
            $queryUrl .= "&cnt=$endOrCount";
323
        } else {
324
            throw new \InvalidArgumentException('$endOrCount must be either a \DateTime or a positive integer.');
325
        }
326
        $queryUrl .= "&type=$type&units=$units&lang=$lang";
327
328
        if (!empty($appid)) {
329
            $queryUrl .= "&APPID=$appid";
330
        }
331
332
        return $this->cacheOrFetchResult($queryUrl);
333
    }
334
335
    /**
336
     * Returns whether or not the last result was fetched from the cache.
337
     *
338
     * @return bool true if last result was fetched from cache, false otherwise.
339
     */
340
    public function wasCached()
341
    {
342
        return $this->wasCached;
343
    }
344
345
    /**
346
     * Fetches the result or delivers a cached version of the result.
347
     *
348
     * @param string $url
349
     *
350
     * @return string
351
     */
352
    private function cacheOrFetchResult($url)
353
    {
354
        if ($this->cacheClass !== false) {
355
            /** @var AbstractCache $cache */
356
            $cache = $this->cacheClass;
357
            $cache->setSeconds($this->seconds);
358
            if ($cache->isCached($url)) {
359
                $this->wasCached = true;
360
                return $cache->getCached($url);
361
            }
362
            $result = $this->fetcher->fetch($url);
363
            $cache->setCached($url, $result);
364
        } else {
365
            $result = $this->fetcher->fetch($url);
366
        }
367
        $this->wasCached = false;
368
369
        return $result;
370
    }
371
372
    /**
373
     * Build the url to fetch weather data from.
374
     *
375
     * @param        $query
376
     * @param        $units
377
     * @param        $lang
378
     * @param        $appid
379
     * @param        $mode
380
     * @param string $url The url to prepend.
381
     *
382
     * @return bool|string The fetched url, false on failure.
383
     */
384
    private function buildUrl($query, $units, $lang, $appid, $mode, $url)
385
    {
386
        $queryUrl = $this->buildQueryUrlParameter($query);
387
388
        $url = $url . "$queryUrl&units=$units&lang=$lang&mode=$mode";
389
        if (!empty($appid)) {
390
            $url .= "&APPID=$appid";
391
        }
392
393
        return $url;
394
    }
395
396
    /**
397
     * Builds the query string for the url.
398
     *
399
     * @param mixed $query
400
     *
401
     * @return string The built query string for the url.
402
     * @throws \InvalidArgumentException If the query parameter is invalid.
403
     */
404
    private function buildQueryUrlParameter($query)
405
    {
406
        switch ($query) {
407
            case is_array($query) && isset($query['lat']) && isset($query['lon']) && is_numeric($query['lat']) && is_numeric($query['lon']):
408
                return "lat={$query['lat']}&lon={$query['lon']}";
409
            case is_numeric($query):
410
                return "id=$query";
411
            case is_string($query):
412
                return "q=" . urlencode($query);
413
            default:
414
                throw new \InvalidArgumentException('Error: $query has the wrong format. See the documentation of OpenWeatherMap::getRawData() to read about valid formats.');
415
        }
416
    }
417
418
    /**
419
     * @param string $answer The content returned by OpenWeatherMap.
420
     *
421
     * @return \SimpleXMLElement
422
     * @throws OWMException If the content isn't valid XML.
423
     */
424
    private function parseXML($answer)
425
    {
426
        // Disable default error handling of SimpleXML (Do not throw E_WARNINGs).
427
        libxml_use_internal_errors(true);
428
        libxml_clear_errors();
429
        try {
430
            return new \SimpleXMLElement($answer);
431
        } catch (\Exception $e) {
432
            // Invalid xml format. This happens in case OpenWeatherMap returns an error.
433
            // OpenWeatherMap always uses json for errors, even if one specifies xml as format.
434
            $error = json_decode($answer, true);
435
            if (isset($error['message'])) {
436
                throw new OWMException($error['message'], $error['cod']);
437
            } else {
438
                throw new OWMException('Unknown fatal error: OpenWeatherMap returned the following json object: ' . $answer);
439
            }
440
        }
441
    }
442
}
443