Get geographical weather data by city name or cordinates
Some checks failed
Tests / Tests PHP 7.4 (push) Has been cancelled
Tests / Tests PHP 8 (push) Has been cancelled
Tests / Tests PHP 8.1 (push) Has been cancelled

This commit is contained in:
Oh
2025-09-04 11:52:12 +02:00
parent c87bf18b4e
commit a0d4bf6f91
10 changed files with 1125 additions and 2 deletions

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\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);
}
}