misteio/cloudinary-bundle
A Symfony bundle that integrates Cloudinary’s image and video upload, transformation, and delivery services into your PHP application, with configuration for cloud credentials and asset handling.
Read this first: this is a Symfony bundle, not a Laravel package. It has no ServiceProvider, no config/cloudinary.php, no publishable assets, and Composer will not auto-register it in a Laravel app. It is a thin DependencyInjection wrapper around the official cloudinary/cloudinary_php SDK — 8 stars, 0 dependents, and no recent activity. Treat it as "a config file plus a service alias," because that's all it is.
If you are actually building in Laravel, do not pull this in. Use cloudinary-labs/cloudinary-laravel (maintained, ships a facade, a filesystem disk driver, and a config file) or bind the raw SDK yourself. If you have inherited a Symfony app using this bundle, the day-to-day API below is what you'll touch.
Where to look first
DependencyInjection/Configuration.php — the canonical list of config keys (cloud_name, api_key, api_secret, upload_prefix, secure, cdn_subdomain, etc.). Anything not listed there you cannot set via YAML.Resources/config/services.yml — shows the service id you inject (typically cloudinary / misteio_cloudinary) and which SDK class it aliases.Cloudinary\Api, Cloudinary\Uploader, Cloudinary::cloudinary_url). The bundle adds nothing on top, so SDK docs are the real reference.First use case — the 90% path: upload a file, persist the public_id, render a transformed URL.
// Symfony: constructor-inject the service
public function __construct(private \Cloudinary\Api $cloudinary) {}
$result = $this->cloudinary->upload('avatar.jpg', [
'folder' => 'users/avatars',
'public_id' => 'user_' . $user->getId(),
'overwrite' => true,
'invalidate' => true,
'transformation' => ['width' => 400, 'height' => 400, 'crop' => 'fill', 'gravity' => 'face'],
]);
$user->setAvatarPublicId($result['public_id']); // store the ID, not the URL
Laravel equivalent if you skip the bundle — bind the SDK once and move on:
// app/Providers/CloudinaryServiceProvider.php
$this->app->singleton(\Cloudinary\Cloudinary::class, fn () => new \Cloudinary\Cloudinary([
'cloud' => [
'cloud_name' => config('services.cloudinary.cloud_name'),
'api_key' => config('services.cloudinary.api_key'),
'api_secret' => config('services.cloudinary.api_secret'),
],
'url' => ['secure' => true],
]));
Store public_id, derive everything else. The single most important habit: persist public_id (plus resource_type and type when not image/upload) in your own schema. URLs, transformations, and deletions are all functions of that ID. Storing an absolute secure_url bakes in a CDN hostname and a transformation version you will later regret.
// URL generation is pure string building — no API call, no latency
$url = cloudinary_url($user->avatar_public_id, [
'width' => 200, 'height' => 200, 'crop' => 'thumb', 'gravity' => 'face',
'fetch_format' => 'auto', 'quality' => 'auto',
]);
Use fetch_format=auto / quality=auto (f_auto,q_auto). This is the highest-leverage one-line change you can make: Cloudinary serves WebP/AVIF to browsers that accept it and picks a quality that's visually indistinguishable. Do this before you optimize anything else.
Wrap the SDK behind your own interface. The bundle exposes the global-ish SDK class, which makes it awkward to mock and easy to scatter throughout controllers. Define App\Contracts\ImageStorage with upload(), delete(), url(), and implement it over Cloudinary. This also means the eventual migration off this abandoned bundle is a one-class change.
interface ImageStorage {
public function upload(UploadedFile $file, string $folder): string; // returns public_id
public function delete(string $publicId): void;
public function url(string $publicId, array $transform = []): string;
}
Queue the upload, not the URL. Uploads hit Cloudinary's API (hundreds of ms, occasionally seconds for video). Do them in a job after the request returns, and give your DB a placeholder state (avatar_status = 'processing'). URL generation is synchronous and cheap — never queue it.
Laravel disk integration. If you'd rather stay idiomatic, register a custom filesystem disk so Storage::disk('cloudinary')->put(...) and ->url() work with $request->file('photo')->store('photos', 'cloudinary'). cloudinary-labs/cloudinary-laravel gives you this out of the box; hand-rolling it is ~40 lines around upload() and cloudinary_url().
Deletion is two-phase. deleteAssets only removes the asset; existing URLs may still resolve from CDN edge caches until invalidate: true propagates (and invalidation is a rate-limited, sometimes paid feature). Plan for "deleted but still visible for a few minutes."
Signed uploads for browser-direct (unsigned presets only for public content). If you let the browser upload directly, do not embed your API secret. Use an unsigned upload preset scoped to a folder, or generate a signature server-side. The bundle has no opinion here — it's straight SDK config (upload_preset, signature).
composer.json, and assume you may need to replace it with a direct cloudinary/cloudinary_php dependency at any time. The interface pattern above is your insurance.Configuration.php cannot be passed via bundle config (e.g. per-environment upload presets, cname). Work around it by injecting parameters directly into the SDK constructor yourself.url vs secure_url. The bundle's secure flag defaults can bite you in production behind HTTPS — mixed-content warnings on images. Explicitly set secure: true in config and confirm you're using secure_url, not url.public_id is not a filename. If you pass photo.jpg, Cloudinary strips the extension for images but keeps it for resource_type: raw. Overwriting the same public_id with a different resource_type does not replace the old asset — you now have two assets under one ID path. Always include resource_type and type in delete/lookup calls when they aren't the defaults.w_100, w_101, … will happily create thousands of variants. Constrain widths to a fixed set (a srcset ladder like 320/640/1280/1920) rather than arbitrary values.\Cloudinary\Api\Exception\ApiError and log $e->getMessage() plus the response http_code and the public_id you attempted. For local failures, dump the request payload before the call — most bugs are a wrong resource_type or a missing upload_preset.services.yaml with your own class that composes the SDK. There are no events, no middleware, no hooks — don't go looking for them.cloudinary-labs/cloudinary-laravel or a 20-line service provider. The underlying SDK is the same; the bundle contributes nothing you can't write yourself in an afternoon.How can I help you explore Laravel packages today?