feat: ForecastMapper and file Cache

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:38:30 +02:00
co-authored by Claude Fable 5.1
parent d657fd069f
commit 89e1e544c5
6 changed files with 253 additions and 1 deletions
+53
View File
@@ -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';
}
}
+45
View File
@@ -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;
}
}