zing/laravel-flysystem-oss
A Laravel Flysystem adapter for Alibaba Cloud OSS that lets you use Laravel's Storage API with OSS buckets for uploading, reading, and managing files through familiar filesystem methods.
This is a Flysystem v3 driver that plugs Alibaba Cloud OSS into Laravel's Storage facade, so day-to-day you never touch the OSS SDK directly — you add one disk and keep using Storage::disk('oss').
Install and wire up (5 minutes):
composer require zingimmick/laravel-flysystem-oss
Package discovery registers the service provider; no config/app.php edit needed. Then add a disk in config/filesystems.php and mirror the keys into .env:
'oss' => [
'driver' => 'oss',
'access_key_id' => env('OSS_ACCESS_KEY_ID'),
'access_key_secret' => env('OSS_ACCESS_KEY_SECRET'),
'bucket' => env('OSS_BUCKET'),
'endpoint' => env('OSS_ENDPOINT'), // e.g. oss-cn-hangzhou.aliyuncs.com
'domain' => env('OSS_DOMAIN'), // optional CDN / custom domain
'scheme' => env('OSS_SCHEME', 'https'),
'is_cname' => env('OSS_IS_CNAME', false),
'options' => [],
],
Copy the key names from the package README rather than from a generic OSS tutorial — this fork does not use the same key names as the iidestiny package that most blog posts describe.
First use case: upload an avatar and hand back a URL.
$path = Storage::disk('oss')->putFile('avatars', $request->file('avatar'));
$url = Storage::disk('oss')->url($path);
Where to look first: config/filesystems.php (the disk block), then Storage::disk('oss') in tinker to confirm credentials and endpoint before writing any feature code. Storage::disk('oss')->files() returning a list is your smoke test.
Version context for 2.8.1: this release is almost entirely compatibility plumbing — Laravel 13, PHP 8.5, PHPUnit 12 — plus one behavioural fix to how the scheme config key is read. It is a safe minor bump; there are no breaking changes, but if you were relying on scheme being ignored (i.e. URLs coming out as http:// regardless of config), expect generated URLs to change.
Make OSS the default disk, not a special case. Set FILESYSTEM_DISK=oss so Storage::put(), Storage::url(), and $request->file()->store() all route to OSS. This keeps tests and local dev swappable via a single env var.
Prefer temporaryUrl() for anything user-facing on private buckets.
return Storage::disk('oss')->temporaryUrl($path, now()->addMinutes(15));
Call it at render time (or cache the URL for slightly less than its TTL) rather than persisting signed URLs in the database — they expire and will silently 403.
Store the path, resolve the URL. Keep a relative key (avatars/2024/abc.jpg) in your model and generate the URL in an accessor. Swapping domain later (CDN migration, bucket rename) then costs you a config change instead of a data migration.
Pass per-object options on write for content type and cache headers:
Storage::disk('oss')->put($path, $contents, [
'ContentType' => 'image/webp',
'CacheControl' => 'public, max-age=31536000',
'ContentDisposition' => 'attachment; filename="report.pdf"',
]);
The options array on the disk config applies the same defaults to every write on that disk — useful for a dedicated oss-assets disk.
Use a separate disk per bucket or per visibility concern (oss-private for uploads, oss-public/oss-cdn for assets). It is cheaper and clearer than branching on visibility inside callers, and it lets you point domain at a CDN only for the public one.
Stream large files instead of loading them. putFile()/putFileAs() and writeStream() avoid buffering whole objects in PHP memory; reserve put() with a string body for genuinely small payloads (thumbnails, JSON, generated PDFs under a few MB).
Directory semantics are prefix semantics. makeDirectory() is a no-op-ish abstraction and deleteDirectory() deletes by prefix. That is usually what you want, but be careful with deleteDirectory('uploads') when uploads-backup/ also lives under the same bucket — the prefix match is literal, so name your top-level prefixes so they cannot collide as string prefixes.
In tests, fake the disk rather than hitting OSS:
Storage::fake('oss');
and assert with Storage::disk('oss')->assertExists($path). For integration tests, run them against a dedicated throwaway bucket guarded by an env check, and clean up with deleteDirectory() in tearDown.
Laravel 13 / PHP 8.5 note: if you are on the freshly supported versions as of 2.8.1, pin with "zingimmick/laravel-flysystem-oss": "^2.8.1" — earlier 2.8.x versions declare narrower framework/PHP constraints and Composer will resolve you back to an older Laravel. The PHPUnit 12 support matters for your own suite: if you assert on Storage fakes, upgrade your test deps in the same PR so you do not straddle the Flysystem/PHPUnit matrix.
The scheme fix will change URLs. 2.8.1 corrects how the scheme config key is handled. If you previously set 'scheme' => 'http' or omitted it and ended up with the "wrong" scheme anyway, that behaviour is now consistent — check any code or fixtures that assert on absolute URLs, and re-check signed URLs behind a proxy where the scheme matters for signature validity.
Config key names are not portable. access_key_id/access_key_secret (this package) vs access_id/access_key (the iidestiny fork). Copying a snippet from a blog post yields a driver that boots, lists nothing, and throws an auth error that does not mention the missing key. Read the README for the version you installed.
url() returns a syntactic URL, not a permission grant. On a private bucket, Storage::url($path) looks correct and 403s in the browser. That confuses people during debugging — reach for temporaryUrl() to prove credentials work.
is_cname must match reality. If your domain is a custom domain bound to the bucket via CNAME, set is_cname => true; otherwise the adapter builds a bucket.endpoint style host and requests fail or land on the wrong host. Likewise endpoint should be the region endpoint, not a full URL with scheme — the scheme key supplies that.
Signed URLs expire, and clock skew breaks them. A container with drifting time produces UrlExpired or SignatureDoesNotMatch errors that look like credential problems. Check NTP before rotating keys.
Debug by escalating from the facade. First Storage::disk('oss')->files('/') (credentials + endpoint + bucket), then put() a tiny string (write permission), then temporaryUrl() (signing). That isolates config errors from permission errors from signature errors faster than reading stack traces.
Storage::fake('oss') does not exercise the adapter. It swaps in a local fake, so endpoint, scheme, CNAME, and option handling never run. Keep at least one thin real-bucket integration test, because that is exactly the class of bug 2.8.1's scheme fix addressed — config resolution bugs are invisible to fakes.
Extension points are thin by design. The provider registers a oss driver via the Storage manager; if you need custom behaviour (per-tenant buckets, custom HTTP client options, signing tweaks) extend Illuminate\Filesystem\FilesystemManager in your own service provider or wrap Storage::disk() in a small repository class rather than subclassing the adapter. You can also register additional disks at runtime with Storage::build([...]), which is handy for multi-tenant apps where the bucket name comes from a model.
Keep an eye on the dependency chain. This package wraps Flysystem v3 plus the Aliyun SDK; a Laravel major bump (13) or PHP bump (8.5) is a signal to run your storage tests first, since the failure mode is usually a silent behaviour change in the adapter, not a loud exception.
How can I help you explore Laravel packages today?