jrm2k6/cloudder
Cloudder is a Laravel wrapper for the Cloudinary PHP SDK, offering a simple facade to upload, transform, tag, and delete images and videos in the cloud. It streamlines media management in Laravel apps with an easy, expressive API for Cloudinary operations.
Install and register. The package predates Laravel 5.5 auto-discovery, so Composer alone won't wire it up:
composer require jrm2k6/cloudder
Then in config/app.php:
'providers' => [
// ...
Jrm2k6\Cloudder\CloudderServiceProvider::class,
],
'aliases' => [
// ...
'Cloudder' => Jrm2k6\Cloudder\Facades\Cloudder::class,
],
Publish the config and set credentials.
php artisan vendor:publish --provider="Jrm2k6\Cloudder\CloudderServiceProvider"
This drops config/cloudder.php with cloud_name, api_key, api_secret (and usually an upload_preset). Depending on the version you resolve, those may be hardcoded strings rather than env() lookups — if so, edit the file to read CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET and add them to .env. Never commit the secret.
Where to look first. Open vendor/jrm2k6/cloudder/src/Cloudder.php and read the method signatures before anything else. This wrapper is thin, and the return types ($this vs. array) differ between tags. Reading ~200 lines saves you an hour of guessing.
First use case — server-side upload from a form.
use Cloudder;
public function store(Request $request)
{
$request->validate(['image' => 'required|image|max:5120']);
try {
Cloudder::upload($request->file('image')->getRealPath(), null, [
'folder' => 'avatars',
'width' => 400,
'height' => 400,
'crop' => 'fill',
'quality' => 80,
'fetch_format' => 'auto',
]);
$result = Cloudder::getResult(); // array: public_id, secure_url, format, bytes...
$publicId = Cloudder::getPublicId(); // string, use this as your DB reference
return redirect()->back()->with('url', $result['secure_url']);
} catch (\Exception $e) {
report($e);
return back()->withErrors('Upload failed.');
}
}
Store public_id, not the URL. URLs are derivable; public IDs are the durable handle for deletes, renames, and tag operations.
Store public IDs, render URLs on demand. Persist $result['public_id'] plus whatever metadata you care about (format, bytes, width, height). Generate display URLs at read time so you can change transformations without a data migration:
$thumb = Cloudder::show($user->avatar_public_id, [
'width' => 120, 'height' => 120, 'crop' => 'thumb', 'gravity' => 'face',
]);
Isolate Cloudinary in a service class. The facade carries mutable state (uploadedResult, publicId), so calling it from controllers and jobs interchangeably makes ordering bugs likely. Wrap it:
class ImageService
{
public function upload(UploadedFile $file, string $folder): array
{
Cloudder::upload($file->getRealPath(), null, ['folder' => $folder]);
$result = Cloudder::getResult();
return [
'public_id' => $result['public_id'],
'format' => $result['format'],
'bytes' => $result['bytes'],
];
}
public function delete(string $publicId): void
{
Cloudder::delete($publicId);
}
}
Now swapping the archived wrapper for the official SDK is a one-file change instead of a grep across the codebase.
Queue heavyweight work. Uploading large images or triggering eager transformations synchronously will blow past PHP's max_execution_time. Dispatch a job:
UploadImageToCloudinary::dispatch($path, $publicId)->onQueue('media');
Pass the path (or a temporary file), not the UploadedFile object — serializing request objects into queue payloads is fragile. Alternatively use Cloudinary's own async => true option and poll/webhook for completion.
Manage lifecycle with a model event or observer. Deleting a record without deleting the asset leaves orphaned storage you pay for:
static::deleted(fn ($post) => Cloudder::delete($post->image_public_id));
Guard against nulls and wrap in try/catch — a Cloudinary outage should not roll back a database delete.
Presigned / unsigned browser uploads. For large files, skip your server entirely: set an unsigned upload preset in the Cloudinary dashboard and post directly from the browser, then send only the returned public_id to your Laravel endpoint. The wrapper is not involved in this flow at all, which is fine — you only need the API credentials for server-side deletion and signed operations. Validate on arrival that the public_id actually exists (Cloudder::show()) before trusting it.
Folder and tag organization. Use folder on upload ('users/avatars', 'posts/2024') and tags for cross-cutting concerns (['featured', 'homepage']). Bulk operations (Cloudder::addTag, removeTag, deleteResourcesByPrefix) are far cheaper than per-asset loops.
Cloudinary\Uploader → Cloudinary\Api\Upload\UploadApi), and this wrapper targets the old global-namespace API. Pin cloudinary/cloudinary_php to a compatible major in your composer.json, or a composer update will silently break the facade with "class not found."each()/create_function() style code. Verify in a throwaway container before committing to the package.php artisan config:cache snapshots env() at build time. Changing CLOUDINARY_* in .env on a cached production box does nothing until you re-cache. Same trap applies to CLOUDINARY_URL, which the underlying SDK reads on its own and can shadow your published config.upload() may return $this, not the result. Verify the signature. If you wrote $result = Cloudder::upload(...) and then $result['public_id'], you may be indexing into an object. Use getResult() / getPublicId() explicitly.secure_url, never url. On HTTPS pages the plain url triggers mixed-content blocking, often only visible in the browser console rather than as a server error.uploadedResult/publicId persist between jobs. If a job uploads and a later code path reads getResult() without a fresh upload, you can get the previous job's asset. Reset explicitly or resolve a fresh instance per operation.show() must match your account settings. If private_cdn or secure_distribution differs between the SDK config and your account, generated URLs 404 rather than erroring. Confirm the URL Cloudinary's media library produces matches what you render.public_id serves the stale cached version for minutes to hours. Pass invalidate => true, or version the public ID (e.g., append a hash) so the URL changes.delete() succeeds on missing assets. Cloudinary returns "result" => "not found" rather than throwing. Check the response if deletion is idempotency-critical for your cleanup jobs.CLOUDINARY_UPLOAD_PRESET is configured and also passed per-call, the preset wins for fields it defines. Debug "my crop setting is ignored" by checking the preset in the dashboard first.getResult() array on failure. Cloudinary embeds useful context (error.message, error.http_code) that the wrapper passes straight through without normalization, and PHP's exception message is often just "Bad Request".cloudinary-labs/cloudinary-laravel or the raw SDK behind your service class. Because you isolated calls, this is mechanical — the public ID scheme and stored columns carry over unchanged.How can I help you explore Laravel packages today?