overtrue/flysystem-qiniu
Flysystem adapter for Qiniu Cloud Storage (七牛云), letting Laravel/PHP apps read, write, delete, and list files on Qiniu buckets through Flysystem’s filesystem API. Configure access keys, bucket, and domain to use Qiniu for uploads and storage.
overtrue/flysystem-qiniu targets Flysystem 3.x. Check composer show overtrue/flysystem-qiniu league/flysystem after install — the adapter's method signatures and exception behavior differ between Flysystem 2 and 3, and mixing them produces confusing BadMethodCallExceptions.new QiniuAdapter($accessKey, $secretKey, $bucket, $domain) and that is the whole configuration surface. There is no YAML, no service provider, no published config file — expect to wire everything yourself.$adapter = new QiniuAdapter($ak, $sk, $bucket, $domain);
$fs = new \League\Flysystem\MyFilesystem($adapter);
$fs->write('hello.txt', 'hello');
echo $fs->publicUrl('hello.txt');
Laravel disk registration. The package ships no Laravel auto-discovery provider, so register a custom driver in AppServiceProvider::boot():
Storage::extend('qiniu', function ($app, $config) {
$adapter = new QiniuAdapter(
$config['key'], $config['secret'], $config['bucket'], $config['domain']
);
return new \League\Flysystem\Filesystem($adapter, [
'visibility' => 'public',
]);
});
Then declare the disk in config/filesystems.php and keep credentials in env(). Because there is no config file to publish, this is the canonical integration path and you should treat it as boilerplate rather than a workaround.
Treat it as a file storage adapter, not a CDN manager. The productive pattern is: upload and delete through the adapter, but do URL construction, cache busting, image processing, and invalidation in your own service class. The adapter hands you a raw getUrl(), which returns whatever domain string you passed in plus the key — no signing, no transformation.
Use streaming for user uploads. writeStream() avoids loading the whole file into memory and composes cleanly with Laravel's UploadedFile:
$stream = fopen($file->getRealPath(), 'r');
Storage::disk('qiniu')->writeStream($path, $stream);
Prefer this over put()/write() anywhere a user controls file size. Qiniu's SDK has its own resumable/chunked uploader, but the adapter does not expose it — for very large objects you should call qiniu/php-sdk directly and skip the adapter.
Separate public and private disks. Qiniu visibility is a bucket-level property, not a per-object one. Model this as two disks (qiniu-public, qiniu-private) pointing at two buckets, and route user-facing assets to the public one. Trying to toggle visibility per file through Flysystem will appear to work and silently not do what you expect.
Directory semantics: use prefixes deliberately. Qiniu keys are flat strings; "directories" are just shared prefixes. That means list operations, deleteDirectory, and prefix-scoped queries are all simulated. A useful pattern is to encode tenant or user scope as the first path segment (avatars/{user_id}/{uuid}.jpg) and list with listContents("avatars/{$id}"). Do not rely on empty directories existing.
Testing. Never let the test suite reach Qiniu — quota, latency, and flakiness will punish you. Swap the disk for Flysystem's InMemoryFilesystemAdapter in the test container, and keep a single opt-in integration test (env-gated) that exercises the real adapter for upload/URL/delete. Adapter-specific behavior — URL format, prefix listing — belongs in that integration test, because the in-memory adapter will not reproduce it.
https://cdn.example.com, others cdn.example.com. If your URLs come out as https://cdn.example.com//path or get double-schemed, this is why. Normalize once in your own config layer (trim trailing slash, strip scheme) and pass the exact form your installed version wants — verify by reading the adapter source, it is short.getUrl() on a private bucket returns a link that 403s. There is no error; you just get a URL that does not work. Use getTemporaryUrl($path, $expiry) for private buckets, and be aware Qiniu signs with an expiry timestamp, so caching signed URLs beyond their lifetime defeats the purpose.setVisibility/getVisibility cannot change how Qiniu serves an object. Don't build access control on top of them; build it on bucket choice plus signed URLs.UnableToReadFile / UnableToWriteFile family. If your application catches Flysystem exceptions to render friendly errors, wrap adapter calls in a small service that translates Qiniu\Exception (or the SDK's error arrays) into the Flysystem types.fileExists() costs an API call. Every exists() / stat() is a round trip to Qiniu with non-trivial latency from outside China. Avoid calling it in loops and in request-path hot code; prefer try { readStream() } catch (...).listContents(..., true) over a large bucket can be slow and can trip quota. Use narrow prefixes and iterate lazily rather than collecting everything into an array... segments produce keys that look different between your local disk and Qiniu. Normalize keys in one place before handing them to the adapter, and never store the raw user-supplied filename as the key.qiniu/php-sdk; a minor bump there has historically changed exception types and error shapes. Constrain it in composer.json rather than inheriting whatever ^7 resolves to.How can I help you explore Laravel packages today?