zgldh/qiniu-laravel-storage
Qiniu cloud storage driver for Laravel. Integrates Qiniu object storage with Laravel's filesystem, letting you upload, store and serve files through the familiar Storage facade. Supports custom domains, upload tokens and easy configuration via a dedicated filesystem disk.
Install the driver and publish its config:
composer require zgldh/qiniu-laravel-storage
php artisan vendor:publish --provider="zgldh\QiniuStorage\QiniuStorageServiceProvider"
On Laravel 5.5+ the service provider is auto-discovered. On 5.4 and older, register zgldh\QiniuStorage\QiniuStorageServiceProvider in config/app.php. The package registers a custom qiniu filesystem driver, so from then on everything goes through Laravel’s normal Storage API.
Set credentials in .env (names depend slightly on the published config, but the values are always the same four):
QINIU_ACCESS_KEY=...
QINIU_SECRET_KEY=...
QINIU_BUCKET=my-bucket
QINIU_DOMAIN=https://cdn.example.com # CDN domain bound to the bucket
Then add a disk in config/filesystems.php:
'qiniu' => [
'driver' => 'qiniu',
'access_key' => env('QINIU_ACCESS_KEY'),
'secret_key' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
'domain' => env('QINIU_DOMAIN'),
],
Older releases of this package read a separate config/qiniu.php with accessKey/secretKey/bucket/domain and use a disk definition that only contains 'driver' => 'qiniu'. Open the file that vendor:publish actually produced before you start debugging.
First use case — avatar upload. This is the shortest path to proving credentials, bucket and CDN domain are all correct:
$path = $request->file('avatar')->store('avatars', 'qiniu');
$url = Storage::disk('qiniu')->url($path);
// smoke test in tinker
Storage::disk('qiniu')->put('healthcheck.txt', 'ok');
Storage::disk('qiniu')->exists('healthcheck.txt'); // true
If put succeeds but the URL 404s, the problem is almost always the domain or the bucket’s CDN binding, not the code.
Single disk, configured as default where appropriate. Most projects add qiniu as a disk and either call Storage::disk('qiniu') explicitly or set FILESYSTEM_DRIVER=qiniu. Be explicit in application code — it makes local/test switching obvious.
Name files yourself, don’t trust client names. Qiniu keys are flat strings; there are no real directories, only prefixes. Use a deterministic, collision-free key and store it in the database:
$key = 'avatars/' . $user->id . '/' . Str::uuid() . '.' . $file->extension();
Storage::disk('qiniu')->putFileAs('', $file, $key);
Never store the full URL in the database — store the key and generate the URL at render time. Domains change, HTTPS gets enabled, and CDN domains get migrated.
Direct browser uploads for large files. Proxying large uploads through PHP is slow and burns memory. Use the package’s token helper server-side and the Qiniu JS SDK client-side:
$token = Storage::disk('qiniu')->getUploadToken([
'scope' => config('filesystems.disks.qiniu.bucket'),
'deadline' => time() + 600,
'saveKey' => 'uploads/$(year)$(mon)/$(etag)$(ext)',
'returnBody' => '{"key":"$(key)","hash":"$(etag)","size":$(fsize)}',
'callbackUrl' => route('qiniu.callback'),
'callbackBody' => 'key=$(key)&hash=$(etag)&size=$(fsize)&uid=' . auth()->id(),
]);
If you use callbacks, verify them server-side with the Qiniu SDK (Auth::verifyCallback) rather than trusting the POST body.
Private buckets. url() produces an unsigned link that will be rejected on a private bucket. Generate signed links on demand with a short TTL:
$url = Storage::disk('qiniu')->privateDownloadUrl($key, 3600);
Return these through a controller or a view composer; never cache signed URLs past their expiry.
Image processing. Qiniu does image transforms at the CDN edge via query strings, not in the package. Wrap it:
function thumb(string $key, int $w = 200): string
{
return Storage::disk('qiniu')->url($key) . "?imageView2/2/w/{$w}";
}
Same idea for imageMogr2, video frame capture, and avsc/vframe — all URL-based. Keep these helpers in one place so you can change the CDN domain in one shot.
Multi-bucket setups. Define multiple disks all using 'driver' => 'qiniu', each with its own bucket and domain (e.g. qiniu-public for avatars, qiniu-private for documents). This is cleaner than swapping config at runtime.
Media metadata. For audio/video, avInfo($key) returns duration, dimensions and bitrate, which is useful for validating uploads. getMetadata($key) gives size and mime type. Both hit the API — don’t call them in list loops.
Cleanup. Delete on model deletion via an observer, but tolerate failure (log and continue) — a failed Qiniu delete should not roll back a database transaction. Qiniu delete of a missing key returns an error; wrap it.
Testing. Storage::fake('qiniu') swaps in a local disk, so put/exists/assertExists work normally in feature tests. URL assertions will use the local fake domain, so assert on the key rather than the full URL.
FilesystemAdapter contract. This package’s last release is from 2022; verify it against your Laravel version before committing, and be prepared to pin Laravel 8 or maintain a fork. Run a put/url/delete cycle in a scratch project first — the failure mode is a driver not supported or a method signature error, not a clear message.domain must be the CDN domain bound to the bucket, with a scheme and without a trailing slash. A source-storage domain (or the bucket’s test domain, which expires) yields 401/404s that look like upload failures. Some versions expect a domains array with a default key rather than a plain string — check the published config..../uuid.ext) or call the CDN refresh API after overwriting.+, # or Chinese characters. Qiniu keys are raw strings; the adapter does not consistently encode when building URLs. Normalize to ASCII-safe keys if you can./avatars/x.jpg and avatars/x.jpg are different objects in some code paths. Strip them at the boundary.allFiles()/directories() are emulated via prefix listing and are paginated (1000 per call). Avoid them in request paths; keep your own index table if you need listing.false. Network errors and API errors surface as Qiniu\Http\Error or SDK exceptions. Wrap uploads in try/catch and inspect the error code (612 = no such file, 614 = file exists). exists() costs a stat call; use it sparingly.file_get_contents() on a 200MB video will OOM before it reaches Qiniu. Use putFile/streams for moderate sizes and browser-direct uploads for anything large. There is no resumable upload on the server side.php artisan config:clear. config:cache bakes env values in, which bites during deploys.StorageService/repository class that wraps Storage::disk('qiniu') behind an interface, so you can swap backends and add cross-cutting concerns (logging, retries, key generation) without touching call sites. Don’t scatter Storage::disk('qiniu') calls across controllers.How can I help you explore Laravel packages today?