apollopy/flysystem-aliyun-oss
A Flysystem adapter for Aliyun OSS (Alibaba Cloud Object Storage Service) that lets Laravel and PHP applications use OSS buckets as storage disks. Configure it to upload, download, and manage files through a familiar filesystem API, making it easy to offload media and assets to Alibaba Cloud.
This package wraps the Alibaba Cloud OSS SDK in a Flysystem adapter. Before installing, confirm you're on a Flysystem v1 stack — that means Laravel 5.x through 8.x. Laravel 9+ ships Flysystem v3, and this adapter (along with virtually all community OSS adapters in this era) will not load. If you're on a newer Laravel, prefer iidestiny/flysystem-oss or a similar v3-compatible adapter.
Minimal setup:
composer require apollopy/flysystem-aliyun-oss
You also need the underlying SDK (aliyuncs/oss-sdk-php) — check whether it landed in composer.json; if not, add it explicitly, since the adapter only knows how to speak the SDK interface, not how to create a client.
First look at the source. The adapter is small — read src/AliyunOssAdapter.php end-to-end before wiring it into Laravel. You want to see the exact constructor signature (typically (OssClient $client, string $bucket, string $prefix = '')) and confirm which Flysystem methods are actually implemented versus falling through to notImplemented().
First real use case: registering it as a Storage driver in a service provider so you can do:
Storage::disk('oss')->put('invoices/2024/04/acme.pdf', $pdfContent);
Instead of wiring Flysystem by hand, use Laravel's Storage::extend in AppServiceProvider::boot():
Storage::extend('aliyun-oss', function ($app, $config) {
$client = new \OSS\OssClient(
$config['key'],
$config['secret'],
$config['endpoint'],
$config['is_cname'] ?? false
);
$adapter = new \Apollo\Flysystem\AliyunOss\AliyunOssAdapter(
$client,
$config['bucket'],
$config['prefix'] ?? ''
);
return new \League\Flysystem\FilesystemAdapter(new \League\Flysystem\Filesystem($adapter));
});
Then add the disk to config/filesystems.php:
'oss' => [
'driver' => 'aliyun-oss',
'key' => env('ALIYUN_ACCESS_KEY_ID'),
'secret' => env('ALIYUN_ACCESS_KEY_SECRET'),
'bucket' => env('ALIYUN_OSS_BUCKET'),
'endpoint' => env('ALIYUN_OSS_ENDPOINT'),
'prefix' => env('ALIYUN_OSS_PREFIX', ''),
'is_cname' => env('ALIYUN_OSS_CNAME', false),
],
That's the whole ramp-up. Everything else is normal Storage usage.
Treat the Storage facade as the primary interface. Once registered, you should almost never touch OssClient or AliyunOssAdapter directly. Storage::disk('oss')->putFileAs(...), ->delete(...), ->exists(...), ->url(...), ->temporaryUrl(...) all route through the adapter and are the ergonomic surface for day-to-day code.
Per-environment prefixes. Put dev/, staging/, prod/ (or per-tenant paths) in the prefix constructor argument or in the file path itself. This keeps dev uploads physically separated in the same bucket without needing three buckets — cheap, and invaluable when you accidentally point staging at the prod disk.
Signed URLs for private buckets. If your bucket is private (it usually should be), generate download links with temporaryUrl() rather than storing public URLs. Storage::disk('oss')->temporaryUrl('path/to.pdf', now()->addMinutes(30)) should be your default pattern for user-facing downloads, and you should never persist a raw OSS URL to your database because the signature/expiry changes.
Stream uploads for large files. Avoid put() with file_get_contents(...) on anything over a few MB — use putFile, putFileAs, or Storage::disk('oss')->writeStream(...). The adapter delegates to the SDK's multipart upload path, which is what you want for anything non-trivial.
Controller pattern for uploads:
public function store(Request $request)
{
$path = $request->file('document')->storeAs(
'documents/'.$request->user()->id,
$request->file('document')->hashName(),
'oss'
);
Document::create(['path' => $path, 'user_id' => $request->user()->id]);
return back();
}
Store the abstract path (what storeAs returns), not a URL. Rebuild the URL on read via temporaryUrl() or url().
Service class for cross-cutting OSS concerns. Wrap common operations (uploadAvatar, downloadInvoice, pruneOldTempFiles) in a small OssStorage service that itself depends on Storage::disk('oss'). This isolates adapter-specific quirks from your controllers and makes it trivial to swap the driver later.
Testing integration. In tests, either (a) Storage::fake('oss') — this works because fake() swaps in an in-memory adapter regardless of the real driver — or (b) point the disk at MinIO in CI via endpoint, which also validates your SDK/region configuration. Storage::fake() is almost always the right answer for feature tests; MinIO matters only when you're specifically exercising the adapter.
Pair with a CDN when possible. OSS is often cheaper and faster behind Alibaba Cloud CDN. Set a base URL on the disk or resolve public URLs through your own cdn_url config, so swapping the url() output for a CDN hostname is a config change rather than a code hunt.
Flysystem version drift is the top pitfall. This package targets Flysystem v1. Installing it alongside league/flysystem:^3 (Laravel 9+) will not work — composer will either refuse to install or you'll end up with a class-not-found at runtime for League\Flysystem\FilesystemInterface. Verify with composer why league/flysystem before you commit. If you're on a newer Laravel, stop here and pick a maintenance-tracked adapter.
Endpoint format is easy to get wrong. Use the regional endpoint that matches your bucket's region (e.g. oss-us-east-1.aliyuncs.com, oss-cn-hangzhou.aliyuncs.com), not a generic one. A mismatched region produces cryptic 403s that look like auth failures but aren't. If the bucket is behind a CNAME, you must pass the is_cname flag to OssClient or the SDK will build signed requests against the wrong host.
Prefix handling is silently quirky. The adapter concatenates the prefix to keys — if you pass 'uploads' you may get uploadsfoo.txt rather than uploads/foo.txt. Always pass a trailing slash ('uploads/') and verify with a quick Storage::disk('oss')->put('probe.txt', 'ok') then ->path(...) / ->url(...) to confirm the key is what you expect.
Visibility ≠ file permissions in the usual sense. public here means "public-read ACL on the object", which is a bucket-level policy decision on the OSS side. If your bucket has "block public access" on, ->setVisibility() will fail quietly or throw depending on ACL settings. Decide up front: private bucket + temporaryUrl(), or public bucket + CDN. Mixing on the same bucket leads to confusion.
->url() does not sign. For private buckets, ->url() returns a URL that will 403. Use ->temporaryUrl(). If you need permanent public links, the object must have public-read ACL, and even then check whether your bucket policy allows it — the adapter just returns the SDK-composed URL, it doesn't validate.
Exceptions bubble up as OSS\Http\Exception and friends, not as Flysystem exceptions, in many cases. Wrap OSS calls in your service layer with a try/catch that translates SDK exceptions to your own domain exceptions. Logging the SDK's error message (getMessage()) directly into user-facing flash messages is a common accidental leak of endpoint/bucket names.
Directory semantics differ from local disks. OSS is a flat key-value store; "folders" are prefixes. ->deleteDirectory() issues recursive deletes, which can be slow for large trees. ->listContents() paginates internally — if you're iterating hundreds of thousands of files, use the SDK's iterator directly rather than relying on the adapter, which may not expose pagination controls.
Caching wrapper. If you have hot read paths (checking ->exists() before every render, for example), wrap the adapter in League\Flysystem\Cached\CachedAdapter — it exists in Flysystem v1 and can meaningfully reduce OSS API calls (and cost). Remember to invalidate on write.
Extension point: subclass the adapter. The adapter is small and easy to extend. If you need OSS-specific behaviour not exposed by the Storage facade — e.g. setting object ACLs at upload time, custom metadata headers, image processing (x-oss-process=image/...), or server-side encryption — subclass AliyunOssAdapter and expose the raw OssClient via a getter, rather than dropping down to the SDK in controllers.
Pin the SDK version. aliyuncs/oss-sdk-php has had breaking changes over the years, and this adapter was written against a specific era. Lock it in composer.json ("aliyuncs/oss-sdk-php": "~2.x" or whatever the adapter expects) to avoid surprises when composer update runs six months from now.
Upgrade signal. The package is at 90 stars and appears single-maintainer. If it goes stale, the exit path is straightforward — the adapter is only a few hundred lines, and by the time you need it, you'll already have isolated the calls in a service. Budget a small refactor rather than a rewrite when you migrate to a v3-compatible adapter.
How can I help you explore Laravel packages today?