Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Flysystem Laravel Package

league/flysystem

Flysystem is a PHP filesystem abstraction library offering one API for local and remote storage. Adapters let you read, write, and manage files on S3, FTP, SFTP, Dropbox, and more, so you can swap backends without rewriting application code.

View on GitHub
Deep Wiki
Context7

Getting Started

Flysystem is a filesystem abstraction: one Filesystem API, swappable adapters. The single biggest onboarding step is internalizing that core is just the API — every backend is a separate package.

composer require league/flysystem
composer require league/flysystem-local            # local adapter (extracted in 3.15)
composer require league/flysystem-aws-s3-v3        # or -ftp, -sftp, -webdav, -google-cloud-storage, -azure-blob-storage, -zip

Minimal usage — this is the whole day-to-day surface:

use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;

$fs = new Filesystem(new LocalFilesystemAdapter(__DIR__.'/storage'));

$fs->write('reports/2024-q1.csv', $csv);
$fs->fileExists('reports/2024-q1.csv');   // bool
$fs->mimeType('reports/2024-q1.csv');     // string
$fs->fileSize('reports/2024-q1.csv');     // int
$fs->read('reports/2024-q1.csv');         // string
$fs->delete('reports/2024-q1.csv');

Where to look first: the League\Flysystem\Filesystem class itself (it is the entire contract — ~20 methods), then src/Adapter naming in the README to pick your backend, then League\Flysystem\FilesystemException and its subclasses. Config is passed as an array via the adapter constructor or per-call Config/array.

First use case to convert: any place you have file_put_contents / file_get_contents / mkdir / scandir / unlink scattered across your domain code. Migrate one service (e.g. invoice PDF storage) behind Filesystem, then swap the adapter to S3 in staging without touching the caller. If you are on Laravel 9+, you already have Flysystem 3 underneath the Storage facade — do not build a parallel new Filesystem(...) stack; use Storage::disk() unless you need an adapter Laravel doesn't ship.

Implementation Patterns

Adapter-per-environment, resolved from config. Build the Filesystem once in a container binding/factory, keyed by disk name. Never new LocalFilesystemAdapter(...) at a call site — that is the exact coupling Flysystem exists to remove.

// config/filesystems.php equivalent
'uploads' => match (env('FILESYSTEM_DRIVER')) {
    's3'    => new Filesystem(new AwsS3V3Adapter($s3Client, 'my-bucket', 'uploads/')),
    default => new Filesystem(new LocalFilesystemAdapter(storage_path('app/uploads'))),
},

Streams for anything that could be large. read()/write() buffer the whole payload in memory. readStream/writeStream do not.

$stream = $fs->readStream('exports/huge.csv');
fpassthru($stream);
fclose($stream);

$fs->writeStream('exports/huge.csv', fopen($localTemp, 'r'));

Listing is a lazy, non-rewindable generator. Use shallow by default, deep only when you mean it; wrap in toArray() if you need to iterate twice.

use League\Flysystem\Filesystem;

foreach ($fs->listContents('uploads', Filesystem::LIST_DEEP) as $item) {
    $item->isFile();     // bool
    $item->path();       // string, always relative to the root
    $item->lastModified();
}

DirectoryListing supports filter()/map()/sortByPath() (typed for PHPStan since 3.13), which is where you should put "only images" logic rather than inside the loop.

MountManager when you have more than one backend. Paths become disk://path, and copy/move across filesystems works in one call — this is the cleanest way to do "upload from local temp to S3".

$mounts = new MountManager([
    'local' => new Filesystem(new LocalFilesystemAdapter('/tmp')),
    's3'    => new Filesystem(new AwsS3V3Adapter($client, 'bucket')),
]);

$mounts->copy('local://staging/report.pdf', 's3://reports/report.pdf');
$mounts->write('s3://reports/readme.txt', 'hi');

Path prefixing for multi-tenancy. Rather than baking tenants/{id}/ into every call:

use League\Flysystem\PathPrefixing\PathPrefixedAdapter;

$tenantFs = new Filesystem(new PathPrefixedAdapter($s3Adapter, 'tenant-123'));

checksum, publicUrl, temporaryUrl are prefix-aware since 3.10.x — a strong argument for using the decorator over manual path juggling.

Read-only exposure. ReadonlyFilesystemAdapter for anything user-facing (downloads, public previews) so a bug can't delete the bucket.

Serving files. publicUrl() and temporaryUrl() mean you don't proxy bytes through PHP:

$url = $fs->temporaryUrl('reports/secret.pdf', new DateTimeImmutable('+15 minutes'));

Inject a custom PublicUrlGenerator (CDN domain) rather than string-replacing hosts downstream; ShardedPrefixPublicUrlGenerator and chained strategies exist for CDN sharding.

Integrity checks. checksum($path, Algo::MD5) returns provider-computed checksums where available (S3, GCS, Azure, AsyncAws) and falls back to ad-hoc hashing otherwise — good for verifying uploads and for ETag comparisons.

