codebar-ag/laravel-flysystem-cloudinary
Flysystem adapter that plugs Cloudinary into Laravel's filesystem, letting you store, retrieve, and manage uploaded images and files through the familiar Storage facade. Simple configuration and seamless integration with Laravel's native upload, URL generation, and file handling APIs.
Install the adapter and its transitive Flysystem driver:
composer require codebar-ag/laravel-flysystem-cloudinary
Optionally publish the package config (it just exposes the driver defaults):
php artisan vendor:publish --tag="flysystem-cloudinary-config"
Add credentials to .env. The Cloudinary SDK can read a single CLOUDINARY_URL, which is easier to keep in sync than three separate vars:
CLOUDINARY_URL=cloudinary://API_KEY:API_SECRET@CLOUD_NAME
Register a disk in config/filesystems.php:
'cloudinary' => [
'driver' => 'cloudinary',
'key' => env('CLOUDINARY_KEY'),
'secret' => env('CLOUDINARY_SECRET'),
'cloud_name'=> env('CLOUDINARY_CLOUD_NAME'),
'secure', => true,
'prefix' => env('CLOUDINARY_PREFIX', 'app'),
],
First use case: wire it up as your default storage disk (or a named disk next to local/s3) and move one upload path — a user avatar or a CMS image — onto it. Don’t migrate everything at once; Cloudinary’s model (public IDs, transformation URLs, resource types) behaves differently enough from S3 that you want a single flow working end-to-end first:
$path = $request->file('avatar')->store('avatars', 'cloudinary');
// avatars/<random>.jpg — the public ID, not a filesystem path
$url = Storage::disk('cloudinary')->url($path);
The two files to read first are the disk config you wrote and the underlying flysystem-cloudinary adapter source (in vendor/saucebase-dev/flysystem-cloudinary/). The Laravel package is a thin service provider; almost all behavior lives in the adapter, so debugging usually means reading that class, not this repo.
Upload from a request. Keep using Laravel’s store/storeAs so you stay disk-agnostic — that’s the main payoff of the Flysystem wrapper:
$path = $request->file('photo')->storeAs(
'users/'.$user->id,
$request->file('photo')->hashName(),
'cloudinary'
);
Because Cloudinary folders are just prefixes on the public ID, users/42/abc.jpg becomes the public ID app/users/42/abc.jpg if you set prefix to app.
Generated content. Storage::disk('cloudinary')->put('reports/q1.pdf', $bytes) works, but note the resource-type caveat below for non-image files.
URLs in Blade / API resources. Use Storage::disk('cloudinary')->url($publicId) for the plain CDN URL. For anything with transformations, don’t fight the adapter — compose the URL with the Cloudinary SDK or cloudinary_url() helper (if cloudinary-labs/cloudinary-laravel is also installed) and pass both the public ID and the transformation:
public function avatarUrl(int $size = 200): string
{
$id = $this->avatar_public_id;
return cloudinary()->image($id)
->resize(ResizeMode::fill, $size, $size)
->toUrl();
}
Store the public ID on your models, never a full URL. URLs rot when you change the cloud name, prefix, or transformations.
Multiple disks. Define separate disks when you need different Cloudinary folders or accounts (e.g. cloudinary for user uploads, cloudinary-media for editorial assets). Pointing several disks at the same cloud with different prefix values is the standard multi-tenant pattern.
Testing. Storage::fake('cloudinary') swaps in an in-memory Flysystem disk, so your upload tests run offline. Assert on the stored public ID rather than on a full URL, since the fake won’t produce real CDN URLs.
Interop with cloudinary-labs/cloudinary-laravel. If the project already uses that SDK for uploads, keep Flysystem for the "storage" surface (put/get/delete) and use the SDK for transformations, eager processing, and management APIs. They share the same credentials — don’t duplicate the env vars.
image by default. Cloudinary distinguishes image, video, and raw resource types. Uploading a PDF or ZIP through the plain Flysystem adapter will often land as an image resource and be rejected or delivered incorrectly. Either use a dedicated disk for raw assets or use the Cloudinary SDK’s uploadApi()->upload(..., ['resource_type' => 'raw']) directly.url() does not apply transformations. The Flysystem interface has no concept of them, so the returned URL is the untransformed original. If your code assumes Storage::url() gives you a resized thumbnail, it won’t — build those URLs separately.uploadApi()->invalidate(). In practice, prefer unique filenames (hashName() or a ULID) over overwriting.prefix is not a directory. It’s glued onto the public ID. Change it after launch and every previously stored ID breaks. Pick it once and put it under change control.delete() removes the asset on Cloudinary, but edge caches may serve it for a while. For user-initiated deletion with a legal expectation of removal, also call invalidate().directoryExists() and listing are approximate. Cloudinary’s folder semantics are derived from public IDs; allFiles()/allDirectories() can be slow or paged on large accounts. Treat the disk as a write/read-by-ID store, not a browsable filesystem.php artisan config:cache is fine with this package as long as you reference env() in config/filesystems.php (as shown) and not inside the disk array elsewhere. A common failure is credentials that work locally and break in production because they were read at runtime rather than baked into the cached config.LOG_CHANNEL=stderr and set debug => true in the disk config (if supported by the adapter) or add a Http\Client middleware in a service provider to log the underlying Cloudinary API calls. Most “the file uploaded but I can’t see it” reports trace back to a resource-type mismatch or a wrong prefix.Storage::fake() does not exercise Cloudinary-specific validation, so a green suite doesn’t prove your production uploads will succeed. Keep one integration test (or a manual smoke check) against a staging cloud for image, video, and raw uploads before each release.How can I help you explore Laravel packages today?