webalternatif/flysystem-failover-bundle
Symfony bundle that adds a failover adapter to Flysystem, automatically switching to a backup storage backend when the primary one fails. Configure multiple adapters with priorities so reads and writes keep working during outages or misconfigurations.
This package is a Symfony bundle (Webalternatif FlysystemFailoverBundle) that wires Flysystem's failover storage into a Symfony service container. Its job is narrow: given an ordered list of child filesystems, route writes to the first healthy one and fall back to the next when the active one throws.
For a Laravel developer the practical order of operations is:
CHANGELOG.md first — this release (v0.8.0) only ships a changelog link, and the project is still on a 0.x line, so treat every minor as potentially breaking and pin an exact version ("webalternatif/flysystem-failover-bundle": "0.8.0").DependencyInjection/Configuration.php and the service definitions), not the bundle class. That is where the adapter list shape, aliases, and option names live — and it is the part you will re-implement by hand in Laravel.vendor/league/flysystem*. That is where the real semantics live (operation routing, exception handling, metadata behaviour). The bundle is mostly glue.s3, secondary local (or a second bucket/region), so an ingest job keeps succeeding and you get a reconciliation backlog instead of a 500.Minimal Laravel wiring (no bundle, just the concept):
// config/filesystems.php
'disks' => [
'uploads' => [
'driver' => 'failover',
'primary' => 's3',
'fallback' => 'local',
],
],
// AppServiceProvider::boot()
Storage::extend('failover', function ($app, $config) {
return new Filesystem(new FailoverAdapter([
Storage::disk($config['primary'])->getAdapter(),
Storage::disk($config['fallback'])->getAdapter(),
]));
});
Compose, don't replace. Keep the child disks as normal Laravel disks (s3, local, s3-eu) with their own credentials and env vars in config/filesystems.php. The failover disk should only reference them by name. This keeps Storage::disk('s3') usable for health checks, backfills, and direct downloads.
Ordering is the whole policy. The child list is priority-ordered and the first success wins. Put the fast/authoritative store first; put the durable-but-slow store (local disk, secondary region) last. Document the order in a comment next to the config, because it is the only thing that determines behaviour.
Write-through with read fallback, not replication. Expect writes to land on exactly one child. Plan a reconciliation job: a scheduled command that lists the primary, diffs against the fallback, and copies stragglers up. This is the piece the package does not give you and every production user ends up writing.
Health canary. A small scheduled command that writes and deletes a _health/{date}.txt on each child, logging latency and exceptions per child to a dedicated log channel. Failover decisions made from inside an exception handler are invisible otherwise.
Queue-friendly ingest. Wrap uploads in a job with $tries/backoff and attach the resolved disk name to the job payload. When a failover happens mid-run, the job log tells you which child took the file, which is what you need for the reconciliation diff.
Testing. Use two InMemoryFilesystemAdapter instances and assert the file exists on both/one as intended; simulate an outage with a tiny throwing adapter implementing the same interface. Assert on Storage::disk('uploads')->exists(...) and on the exception being swallowed rather than propagated. Add a test that a read falls through when the first child returns UnableToReadFile.
Integration tips. Storage::disk('uploads')->temporaryUrl() and other Laravel-level conveniences may not survive composition — check per method. Attach Flysystem plugins to the composite, not a child, unless you deliberately want per-child behaviour. For anything metadata-heavy, call through to a specific child disk rather than the failover disk.
Storage::extend, as above. Do not add the bundle to composer.json expecting Laravel to boot it.listContents may or may not be merged/deduplicated depending on version.public on local and public on S3 are different ACLs. Pass visibility explicitly on every write rather than relying on per-child defaults.League\Flysystem\Config / a PSR-3 logger) at the adapter boundary, and inspect the child adapter exception, not the wrapper's. php artisan storage:link is irrelevant here — a missing symlink will never be the cause.FilesystemOperator for metrics/alerting, a custom adapter implementing the same interface for canary and diff commands, and Storage::extend for driver registration. Those three cover nearly every adaptation you will need.How can I help you explore Laravel packages today?