OpenWeatherMap.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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. namespace Cmfcmf;
  18. use Cmfcmf\OpenWeatherMap\AbstractCache;
  19. use Cmfcmf\OpenWeatherMap\CurrentWeather;
  20. use Cmfcmf\OpenWeatherMap\UVIndex;
  21. use Cmfcmf\OpenWeatherMap\CurrentWeatherGroup;
  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. * Main class for the OpenWeatherMap-PHP-API. Only use this class.
  30. *
  31. * @api
  32. */
  33. class OpenWeatherMap
  34. {
  35. /**
  36. * The copyright notice. This is no official text, it was created by
  37. * following the guidelines at http://openweathermap.org/copyright.
  38. *
  39. * @var string $copyright
  40. */
  41. const COPYRIGHT = "Weather data from <a href=\"https://openweathermap.org\">OpenWeatherMap.org</a>";
  42. /**
  43. * @var string The basic api url to fetch weather data from.
  44. */
  45. private $weatherUrl = 'https://api.openweathermap.org/data/2.5/weather?';
  46. /**
  47. * @var string The basic api url to fetch weather group data from.
  48. */
  49. private $weatherGroupUrl = 'https://api.openweathermap.org/data/2.5/group?';
  50. /**
  51. * @var string The basic api url to fetch weekly forecast data from.
  52. */
  53. private $weatherHourlyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast?';
  54. /**
  55. * @var string The basic api url to fetch daily forecast data from.
  56. */
  57. private $weatherDailyForecastUrl = 'https://api.openweathermap.org/data/2.5/forecast/daily?';
  58. /**
  59. * @var string The basic api url to fetch history weather data from.
  60. */
  61. private $weatherHistoryUrl = 'https://history.openweathermap.org/data/2.5/history/city?';
  62. /**
  63. * @var string The basic api url to fetch uv index data from.
  64. */
  65. private $uvIndexUrl = 'https://api.openweathermap.org/v3/uvi';
  66. /**
  67. * @var AbstractCache|bool $cache The cache to use.
  68. */
  69. private $cache = false;
  70. /**
  71. * @var int
  72. */
  73. private $seconds;
  74. /**
  75. * @var bool
  76. */
  77. private $wasCached = false;
  78. /**
  79. * @var FetcherInterface The url fetcher.
  80. */
  81. private $fetcher;
  82. /**
  83. * @var string
  84. */
  85. private $apiKey = '';
  86. /**
  87. * Constructs the OpenWeatherMap object.
  88. *
  89. * @param string $apiKey The OpenWeatherMap API key. Required and only optional for BC.
  90. * @param null|FetcherInterface $fetcher The interface to fetch the data from OpenWeatherMap. Defaults to
  91. * CurlFetcher() if cURL is available. Otherwise defaults to
  92. * FileGetContentsFetcher() using 'file_get_contents()'.
  93. * @param bool|string $cache If set to false, caching is disabled. Otherwise this must be a class
  94. * extending AbstractCache. Defaults to false.
  95. * @param int $seconds How long weather data shall be cached. Default 10 minutes.
  96. *
  97. * @throws \Exception If $cache is neither false nor a valid callable extending Cmfcmf\OpenWeatherMap\Util\Cache.
  98. *
  99. * @api
  100. */
  101. public function __construct($apiKey = '', $fetcher = null, $cache = false, $seconds = 600)
  102. {
  103. if (!is_string($apiKey) || empty($apiKey)) {
  104. // BC
  105. $seconds = $cache !== false ? $cache : 600;
  106. $cache = $fetcher !== null ? $fetcher : false;
  107. $fetcher = $apiKey !== '' ? $apiKey : null;
  108. } else {
  109. $this->apiKey = $apiKey;
  110. }
  111. if ($cache !== false && !($cache instanceof AbstractCache)) {
  112. throw new \InvalidArgumentException('The cache class must implement the FetcherInterface!');
  113. }
  114. if (!is_numeric($seconds)) {
  115. throw new \InvalidArgumentException('$seconds must be numeric.');
  116. }
  117. if (!isset($fetcher)) {
  118. $fetcher = (function_exists('curl_version')) ? new CurlFetcher() : new FileGetContentsFetcher();
  119. }
  120. if ($seconds == 0) {
  121. $cache = false;
  122. }
  123. $this->cache = $cache;
  124. $this->seconds = $seconds;
  125. $this->fetcher = $fetcher;
  126. }
  127. /**
  128. * Sets the API Key.
  129. *
  130. * @param string $apiKey API key for the OpenWeatherMap account.
  131. *
  132. * @api
  133. */
  134. public function setApiKey($apiKey)
  135. {
  136. $this->apiKey = $apiKey;
  137. }
  138. /**
  139. * Returns the API Key.
  140. *
  141. * @return string
  142. *
  143. * @api
  144. */
  145. public function getApiKey()
  146. {
  147. return $this->apiKey;
  148. }
  149. /**
  150. * Returns the current weather at the place you specified.
  151. *
  152. * @param array|int|string $query The place to get weather information for. For possible values see below.
  153. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  154. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  155. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  156. *
  157. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  158. * @throws \InvalidArgumentException If an argument error occurs.
  159. *
  160. * @return CurrentWeather The weather object.
  161. *
  162. * There are four ways to specify the place to get weather information for:
  163. * - Use the city name: $query must be a string containing the city name.
  164. * - Use the city id: $query must be an integer containing the city id.
  165. * - Use the coordinates: $query must be an associative array containing the 'lat' and 'lon' values.
  166. * - Use the zip code: $query must be a string, prefixed with "zip:"
  167. *
  168. * Zip code may specify country. e.g., "zip:77070" (Houston, TX, US) or "zip:500001,IN" (Hyderabad, India)
  169. *
  170. * @api
  171. */
  172. public function getWeather($query, $units = 'imperial', $lang = 'en', $appid = '')
  173. {
  174. $answer = $this->getRawWeatherData($query, $units, $lang, $appid, 'xml');
  175. $xml = $this->parseXML($answer);
  176. return new CurrentWeather($xml, $units);
  177. }
  178. /**
  179. * Returns the current weather for a group of city ids.
  180. *
  181. * @param array $ids The city ids to get weather information for
  182. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  183. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  184. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  185. *
  186. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  187. * @throws \InvalidArgumentException If an argument error occurs.
  188. *
  189. * @return CurrentWeatherGroup
  190. *
  191. * @api
  192. */
  193. public function getWeatherGroup($ids, $units = 'imperial', $lang = 'en', $appid = '')
  194. {
  195. $answer = $this->getRawWeatherGroupData($ids, $units, $lang, $appid);
  196. $json = $this->parseJson($answer);
  197. return new CurrentWeatherGroup($json, $units);
  198. }
  199. /**
  200. * Returns the forecast for the place you specified. DANGER: Might return
  201. * fewer results than requested due to a bug in the OpenWeatherMap API!
  202. *
  203. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  204. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  205. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  206. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  207. * @param int $days For how much days you want to get a forecast. Default 1, maximum: 16.
  208. *
  209. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  210. * @throws \InvalidArgumentException If an argument error occurs.
  211. *
  212. * @return WeatherForecast
  213. *
  214. * @api
  215. */
  216. public function getWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
  217. {
  218. if ($days <= 5) {
  219. $answer = $this->getRawHourlyForecastData($query, $units, $lang, $appid, 'xml');
  220. } elseif ($days <= 16) {
  221. $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
  222. } else {
  223. throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
  224. }
  225. $xml = $this->parseXML($answer);
  226. return new WeatherForecast($xml, $units, $days);
  227. }
  228. /**
  229. * Returns the DAILY forecast for the place you specified. DANGER: Might return
  230. * fewer results than requested due to a bug in the OpenWeatherMap API!
  231. *
  232. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  233. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  234. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  235. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  236. * @param int $days For how much days you want to get a forecast. Default 1, maximum: 16.
  237. *
  238. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  239. * @throws \InvalidArgumentException If an argument error occurs.
  240. *
  241. * @return WeatherForecast
  242. *
  243. * @api
  244. */
  245. public function getDailyWeatherForecast($query, $units = 'imperial', $lang = 'en', $appid = '', $days = 1)
  246. {
  247. if ($days > 16) {
  248. throw new \InvalidArgumentException('Error: forecasts are only available for the next 16 days. $days must be 16 or lower.');
  249. }
  250. $answer = $this->getRawDailyForecastData($query, $units, $lang, $appid, 'xml', $days);
  251. $xml = $this->parseXML($answer);
  252. return new WeatherForecast($xml, $units, $days);
  253. }
  254. /**
  255. * Returns the weather history for the place you specified.
  256. *
  257. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  258. * @param \DateTime $start
  259. * @param int $endOrCount
  260. * @param string $type Can either be 'tick', 'hour' or 'day'.
  261. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  262. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  263. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  264. *
  265. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  266. * @throws \InvalidArgumentException If an argument error occurs.
  267. *
  268. * @return WeatherHistory
  269. *
  270. * @api
  271. */
  272. public function getWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
  273. {
  274. if (!in_array($type, array('tick', 'hour', 'day'))) {
  275. throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
  276. }
  277. $xml = json_decode($this->getRawWeatherHistory($query, $start, $endOrCount, $type, $units, $lang, $appid), true);
  278. if ($xml['cod'] != 200) {
  279. throw new OWMException($xml['message'], $xml['cod']);
  280. }
  281. return new WeatherHistory($xml, $query);
  282. }
  283. /**
  284. * Returns the current uv index at the location you specified.
  285. *
  286. * @param float $lat The location's latitude.
  287. * @param float $lon The location's longitude.
  288. *
  289. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  290. * @throws \InvalidArgumentException If an argument error occurs.
  291. *
  292. * @return UVIndex The uvi object.
  293. *
  294. * @api
  295. */
  296. public function getCurrentUVIndex($lat, $lon)
  297. {
  298. $answer = $this->getRawCurrentUVIndexData($lat, $lon);
  299. $json = $this->parseJson($answer);
  300. return new UVIndex($json);
  301. }
  302. /**
  303. * Returns the uv index at date, time and location you specified.
  304. *
  305. * @param float $lat The location's latitude.
  306. * @param float $lon The location's longitude.
  307. * @param \DateTimeInterface $dateTime The date and time to request data for.
  308. * @param string $timePrecision This decides about the timespan OWM will look for the uv index. The tighter
  309. * the timespan, the less likely it is to get a result. Can be 'year', 'month',
  310. * 'day', 'hour', 'minute' or 'second', defaults to 'day'.
  311. *
  312. * @throws OpenWeatherMap\Exception If OpenWeatherMap returns an error.
  313. * @throws \InvalidArgumentException If an argument error occurs.
  314. *
  315. * @return UVIndex The uvi object.
  316. *
  317. * @api
  318. */
  319. public function getUVIndex($lat, $lon, $dateTime, $timePrecision = 'day')
  320. {
  321. $answer = $this->getRawUVIndexData($lat, $lon, $dateTime, $timePrecision);
  322. $json = $this->parseJson($answer);
  323. return new UVIndex($json);
  324. }
  325. /**
  326. * Directly returns the xml/json/html string returned by OpenWeatherMap for the current weather.
  327. *
  328. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  329. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  330. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  331. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  332. * @param string $mode The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
  333. *
  334. * @return string Returns false on failure and the fetched data in the format you specified on success.
  335. *
  336. * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
  337. *
  338. * @api
  339. */
  340. public function getRawWeatherData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
  341. {
  342. $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherUrl);
  343. return $this->cacheOrFetchResult($url);
  344. }
  345. /**
  346. * Directly returns the JSON string returned by OpenWeatherMap for the group of current weather.
  347. * Only a JSON response format is supported for this webservice.
  348. *
  349. * @param array $ids The city ids to get weather information for
  350. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  351. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  352. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  353. *
  354. * @return string Returns false on failure and the fetched data in the format you specified on success.
  355. *
  356. * @api
  357. */
  358. public function getRawWeatherGroupData($ids, $units = 'imperial', $lang = 'en', $appid = '')
  359. {
  360. $url = $this->buildUrl($ids, $units, $lang, $appid, 'json', $this->weatherGroupUrl);
  361. return $this->cacheOrFetchResult($url);
  362. }
  363. /**
  364. * Directly returns the xml/json/html string returned by OpenWeatherMap for the hourly forecast.
  365. *
  366. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  367. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  368. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  369. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  370. * @param string $mode The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default).
  371. *
  372. * @return string Returns false on failure and the fetched data in the format you specified on success.
  373. *
  374. * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
  375. *
  376. * @api
  377. */
  378. public function getRawHourlyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
  379. {
  380. $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherHourlyForecastUrl);
  381. return $this->cacheOrFetchResult($url);
  382. }
  383. /**
  384. * Directly returns the xml/json/html string returned by OpenWeatherMap for the daily forecast.
  385. *
  386. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  387. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  388. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  389. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  390. * @param string $mode The format of the data fetched. Possible values are 'json', 'html' and 'xml' (default)
  391. * @param int $cnt How many days of forecast shall be returned? Maximum (and default): 16
  392. *
  393. * @throws \InvalidArgumentException If $cnt is higher than 16.
  394. *
  395. * @return string Returns false on failure and the fetched data in the format you specified on success.
  396. *
  397. * Warning: If an error occurs, OpenWeatherMap ALWAYS returns json data.
  398. *
  399. * @api
  400. */
  401. public function getRawDailyForecastData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml', $cnt = 16)
  402. {
  403. if ($cnt > 16) {
  404. throw new \InvalidArgumentException('$cnt must be 16 or lower!');
  405. }
  406. $url = $this->buildUrl($query, $units, $lang, $appid, $mode, $this->weatherDailyForecastUrl) . "&cnt=$cnt";
  407. return $this->cacheOrFetchResult($url);
  408. }
  409. /**
  410. * Directly returns the json string returned by OpenWeatherMap for the weather history.
  411. *
  412. * @param array|int|string $query The place to get weather information for. For possible values see ::getWeather.
  413. * @param \DateTime $start The \DateTime object of the date to get the first weather information from.
  414. * @param \DateTime|int $endOrCount Can be either a \DateTime object representing the end of the period to
  415. * receive weather history data for or an integer counting the number of
  416. * reports requested.
  417. * @param string $type The period of the weather history requested. Can be either be either "tick",
  418. * "hour" or "day".
  419. * @param string $units Can be either 'metric' or 'imperial' (default). This affects almost all units returned.
  420. * @param string $lang The language to use for descriptions, default is 'en'. For possible values see http://openweathermap.org/current#multi.
  421. * @param string $appid Your app id, default ''. See http://openweathermap.org/appid for more details.
  422. *
  423. * @throws \InvalidArgumentException
  424. *
  425. * @return string Returns false on failure and the fetched data in the format you specified on success.
  426. *
  427. * Warning If an error occurred, OpenWeatherMap ALWAYS returns data in json format.
  428. *
  429. * @api
  430. */
  431. public function getRawWeatherHistory($query, \DateTime $start, $endOrCount = 1, $type = 'hour', $units = 'imperial', $lang = 'en', $appid = '')
  432. {
  433. if (!in_array($type, array('tick', 'hour', 'day'))) {
  434. throw new \InvalidArgumentException('$type must be either "tick", "hour" or "day"');
  435. }
  436. $url = $this->buildUrl($query, $units, $lang, $appid, 'json', $this->weatherHistoryUrl);
  437. $url .= "&type=$type&start={$start->format('U')}";
  438. if ($endOrCount instanceof \DateTime) {
  439. $url .= "&end={$endOrCount->format('U')}";
  440. } elseif (is_numeric($endOrCount) && $endOrCount > 0) {
  441. $url .= "&cnt=$endOrCount";
  442. } else {
  443. throw new \InvalidArgumentException('$endOrCount must be either a \DateTime or a positive integer.');
  444. }
  445. return $this->cacheOrFetchResult($url);
  446. }
  447. /**
  448. * Directly returns the json string returned by OpenWeatherMap for the current UV index data.
  449. *
  450. * @param float $lat The location's latitude.
  451. * @param float $lon The location's longitude.
  452. *
  453. * @return bool|string Returns the fetched data.
  454. *
  455. * @api
  456. */
  457. public function getRawCurrentUVIndexData($lat, $lon)
  458. {
  459. if (!$this->apiKey) {
  460. throw new \RuntimeException('Before using this method, you must set the api key using ->setApiKey()');
  461. }
  462. if (!is_float($lat) || !is_float($lon)) {
  463. throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
  464. }
  465. $url = $this->buildUVIndexUrl($lat, $lon);
  466. return $this->cacheOrFetchResult($url);
  467. }
  468. /**
  469. * Directly returns the json string returned by OpenWeatherMap for the UV index data.
  470. *
  471. * @param float $lat The location's latitude.
  472. * @param float $lon The location's longitude.
  473. * @param \DateTimeInterface $dateTime The date and time to request data for.
  474. * @param string $timePrecision This decides about the timespan OWM will look for the uv index. The tighter
  475. * the timespan, the less likely it is to get a result. Can be 'year', 'month',
  476. * 'day', 'hour', 'minute' or 'second', defaults to 'day'.
  477. *
  478. * @return bool|string Returns the fetched data.
  479. *
  480. * @api
  481. */
  482. public function getRawUVIndexData($lat, $lon, $dateTime, $timePrecision = 'day')
  483. {
  484. if (!$this->apiKey) {
  485. throw new \RuntimeException('Before using this method, you must set the api key using ->setApiKey()');
  486. }
  487. if (!is_float($lat) || !is_float($lon)) {
  488. throw new \InvalidArgumentException('$lat and $lon must be floating point numbers');
  489. }
  490. if (interface_exists('DateTimeInterface') && !$dateTime instanceof \DateTimeInterface || !$dateTime instanceof \DateTime) {
  491. throw new \InvalidArgumentException('$dateTime must be an instance of \DateTime or \DateTimeInterface');
  492. }
  493. $url = $this->buildUVIndexUrl($lat, $lon, $dateTime, $timePrecision);
  494. return $this->cacheOrFetchResult($url);
  495. }
  496. /**
  497. * Returns whether or not the last result was fetched from the cache.
  498. *
  499. * @return bool true if last result was fetched from cache, false otherwise.
  500. */
  501. public function wasCached()
  502. {
  503. return $this->wasCached;
  504. }
  505. /**
  506. * @deprecated Use {@link self::getRawWeatherData()} instead.
  507. */
  508. public function getRawData($query, $units = 'imperial', $lang = 'en', $appid = '', $mode = 'xml')
  509. {
  510. return $this->getRawWeatherData($query, $units, $lang, $appid, $mode);
  511. }
  512. /**
  513. * Fetches the result or delivers a cached version of the result.
  514. *
  515. * @param string $url
  516. *
  517. * @return string
  518. */
  519. private function cacheOrFetchResult($url)
  520. {
  521. if ($this->cache !== false) {
  522. /** @var AbstractCache $cache */
  523. $cache = $this->cache;
  524. $cache->setSeconds($this->seconds);
  525. if ($cache->isCached($url)) {
  526. $this->wasCached = true;
  527. return $cache->getCached($url);
  528. }
  529. $result = $this->fetcher->fetch($url);
  530. $cache->setCached($url, $result);
  531. } else {
  532. $result = $this->fetcher->fetch($url);
  533. }
  534. $this->wasCached = false;
  535. return $result;
  536. }
  537. /**
  538. * Build the url to fetch weather data from.
  539. *
  540. * @param $query
  541. * @param $units
  542. * @param $lang
  543. * @param $appid
  544. * @param $mode
  545. * @param string $url The url to prepend.
  546. *
  547. * @return bool|string The fetched url, false on failure.
  548. */
  549. private function buildUrl($query, $units, $lang, $appid, $mode, $url)
  550. {
  551. $queryUrl = $this->buildQueryUrlParameter($query);
  552. $url = $url."$queryUrl&units=$units&lang=$lang&mode=$mode&APPID=";
  553. $url .= empty($appid) ? $this->apiKey : $appid;
  554. return $url;
  555. }
  556. /**
  557. * @param float $lat
  558. * @param float $lon
  559. * @param \DateTime|\DateTimeImmutable $dateTime
  560. * @param string $timePrecision
  561. *
  562. * @return string
  563. */
  564. private function buildUVIndexUrl($lat, $lon, $dateTime = null, $timePrecision = null)
  565. {
  566. if ($dateTime !== null) {
  567. $format = '\Z';
  568. switch ($timePrecision) {
  569. /** @noinspection PhpMissingBreakStatementInspection */
  570. case 'second':
  571. $format = ':s' . $format;
  572. /** @noinspection PhpMissingBreakStatementInspection */
  573. case 'minute':
  574. $format = ':i' . $format;
  575. /** @noinspection PhpMissingBreakStatementInspection */
  576. case 'hour':
  577. $format = '\TH' . $format;
  578. /** @noinspection PhpMissingBreakStatementInspection */
  579. case 'day':
  580. $format = '-d' . $format;
  581. /** @noinspection PhpMissingBreakStatementInspection */
  582. case 'month':
  583. $format = '-m' . $format;
  584. case 'year':
  585. $format = 'Y' . $format;
  586. break;
  587. default:
  588. throw new \InvalidArgumentException('$timePrecision is invalid.');
  589. }
  590. // OWM only accepts UTC timezones.
  591. $dateTime->setTimezone(new \DateTimeZone('UTC'));
  592. $dateTime = $dateTime->format($format);
  593. } else {
  594. $dateTime = 'current';
  595. }
  596. return sprintf($this->uvIndexUrl . '/%s,%s/%s.json?appid=%s', $lat, $lon, $dateTime, $this->apiKey);
  597. }
  598. /**
  599. * Builds the query string for the url.
  600. *
  601. * @param mixed $query
  602. *
  603. * @return string The built query string for the url.
  604. *
  605. * @throws \InvalidArgumentException If the query parameter is invalid.
  606. */
  607. private function buildQueryUrlParameter($query)
  608. {
  609. switch ($query) {
  610. case is_array($query) && isset($query['lat']) && isset($query['lon']) && is_numeric($query['lat']) && is_numeric($query['lon']):
  611. return "lat={$query['lat']}&lon={$query['lon']}";
  612. case is_array($query) && is_numeric($query[0]):
  613. return 'id='.implode(',', $query);
  614. case is_numeric($query):
  615. return "id=$query";
  616. case is_string($query) && strpos($query, 'zip:') === 0:
  617. $subQuery = str_replace('zip:', '', $query);
  618. return 'zip='.urlencode($subQuery);
  619. case is_string($query):
  620. return 'q='.urlencode($query);
  621. default:
  622. throw new \InvalidArgumentException('Error: $query has the wrong format. See the documentation of OpenWeatherMap::getWeather() to read about valid formats.');
  623. }
  624. }
  625. /**
  626. * @param string $answer The content returned by OpenWeatherMap.
  627. *
  628. * @return \SimpleXMLElement
  629. * @throws OWMException If the content isn't valid XML.
  630. */
  631. private function parseXML($answer)
  632. {
  633. // Disable default error handling of SimpleXML (Do not throw E_WARNINGs).
  634. libxml_use_internal_errors(true);
  635. libxml_clear_errors();
  636. try {
  637. return new \SimpleXMLElement($answer);
  638. } catch (\Exception $e) {
  639. // Invalid xml format. This happens in case OpenWeatherMap returns an error.
  640. // OpenWeatherMap always uses json for errors, even if one specifies xml as format.
  641. $error = json_decode($answer, true);
  642. if (isset($error['message'])) {
  643. throw new OWMException($error['message'], isset($error['cod']) ? $error['cod'] : 0);
  644. } else {
  645. throw new OWMException('Unknown fatal error: OpenWeatherMap returned the following json object: ' . $answer);
  646. }
  647. }
  648. }
  649. /**
  650. * @param string $answer The content returned by OpenWeatherMap.
  651. *
  652. * @return \stdClass
  653. * @throws OWMException If the content isn't valid JSON.
  654. */
  655. private function parseJson($answer)
  656. {
  657. $json = json_decode($answer);
  658. if (json_last_error() !== JSON_ERROR_NONE) {
  659. throw new OWMException('OpenWeatherMap returned an invalid json object. JSON error was: ' . $this->json_last_error_msg());
  660. }
  661. if (isset($json->message)) {
  662. throw new OWMException('An error occurred: '. $json->message);
  663. }
  664. return $json;
  665. }
  666. private function json_last_error_msg()
  667. {
  668. if (function_exists('json_last_error_msg')) {
  669. return json_last_error_msg();
  670. }
  671. static $ERRORS = array(
  672. JSON_ERROR_NONE => 'No error',
  673. JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
  674. JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
  675. JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
  676. JSON_ERROR_SYNTAX => 'Syntax error',
  677. JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
  678. );
  679. $error = json_last_error();
  680. return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error';
  681. }
  682. }