Dokumentation durchblättern
Module entwickeln
Speicher & Backup
Wie Module über StorageBackendRegistry und BackupTargetRegistry zusätzliche Speicher- und Backup-Ziele anmelden — mit Fallback-Verhalten, Beispielen und Regeln für sicheren Dateizugriff.
Schneespur speichert Dateien — PDF-Einsatznachweise, Fotos, Dokumente — über austauschbare Speicher-Backends. Der Kern bringt ein lokales Dateisystem-Backend mit; Module ergänzen weitere Ziele wie S3 oder SFTP, ohne dass aufrufender Code sich ändern muss. Genau dasselbe Muster gilt für Backup-Ziele.
Speicher-Backends
Die StorageBackendRegistry verwaltet die Dateiablage. Ein Modul registriert dort ein
zusätzliches Backend, und der restliche Code spricht weiterhin nur die Registry an — nicht
das konkrete Backend. Dadurch lässt sich der Speicherort wechseln, ohne jeden Aufrufer
anzufassen.
Jedes Backend erfüllt dasselbe Interface:
interface StorageBackendInterface
{
public function slug(): string;
public function label(): string;
public function store(string $relativePath, string $contents): void;
public function retrieve(string $relativePath): ?string;
public function delete(string $relativePath): bool;
public function exists(string $relativePath): bool;
public function url(string $relativePath): string;
public function isConfigured(): bool;
}
isConfigured() ist bewusst Teil des Vertrags: Ein S3-Backend ohne Zugangsdaten ist nicht
einsatzbereit, und die Registry kann das prüfen, bevor sie es als aktives Backend verwendet.
Speicher im eigenen Modul nutzen
Sie lösen das aktive Backend über die Registry auf und schreiben oder lesen relative Pfade:
$storage = app(StorageBackendRegistry::class);
// Write a file
$backend = $storage->resolve(); // active backend
$backend->store('documents/contract-123.pdf', $pdfContent);
// Read with automatic fallback to local
$content = $storage->retrieveWithFallback('documents/contract-123.pdf');
// Get URL with fallback
$url = $storage->urlWithFallback('photos/job-42.jpg');
Fallback-Verhalten
Die StorageBackendRegistry bringt eine eingebaute Rückfall-Logik mit:
retrieveWithFallback()— versucht zuerst das aktive Backend und greift auf das lokale zurück, wenn die Datei dort nicht liegt.urlWithFallback()— dasselbe für die URL-Erzeugung.
Das ist vor allem während einer Migration nützlich: Wer von lokalem Speicher auf Cloud-Speicher umstellt, hat eine Übergangszeit, in der ältere Dateien noch lokal liegen und neue bereits in der Cloud. Der Fallback überbrückt diese Phase, ohne dass Sie alle Bestände vorab umkopieren müssen.
Ein Speicher-Backend-Modul bauen
Ein eigenes Backend implementiert das Interface und liest seine Konfiguration aus den Modul-Einstellungen. Das folgende Beispiel zeigt die tragenden Methoden für ein S3-Backend; die übrigen folgen demselben Muster:
namespace Schneespur\Module\S3Storage\Storage;
use App\Models\Setting;
use App\Services\Storage\StorageBackendInterface;
use Aws\S3\S3Client;
class S3StorageBackend implements StorageBackendInterface
{
public function slug(): string { return 's3'; }
public function label(): string { return 'Amazon S3'; }
public function store(string $relativePath, string $contents): void
{
$this->client()->putObject([
'Bucket' => Setting::get('s3.bucket'),
'Key' => $relativePath,
'Body' => $contents,
]);
}
public function retrieve(string $relativePath): ?string
{
try {
$result = $this->client()->getObject([
'Bucket' => Setting::get('s3.bucket'),
'Key' => $relativePath,
]);
return (string) $result['Body'];
} catch (\Throwable) {
return null;
}
}
public function delete(string $relativePath): bool { /* ... */ }
public function exists(string $relativePath): bool { /* ... */ }
public function url(string $relativePath): string { /* ... */ }
public function isConfigured(): bool
{
return !empty(Setting::get('s3.bucket'))
&& !empty(Setting::get('s3.key'))
&& !empty(Setting::get('s3.secret'));
}
private function client(): S3Client { /* ... */ }
}
Beachten Sie, dass retrieve() bei einem Fehler null zurückgibt statt eine Ausnahme
durchzureichen. Das ist die Voraussetzung dafür, dass retrieveWithFallback() sauber auf das
lokale Backend ausweichen kann, statt am Cloud-Fehler abzubrechen.
Die Konfiguration liest das Backend über Setting::get() mit dem Modul-Slug als Präfix
(s3.bucket, s3.key, s3.secret). Wie Module Einstellungen registrieren, steht unter
ServiceProvider.
Registriert wird das Backend im boot() des ServiceProviders:
app(StorageBackendRegistry::class)->register('s3', S3StorageBackend::class);
Backup-Ziele
Die BackupTargetRegistry bestimmt, wohin Backups geschrieben werden. Der Kern bietet ein
lokales Backup-Ziel; Module ergänzen Cloud-Ziele. Das Prinzip ist dasselbe wie bei den
Speicher-Backends, der Vertrag ist aber schlanker — ein Backup-Ziel muss nur ablegen und
zurückspielen können:
interface BackupTargetInterface
{
public function slug(): string;
public function label(): string;
public function store(string $sourcePath): bool; // store a backup file
public function restore(string $identifier, string $destinationPath): bool; // restore from backup
public function isConfigured(): bool;
}
Ein Backup-Ziel-Modul bauen
namespace Schneespur\Module\CloudBackup\Backup;
use App\Models\Setting;
use App\Services\Backup\BackupTargetInterface;
class S3BackupTarget implements BackupTargetInterface
{
public function slug(): string { return 's3-backup'; }
public function label(): string { return 'Amazon S3 Backup'; }
public function store(string $sourcePath): bool
{
// Upload the backup file to S3
$key = 'backups/' . basename($sourcePath);
// ... S3 upload logic
return true;
}
public function restore(string $identifier, string $destinationPath): bool
{
// Download backup from S3 and write to $destinationPath
return true;
}
public function isConfigured(): bool
{
return !empty(Setting::get('s3-backup.bucket'));
}
}
Registriert wird das Ziel wie gewohnt im boot():
app(BackupTargetRegistry::class)->register('s3-backup', S3BackupTarget::class);
Aktives Ziel
Welches Backup-Ziel aktiv ist, steht in der Einstellung backup_target. Die Administration
wählt es auf der Backup-Einstellungsseite aus; die verfügbaren Ziele liefert
availableTargets(). So entscheidet der Betrieb über die Oberfläche, wohin Backups gehen —
das Modul stellt die Möglichkeit bereit, erzwingt sie aber nicht.
Empfehlungen für den Umgang mit Dateien
Diese Regeln halten Datei-Handling sicher und gegenüber einem Backend-Wechsel robust:
- Dateien nie in
public/ablegen — immer über das Speicher-Backend. Was inpublic/liegt, ist ohne Zugriffsprüfung im Web erreichbar. - Relative Pfade verwenden — das Backend kümmert sich um absolute Pfade. So bleibt der Code unabhängig davon, ob lokal oder in der Cloud gespeichert wird.
- Pfade mit dem Modul-Slug präfixen — z. B.
documents/,invoices/. Das hält die Ablage übersichtlich und vermeidet Kollisionen zwischen Modulen. - Dateien über authentifizierte Routen ausliefern — vor dem Download die Berechtigung prüfen.
- Metadaten in der Datenbank halten —
file_path,mime_type,file_size. Der Speicher hält die Bytes, die Datenbank das Wissen darüber.
Authentifizierte Download-Route
Eine Datei sollte nie direkt aus dem Speicher ins Web durchgereicht werden, ohne dass
geprüft wird, wer sie abrufen darf. Die Route prüft erst die Berechtigung über Gate und
liefert den Inhalt dann mit passenden Headern aus:
Route::get('documents/{document}/download', function (Document $document) {
Gate::authorize('documents.view');
$storage = app(StorageBackendRegistry::class);
$content = $storage->retrieveWithFallback($document->file_path);
if ($content === null) {
abort(404);
}
return response($content)
->header('Content-Type', $document->mime_type)
->header('Content-Disposition', 'attachment; filename="' . $document->title . '"');
})->name('admin.documents.download');
Wie Module Routen und Berechtigungen anmelden, steht unter Routen & APIs und Berechtigungen & Rollen.