http = $http; $this->apiKey = $apiKey; $this->baseUrl = rtrim($baseUrl, '/'); } /** * Fetch One Call (3.0) current/forecast data * * @param float $lat * @param float $lon * @param array{ * exclude?: string, * units?: 'standard'|'metric'|'imperial', * lang?: string * } $options * @return array * @throws \RuntimeException on HTTP or API error */ public function oneCall(float $lat, float $lon, array $options = []): array { $query = [ 'lat' => $lat, 'lon' => $lon, 'appid' => $this->apiKey, ]; if (!empty($options['exclude'])) { $query['exclude'] = $options['exclude']; // comma-separated string } if (!empty($options['units'])) { $query['units'] = $options['units']; } if (!empty($options['lang'])) { $query['lang'] = $options['lang']; } try { $res = $this->http->request('GET', "{$this->baseUrl}/onecall", [ 'query' => $query, 'http_errors' => false, 'timeout' => 8.0, 'connect_timeout' => 5.0, ]); } catch (GuzzleException $e) { throw new \RuntimeException('Weather service is unavailable', 0, $e); } $status = $res->getStatusCode(); $body = (string) $res->getBody(); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException('Invalid response from weather service'); } if ($status >= 400) { $message = $data['message'] ?? 'OpenWeather API error'; throw new \RuntimeException($message); } return $data; } /** * Geocode a city name to coordinates using OpenWeather Geocoding API. * * @param string $city * @return array{lat: float, lon: float} */ public function geocodeCity(string $city): array { $query = [ 'q' => $city, 'limit' => 1, 'appid' => $this->apiKey, ]; try { $res = $this->http->request('GET', 'https://api.openweathermap.org/geo/1.0/direct', [ 'query' => $query, 'http_errors' => false, 'timeout' => 8.0, 'connect_timeout' => 5.0, ]); } catch (GuzzleException $e) { throw new \RuntimeException('Geocoding service is unavailable', 0, $e); } $status = $res->getStatusCode(); $body = (string) $res->getBody(); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException('Invalid response from geocoding service'); } if ($status >= 400) { $message = is_array($data) && isset($data['message']) ? $data['message'] : 'Geocoding API error'; throw new \RuntimeException($message); } if (!is_array($data) || count($data) === 0 || !isset($data[0]['lat'], $data[0]['lon'])) { throw new \RuntimeException('City not found'); } return [ 'lat' => (float) $data[0]['lat'], 'lon' => (float) $data[0]['lon'], ]; } /** * Convenience method: One Call by city name. * * @param string $city * @param array{ * exclude?: string, * units?: 'standard'|'metric'|'imperial', * lang?: string * } $options * @return array */ public function oneCallByCity(string $city, array $options = []): array { $coords = $this->geocodeCity($city); return $this->oneCall($coords['lat'], $coords['lon'], $options); } }