feat: ForecastMapper and file Cache
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm;
|
||||
|
||||
/** Eenvoudige bestandscache per sleutel: één JSON-bestand met stored_at en data. Schrijft atomisch. */
|
||||
final class Cache
|
||||
{
|
||||
public function __construct(private readonly string $dir)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return array{data: array, stored_at: int}|null */
|
||||
public function read(string $key): ?array
|
||||
{
|
||||
$file = $this->file($key);
|
||||
if (!is_file($file)) {
|
||||
return null;
|
||||
}
|
||||
$raw = json_decode((string) file_get_contents($file), true);
|
||||
if (!is_array($raw) || !is_array($raw['data'] ?? null) || !is_int($raw['stored_at'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
return ['data' => $raw['data'], 'stored_at' => $raw['stored_at']];
|
||||
}
|
||||
|
||||
public function isFresh(string $key, int $ttl, int $now): bool
|
||||
{
|
||||
$entry = $this->read($key);
|
||||
return $entry !== null && ($now - $entry['stored_at']) < $ttl;
|
||||
}
|
||||
|
||||
public function write(string $key, array $data, int $now): void
|
||||
{
|
||||
if (!is_dir($this->dir) && !mkdir($this->dir, 0750, true) && !is_dir($this->dir)) {
|
||||
throw new \RuntimeException("Cachemap kan niet worden aangemaakt: {$this->dir}");
|
||||
}
|
||||
$file = $this->file($key);
|
||||
$tmp = $file . '.tmp.' . uniqid('', true);
|
||||
$json = json_encode(['stored_at' => $now, 'data' => $data], JSON_THROW_ON_ERROR);
|
||||
if (file_put_contents($tmp, $json, LOCK_EX) === false || !rename($tmp, $file)) {
|
||||
@unlink($tmp);
|
||||
throw new \RuntimeException("Cachebestand kan niet worden geschreven: $file");
|
||||
}
|
||||
}
|
||||
|
||||
private function file(string $key): string
|
||||
{
|
||||
$safe = strtolower((string) preg_replace('/[^a-z0-9_-]+/i', '_', $key));
|
||||
return $this->dir . '/' . trim($safe, '_') . '.json';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm;
|
||||
|
||||
/** Zet het "daily"-blok van Open-Meteo om naar 5 dagen vanaf morgen (index 0 is vandaag). */
|
||||
final class ForecastMapper
|
||||
{
|
||||
private const DAYS = 5;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $daily
|
||||
* @return list<array{date: string, weather_code: ?int, temp_max: ?float, temp_min: ?float, precip_mm: ?float, precip_prob: ?int}>
|
||||
*/
|
||||
public static function map(array $daily): array
|
||||
{
|
||||
$dates = is_array($daily['time'] ?? null) ? $daily['time'] : [];
|
||||
$out = [];
|
||||
for ($i = 1; $i <= self::DAYS && isset($dates[$i]); $i++) {
|
||||
if (!is_string($dates[$i])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'date' => $dates[$i],
|
||||
'weather_code' => self::intOrNull($daily['weather_code'][$i] ?? null),
|
||||
'temp_max' => self::floatOrNull($daily['temperature_2m_max'][$i] ?? null),
|
||||
'temp_min' => self::floatOrNull($daily['temperature_2m_min'][$i] ?? null),
|
||||
'precip_mm' => self::floatOrNull($daily['precipitation_sum'][$i] ?? null),
|
||||
'precip_prob' => self::intOrNull($daily['precipitation_probability_max'][$i] ?? null),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function floatOrNull(mixed $v): ?float
|
||||
{
|
||||
return is_int($v) || is_float($v) ? (float) $v : null;
|
||||
}
|
||||
|
||||
private static function intOrNull(mixed $v): ?int
|
||||
{
|
||||
return is_int($v) || is_float($v) ? (int) round($v) : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user