cloudinary/cloudinary_php
Cloudinary PHP SDK lets PHP applications upload, transform, optimize, and manage images and videos in Cloudinary. Install via Composer to generate image/video tags, secure URLs, and handle asset management. Version 3.x supports PHP 8.0–8.4.
Installation
composer require cloudinary/cloudinary_php
Register the service provider in config/app.php (Laravel 5.5+ auto-discovers).
Configuration
Add credentials to .env:
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Publish the config file:
php artisan vendor:publish --provider="Cloudinary\CloudinaryLaravel\CloudinaryServiceProvider"
Edit config/cloudinary.php for default settings (e.g., folder structure, transformations).
First Use Case Upload an image from a request:
use Cloudinary\Cloudinary;
use Cloudinary\Configuration\Configuration;
$cloudinary = new Cloudinary(new Configuration([
'cloud' => [
'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
'api_key' => env('CLOUDINARY_API_KEY'),
'api_secret' => env('CLOUDINARY_API_SECRET'),
],
]));
$result = $cloudinary->uploadApi()->upload(
fopen($_FILES['image']['tmp_name'], 'r'),
['folder' => 'user_uploads']
);
What's New in 3.3.0 — Custom PSR-18 HTTP Client
The SDK now accepts any PSR-18 ClientInterface implementation, so you can control transport behaviour (timeouts, retries, proxies, logging, tracing) or swap in a mock client for tests. See Implementation Patterns → Custom HTTP Client below.
$result = $cloudinary->uploadApi()->upload(
fopen($_FILES['file']['tmp_name'], 'r'),
['folder' => 'uploads/' . auth()->id()]
);
$result = $cloudinary->uploadApi()->upload(
'https://example.com/image.jpg',
['folder' => 'remote_uploads']
);
Inject a PSR-18 client into the Cloudinary configuration — useful for retries, logging, timeouts, proxies and testing:
use Cloudinary\Cloudinary;
use Cloudinary\Configuration\Configuration;
use GuzzleHttp\Client;
$config = new Configuration(config('cloudinary'));
$config->httpClient = new Client([
'timeout' => 30,
'connect_timeout' => 5,
]);
$cloudinary = new Cloudinary($config);
Bind it once in a service provider so the whole app shares the configured client:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(Cloudinary::class, function () {
$config = new Configuration(config('cloudinary'));
$config->httpClient = new \GuzzleHttp\Client([
'timeout' => 30,
'handler' => \GuzzleHttp\HandlerStack::create(), // add middleware here
]);
return new Cloudinary($config);
});
}
Guzzle and symfony/http-client (Psr18Client) both satisfy PSR-18. Laravel's own Http facade does not — wrap Guzzle or Symfony instead.
Because the client is now a dependency, you can attach middleware for request IDs, structured logging, or metrics without touching SDK internals.
$url = $cloudinary->url()->transformation([
'width' => 500,
'height' => 300,
'crop' => 'limit',
'gravity' => 'face'
])->generate('sample.jpg');
$url = $cloudinary->url()->transformation([
'effect' => 'sepia'
])->generate('private.jpg', ['expires' => time() + 3600]);
$cloudinary->deleteResourcesByTag('user_' . auth()->id());
// OR delete by public ID
$cloudinary->deleteResources(['public_id' => 'sample.jpg']);
$publicIds = ['image1.jpg', 'image2.jpg'];
$cloudinary->deleteResources($publicIds);
Create a custom Cloudinary adapter for Storage::disk():
// config/filesystems.php
'cloudinary' => [
'driver' => 'cloudinary',
'cloudinary' => [
'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
'api_key' => env('CLOUDINARY_API_KEY'),
'api_secret' => env('CLOUDINARY_API_SECRET'),
],
],
Then use it like any other disk:
Storage::disk('cloudinary')->put('folder/file.jpg', fopen('local.jpg', 'r'));
Use accessors/mutators in models:
class Product extends Model {
public function getImageUrlAttribute() {
return $this->image_id
? $this->cloudinary()->url()->generate($this->image_id)
: null;
}
public function setImageUrlAttribute($url) {
$result = $this->cloudinary()->uploadApi()->upload($url);
$this->image_id = $result['public_id'];
}
}
API Key/Secret Mismatch
Authentication failed errors..env and ensure no trailing spaces in credentials.Folder Permissions
['folder' => 'user_' . auth()->id() . '/uploads']
Transformation Errors
Invalid transformation or broken images.generate() method to test URLs before rendering.Rate Limiting
429 Too Many Requests.try/catch blocks around your code.Private Assets
Route::get('/private/{id}', function ($id) {
$url = $this->cloudinary->url()->generate($id, ['expires' => time() + 300]);
return redirect()->to($url);
});
Custom PSR-18 Client Pitfalls (3.3.0)
Psr\Http\Client\ClientInterface. Illuminate\Support\Facades\Http does not — use GuzzleHttp\Client or Symfony\Component\HttpClient\Psr18Client.ClientExceptionInterface, the SDK can't surface API errors and you'll get confusing "empty response" failures. Always rethrow or wrap as a PSR-18 exception.POST /upload can create duplicate assets on a timeout-then-success. Restrict retries to reads (URL/admin calls) or use Cloudinary upload_preset/public_id idempotency by supplying a deterministic public_id.timeout in the tens of seconds (or none) and a modest connect_timeout.verify with your CA bundle or proxy instead.guzzlehttp/guzzle isn't pulled in by the SDK for this feature; add it explicitly if you use it.Default Folder
config/cloudinary.php:
'default_folder' => 'app_uploads',
Transformation Caching
$url = $cloudinary->url()->transformation([
'fetch_format' => 'auto',
'quality' => 'auto'
])->generate('image.jpg', ['c_name' => 'my_cache']);
Async Uploads
upload() method with async:
$result = $cloudinary->uploadApi()->upload($file, ['async' => true]);
Where to Configure the HTTP Client
Configuration object, not in config/cloudinary.php's credential array (it can't be serialised). Keep it in a service provider or a small factory class so config:cache stays safe.Custom Upload Presets
$cloudinary->uploadApi()->upload($file, [
'upload_preset' => 'my_preset_name'
]);
Webhooks
// Example: Listen for upload notifications
$cloudinary->uploadApi()->upload($file, [
'notification_url' => route('cloudinary.webhook')
]);
Middleware for Transformations
public function handle($request, Closure $next) {
$response = $next($request);
$response->setContent(
str_replace(
'sample.jpg',
$this->cloudinary->url()->transformation(['width' => 800])->generate('sample.jpg'),
$response->getContent()
)
);
return $response;
}
HTTP Client Middleware as an Extension Point (3.3.0)
$stack = HandlerStack::create();
$stack->push(Middleware::tap(
fn ($req) => logger()->debug('cloudinary.request', ['uri' => (string) $req->getUri()]),
fn ($req, $res) => logger()->debug('cloudinary.response', ['status' => $res->getStatusCode()])
));
$config->httpClient = new Client(['handler' => $stack]);
X-Request-Id), or route through an egress proxy in locked-down environments.Fallback for Local Storage
try {
$url = $cloudinary->url()->generate('image.jpg');
} catch (\Exception $e) {
$url = Storage::disk('local')->url('fallback/image.jpg');
}
Testing
use Cloudinary\Cloudinary;
use Cloudinary\Configuration\Configuration;
use Http\Mock\Client as MockClient;
$mock = new MockClient(); // php-http/mock-client
$mock->addResponse(new \GuzzleHttp\Psr7\Response(200, [], json_encode([
'public_id' => 'sample', 'secure_url' => 'https://res.cloudinary.com/demo/sample.jpg',
])));
$config = new Configuration(config('cloudinary'));
$config->httpClient = $mock;
$this->app->instance(Cloudinary::class, new Cloudinary($config));
$cloudinary = Mockery::mock(Cloudinary::class);
$cloudinary->shouldReceive('url')->andReturnSelf();
$cloudinary->shouldReceive('generate')->andReturn('http://test.url/image.jpg');
.env.testing with placeholder credentials so URL generation (which is signing-only, no HTTP) works offline.How can I help you explore Laravel packages today?