Dokumentation durchblättern
Module entwickeln
Wetter & Zeitplanung
Wie Module eigene Wetter-Provider und zeitgesteuerte Aufgaben bereitstellen — austauschbare WeatherProvider, das WeatherData-Objekt und der ScheduledTask mit Cron-Ausdruck und Lauf-Protokoll.
Winterdienst hängt am Wetter, und vieles läuft zeitgesteuert im Hintergrund: Daten abrufen, alte Daten löschen, Berichte erzeugen. Dieser Abschnitt zeigt die zwei Erweiterungspunkte dafür — eigene Wetter-Provider und eigene geplante Aufgaben.
Wetter-Provider
Warum austauschbare Provider
Schneespur bezieht Wetterdaten nicht fest verdrahtet von einer einzigen Quelle, sondern über eine austauschbare Schnittstelle. Jeder Provider liefert dieselbe Datenstruktur, egal woher die Werte stammen. So lässt sich die Datenquelle wechseln, ohne den Rest der Anwendung anzufassen — und ein Modul kann eine zusätzliche Quelle nachrüsten, ohne Core-Dateien zu verändern.
Jeder Provider implementiert dasselbe Interface:
interface WeatherProviderInterface
{
public function fetchCurrent(float $lat, float $lon): ?WeatherData;
/** @return array{ok: bool, message: string, latency_ms: int} */
public function testConnection(float $lat, float $lon): array;
public function name(): string;
public function requiresApiKey(): bool;
}
Die vier Methoden teilen sich zwei Aufgaben: fetchCurrent() holt die eigentlichen
Wetterwerte für einen Standort, testConnection() prüft im Hintergrund, ob die Quelle
erreichbar ist und wie schnell sie antwortet. name() und requiresApiKey() liefern der
Oberfläche die Angaben, die sie für die Provider-Auswahl und die Schlüssel-Eingabe
braucht.
Mitgelieferte Provider
Im Kern sind mehrere Provider registriert. Welcher davon im Betrieb sinnvoll ist, hängt vom Anbieter, seinen Nutzungsbedingungen und Ihrem Standort ab — die Auswahl treffen Sie in der Oberfläche.
| Slug | Provider | API-Schlüssel nötig |
|---|---|---|
openmeteo_free | Open-Meteo (free tier) | Nein |
openmeteo_api | Open-Meteo (API tier) | Ja |
brightsky | BrightSky (DWD-Daten) | Nein |
met_norway | Met Norway (MET API) | Nein |
Prüfen Sie vor dem produktiven Einsatz die Nutzungsbedingungen des jeweiligen Anbieters — manche Tarife sind ausdrücklich auf nicht-kommerzielle Nutzung beschränkt. Genau für solche Fälle ist das Provider-System austauschbar: Reicht eine vorhandene Quelle nicht, binden Sie über ein Modul eine eigene ein.
Einen Wetter-Provider als Modul bauen
Ein eigener Provider ist eine Klasse, die WeatherProviderInterface implementiert. Das
folgende Beispiel ruft eine externe API ab und übersetzt deren Antwort in ein
WeatherData-Objekt:
namespace Schneespur\Module\CustomWeather\Weather;
use App\Models\Setting;
use App\Services\Weather\WeatherData;
use App\Services\Weather\WeatherProviderInterface;
use Illuminate\Support\Facades\Http;
class CustomWeatherProvider implements WeatherProviderInterface
{
public function fetchCurrent(float $lat, float $lon): ?WeatherData
{
$response = Http::get('https://api.custom-weather.com/current', [
'lat' => $lat,
'lon' => $lon,
'key' => Setting::get('custom-weather.api_key'),
]);
if (!$response->successful()) return null;
$data = $response->json();
return new WeatherData(
temperature: $data['temp'],
precipitation: $data['precip'],
snowDepth: $data['snow_depth'] ?? null,
windSpeed: $data['wind_speed'],
humidity: $data['humidity'],
weatherCode: $data['wmo_code'],
);
}
public function testConnection(float $lat, float $lon): array
{
$start = microtime(true);
try {
$result = $this->fetchCurrent($lat, $lon);
$ms = (int) ((microtime(true) - $start) * 1000);
return [
'ok' => $result !== null,
'message' => $result ? 'Connection successful' : 'No data returned',
'latency_ms' => $ms,
];
} catch (\Throwable $e) {
return [
'ok' => false,
'message' => $e->getMessage(),
'latency_ms' => (int) ((microtime(true) - $start) * 1000),
];
}
}
public function name(): string { return 'Custom Weather API'; }
public function requiresApiKey(): bool { return true; }
}
Zwei Punkte sind hier wichtig. Erstens gibt fetchCurrent() bei einem Fehlschlag null
zurück statt eine Ausnahme nach oben durchzureichen — die Anwendung kann dann auf eine
andere Quelle ausweichen, statt abzubrechen. Zweitens misst testConnection() die
Antwortzeit in Millisekunden und meldet jeden Fehler als lesbare Nachricht zurück; genau
diese Angaben zeigt die Oberfläche beim Verbindungstest an.
Registriert wird der Provider in der boot()-Phase Ihres
ServiceProviders über die zuständige Registry:
app(WeatherProviderRegistry::class)->register('custom_weather', CustomWeatherProvider::class);
Das WeatherData-Objekt
WeatherData ist das gemeinsame Wertobjekt, in das jeder Provider seine Antwort
übersetzt. Weil alle Provider dieselbe Struktur zurückgeben, muss der Rest der Anwendung
nie wissen, woher die Daten stammen.
| Eigenschaft | Typ | Bedeutung |
|---|---|---|
temperature | float | Temperatur in °C |
precipitation | float | Niederschlag in mm |
snowDepth | ?float | Schneehöhe in cm |
windSpeed | float | Windgeschwindigkeit in km/h |
humidity | int | relative Luftfeuchte in % |
weatherCode | int | WMO-Wettercode |
Ihr Provider ist dafür zuständig, die Felder der externen API auf genau diese Eigenschaften und Einheiten abzubilden. Liefert eine Quelle etwa Temperaturen in Fahrenheit, rechnen Sie im Provider auf °C um.
Konvention: Einheiten der TTL
Die Wetter-Einstellungen verwenden in der Oberfläche Minuten, in der Datenbank dagegen Sekunden (TTL = Gültigkeitsdauer zwischengespeicherter Wetterdaten). Die Umrechnung in beide Richtungen übernimmt der Controller. Wenn Ihr Modul mit den Wetter-TTL-Einstellungen arbeitet, halten Sie sich an dieselbe Konvention, damit die Werte konsistent bleiben.
Geplante Aufgaben
Warum geplante Aufgaben
Manche Arbeit soll nicht auf Knopfdruck laufen, sondern regelmäßig im Hintergrund: täglich abgelaufene Daten löschen, stündlich nach Updates sehen, jede Minute die Warteschlange abarbeiten. Dafür gibt es geplante Aufgaben. Ein Modul kann eigene Aufgaben beisteuern, ohne den Cron-Aufbau der Anwendung zu kennen — es beschreibt nur, was wann laufen soll.
Jede Aufgabe implementiert dieses Interface:
interface ScheduledTaskInterface
{
public function slug(): string; // unique identifier
public function label(): string; // human-readable name
public function schedule(): string; // cron expression
public function handle(): void; // task execution
public function isEnabled(): bool; // whether task should run
public function source(): string; // 'core' or module slug
}
slug() ist die eindeutige Kennung, label() der lesbare Name für die Oberfläche.
schedule() legt über einen Cron-Ausdruck fest, wann die Aufgabe läuft, handle()
enthält die eigentliche Arbeit. Über isEnabled() kann sich eine Aufgabe abhängig von
einer Einstellung selbst stilllegen, und source() weist sie dem Kern ('core') oder
einem Modul-Slug zu — so bleibt nachvollziehbar, woher eine Aufgabe stammt.
Mitgelieferte Aufgaben
Im Kern laufen unter anderem diese Aufgaben:
| Slug | Zeitplan | Beschreibung |
|---|---|---|
retention-delete | täglich | löscht abgelaufene Daten gemäß Aufbewahrungsregel |
update-check | täglich | sucht nach Anwendungs-Updates |
queue-work | * * * * * | arbeitet Jobs aus der Warteschlange ab |
cron-heartbeat | * * * * * | hält Cron-Aktivität für die Funktionsprüfung fest |
Eine geplante Aufgabe bauen
Das folgende Beispiel erzeugt jeden Morgen um 06:00 Uhr einen Bericht und lässt sich über eine Einstellung an- und abschalten:
namespace Schneespur\Module\Reports\Scheduler;
use App\Models\Setting;
use App\Services\Scheduler\ScheduledTaskInterface;
class DailyReportTask implements ScheduledTaskInterface
{
public function slug(): string { return 'daily-report'; }
public function label(): string { return 'Daily Report Generation'; }
public function schedule(): string { return '0 6 * * *'; } // 06:00 daily
public function handle(): void
{
// Generate and send daily report
$this->generateReport();
}
public function isEnabled(): bool
{
return (bool) Setting::get('reports.daily_enabled', false);
}
public function source(): string { return 'reports'; }
}
isEnabled() liest hier den Wert reports.daily_enabled aus den Einstellungen. Steht er
auf false, überspringt die Anwendung die Aufgabe, ohne dass Sie den Code ändern müssen
— Aktivieren und Deaktivieren läuft über die Oberfläche.
Registriert wird die Aufgabe wie der Wetter-Provider in der boot()-Phase über die
zuständige Registry:
app(ScheduledTaskRegistry::class)->register('daily-report', DailyReportTask::class);
Läufe nachvollziehen
Die Registry hält fest, wann eine Aufgabe zuletzt lief, ob sie erfolgreich war und wie lange sie brauchte. Das macht Hintergrundarbeit überprüfbar: Eine Aufgabe, die seit Tagen nicht mehr erfolgreich gelaufen ist, fällt so auf.
$registry = app(ScheduledTaskRegistry::class);
// After running a task
$registry->recordRun('daily-report', 'success', null, 1234); // 1234ms
// On failure
$registry->recordRun('daily-report', 'failed', 'Connection timeout', 5000);
// Get last run info
$lastRun = $registry->lastRun('daily-report');
// → object{slug, status, error_message, duration_ms, ran_at}
// Get all tasks with status
$all = $registry->allWithStatus();
// → ['daily-report' => ['task' => ..., 'last_run' => ...]]
recordRun() protokolliert einen einzelnen Lauf samt Status, Fehlermeldung und Dauer.
lastRun() liefert den letzten Lauf einer Aufgabe, allWithStatus() alle Aufgaben mit
ihrem jeweils letzten Status — genau die Übersicht, die die Oberfläche anzeigt.
Cron-Ausdrücke
Der Zeitplan einer Aufgabe ist ein gewöhnlicher Cron-Ausdruck. Die häufigsten Muster:
| Ausdruck | Bedeutung |
|---|---|
* * * * * | jede Minute |
*/5 * * * * | alle 5 Minuten |
0 * * * * | jede volle Stunde |
0 6 * * * | täglich um 06:00 |
0 0 * * 1 | jeden Montag um Mitternacht |
0 0 1 * * | am ersten Tag jedes Monats |
Die genaue API der hier genutzten Registries — WeatherProviderRegistry und
ScheduledTaskRegistry — steht unter Registries.