Testing. Never hit S3 in unit tests:

$fs = new Filesystem(new InMemoryFilesystemAdapter());

StaticInMemoryAdapterRegistry (3.3) is useful for functional tests where a container resolves filesystems. Pair with ReadonlyFilesystemAdapter to prove your code handles permission rejections.

Testing/production parity gotcha: run S3 tests against MinIO with use_path_style_endpoint => true instead of against the real bucket.

Adapter/decorator extension. Custom backends implement FilesystemAdapter; to wrap an existing adapter (logging, caching, metrics, retry), extend DecoratedAdapter (3.22) instead of reimplementing the 12-method interface.

Gotchas and Tips

  • League\Flysystem\Local\LocalFilesystemAdapter no longer ships with core (extracted in 3.15). Upgrading an old lockfile without adding league/flysystem-local yields confusing class-not-found errors. Old tutorials say League\Flysystem\Adapter\Local — that's v1.
  • Laravel version coupling is the top real-world trap. Laravel 9+ requires Flysystem 3; a package pinning ^1.0|^2.0 will block the upgrade. Also, Storage::put() applies Laravel's own visibility and root logic — raw Flysystem Config passed to a Laravel disk behaves subtly differently than calling Filesystem directly.
  • S3 has no directories. directoryExists() costs a ListObjects call; listContents() on a bucket root can return the top-level prefix itself on some adapters (fixed in 3.0.13 / 3.30.1). Prefer explicit prefixes and shallow listings; deep listings on S3 are paginated network round-trips.
  • Config keys are the hidden API, and they change. Local: lazy_root_creation (3.1), disable_relative_path_resolution (3.34), inconclusive_mime_type (3.23), permissions, file_lock. S3: ACL, stream_reads, version, use_path_style_endpoint, add_content_md5 (3.26), forwarded_options, metadata, multipart settings. Read the changelog for the specific adapter before debugging weird behavior — several "bugs" are just unset options.
  • ACL beats per-call visibility on copy (3.35.2). If the adapter is constructed with an ACL key, a visibility you pass to copy() is ignored. Same class of issue for retain_visibility (3.19): copy/move visibility behavior differs per adapter and per config.
  • Same-path copy/move now no-ops on S3 (3.29) but was an error historically; there is a config option (3.18) for how to handle it. If your retry logic does move($p, $p), verify the configured behavior.
  • Exceptions: catch the interface, not the class. catch (FilesystemException $e) covers UnableToReadFile, UnableToWriteFile, UnableToDeleteFile, UnableToCheckExistence, UnableToListContents (3.4), PathTraversalDetected, CorruptedPathDetected. The underlying driver reason is usually in getPrevious() — added progressively in 3.0.22/3.0.23 and FTP error context in 3.29; log it or you'll see "Unable to write file at location X" with no cause.
  • Path normalization and safety. Leading slashes and .. are rejected (PathTraversalDetected), and broken UTF-8 now reports as a corrupted path (3.35.3). Don't interpolate raw user input into paths — use PathPrefixedAdapter or sanitize before calling.
  • A path of "0" is falsy in PHP and has historically caused unfiltered listings (fixed 3.30.1). If you have numeric-named directories, stay current.
  • Stat cache staleness. The Local adapter caches stat results. If another process mutates the directory outside Flysystem, fileExists()/mimeType() can lie until the cache clears (3.30 improved consistency); SFTP exposes a config to disable the stat cache (3.14).
  • Streams are single-use. A stream you passed to writeStream() is consumed; rewind or reopen it. Unclosed read streams leak file descriptors on long-running workers.
  • Visibility is advisory on FTP/SFTP/WebDAV. Don't build authorization on visibility(); treat it as a local/S3/GCS/Azure concept only.
  • checksum path must be normalized before the adapter sees it (3.29.1); if you compute checksums through a PathPrefixedAdapter, stay on a recent patch.
  • AsyncAwsS3 vs AwsS3V3: AsyncAws is materially lighter (no full AWS SDK) and chunks deletes at 100 objects to avoid memory blowups (3.13), but has a narrower feature surface and slightly different option names. Pick one deliberately; both accept temporaryUrl since 3.10.
  • Long-running workers: SFTP/FTP connections should be closed proactively via disconnect() (3.25) — otherwise queue workers leak sockets. SFTP disconnect_on_destruction and connectivity_pinging are opt-in for a reason.
  • Testing without a faked adapter is the #1 review smell. If a PR type-hints LocalFilesystemAdapter or calls file_get_contents next to Flysystem, it has already lost the abstraction.
  • Upgrading from v1/v2: listContents returns DirectoryListing not arrays, getSize→fileSize, getMimetype→mimeType, exceptions are no longer League\Flysystem\FileNotFoundException. Budget a real migration pass; the v3 API is not a drop-in.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route