149 lines
4.2 KiB
PHP
149 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Ohrionmartin\Weather\Service;
|
|
|
|
use GuzzleHttp\ClientInterface;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
|
|
final class OpenWeatherClient
|
|
{
|
|
private ClientInterface $http;
|
|
private string $apiKey;
|
|
private string $baseUrl;
|
|
|
|
public function __construct(
|
|
ClientInterface $http,
|
|
string $apiKey,
|
|
string $baseUrl = 'https://api.openweathermap.org/data/3.0'
|
|
) {
|
|
$this->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<string,mixed>
|
|
* @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<string,mixed>
|
|
*/
|
|
public function oneCallByCity(string $city, array $options = []): array
|
|
{
|
|
$coords = $this->geocodeCity($city);
|
|
return $this->oneCall($coords['lat'], $coords['lon'], $options);
|
|
}
|
|
}
|