day4/cloudinary
A Laravel package for integrating Cloudinary, making it easy to upload, store, transform, and deliver images and videos through Cloudinary’s cloud service. It offers a simple, framework-friendly way to manage media assets in PHP applications.
This is a single-purpose Nova field package (a field, not a full SDK), so treat it as a UI widget that sits on top of the official cloudinary/cloudinary_php SDK. The first thing to do is not install it blind — open the repository and read the field class, because with zero stars, zero dependents and no README-driven docs, the source is the documentation.
Minimal path:
composer require day4/cloudinary
composer require cloudinary/cloudinary_php
composer.json inside the package for a extra.laravel.providers block and add the provider to config/app.php manually..env and reference them from config/services.php (or whatever config key the package's service provider merges — check mergeConfigFrom):
CLOUDINARY_URL=cloudinary://API_KEY:API_SECRET@CLOUD_NAME
The Cloudinary PHP SDK reads CLOUDINARY_URL natively, which is usually the least-friction path.php artisan vendor:publish --provider="Day4\Cloudinary\..."
use Day4\Cloudinary\Cloudinary;
public function fields(Request $request)
{
return [
Cloudinary::make('Image'),
];
}
Where to look first in the source, in this order: the make() signature and public setters (folder, transformations, multiple-file support), the Vue component in resources/js (that is where the actual upload request and payload shape live), and composer.json for the required Nova major version. The Nova version constraint is the single most common reason a field like this fails to render — a Nova 3-era field will break on Nova 4/5 because Field and the asset registration API changed.
First meaningful use case: a cover_image column on a Post/Product model where an editor uploads in Nova, and your frontend renders a transformed URL. Do not start by trying to make it handle every media type; prove the round-trip (upload → column value → accessor → rendered URL) on one field first.
Decide early what you persist: the public ID or the full URL. Persisting the Cloudinary public_id is almost always correct — it survives domain changes, lets you derive any transformation at render time, and makes deletion possible. Persisting a delivered URL bakes in a transformation and a cloud name that you will later want to change. If the field stores a URL, add an accessor/mutator normalizing to public ID before you build anything on top.
Derive URLs in the model, not in the field. Keep presentation logic out of Nova:
public function coverUrl(string $t = 'w_1200,c_limit,f_auto,q_auto'): ?string
{
return $this->cover_image
? cloudinary()->image($this->cover_image)->toUrl($t)
: null;
}
Use responsive/transform presets as named constants so Nova previews and Blade match. A field that previews a thumbnail while the site renders a 1600px hero causes "the image looks wrong in Nova" reports that are not bugs.
Multi-file / gallery fields. If the field supports multiple uploads, store a JSON array of public IDs and cast it ('gallery' => 'array'). Deleting the model should then trigger a cleanup observer, since Cloudinary assets are not garbage-collected:
protected static function booted()
{
static::deleted(fn ($m) => collect($m->gallery ?? [])
->each(fn ($id) => cloudinary()->destroy($id)));
}
Pair it with a plain fallback field during rollout: keep the existing Text/File field behind a feature flag or on a separate tab, so editors are not blocked by an upload outage or an SDK outage.
Environment split. Use a separate Cloudinary sub-account or folder prefix per environment (prod/, staging/) via the field's folder setter — otherwise staging uploads pollute production media, which is painful to unwind because Cloudinary deletion is per-asset.
Queue nothing in the field; queue on the model. The Nova field uploads synchronously in the browser to Cloudinary (unsigned or signed depending on implementation). Heavy post-processing (thumbnails in your own DB, moderation, AI tagging) belongs in a model observer dispatched to a queue, not in the field.
Testing. Never hit Cloudinary in feature tests. Fake at the boundary: assert the field exists on the resource and that the model stores the public ID string you give it, and stub URL generation via a container binding of the Cloudinary wrapper.
Verify the Nova major version before anything else. Check the package's composer.json for laravel/nova constraints. Nova is a paid, non-public package, so an abandoned field is effectively frozen at whatever Nova version existed when it was last committed. If it mismatches, the field will silently not appear in the form or throw a Vue mount error.
Unsigned vs signed uploads. Many small Nova Cloudinary fields use an unsigned upload preset, which means your cloud name and preset are visible in the compiled JS. That is acceptable for public media but not for private documents — check whether the Vue component calls upload_preset and lock the preset down in the Cloudinary console (allowed formats, max size, folder).
Credential precedence. The PHP SDK resolves config from CLOUDINARY_URL first; if that env var is set but stale, config/services.php values are ignored and you will debug the wrong config. php artisan config:clear after every change, and log cloudinary()->getAccount()->getCloudName() once to confirm which cloud you are actually talking to.
Draft/publish and stale assets. Uploading in Nova stores the asset immediately, before the model is saved. An editor who uploads and then abandons the form leaves an orphan. For high-churn resources, add a scheduled cloudinary:prune-style command that diffs Cloudinary folder contents against the DB.
Deleting. Overwriting a field leaves the old asset alive. In an updating observer, compare getOriginal('cover_image') to the new value and destroy the difference — carefully, since the same public ID may be referenced by another row.
Transformations in the preview. Nova's index view renders at ~40px; asking Cloudinary for a full-size original per row will make a 50-row index slow and burn transformation quota. Ensure the field requests a small derived version for previews (w_80,h_80,c_fill) rather than the raw URL.
Validation and required. Because the value is set by JS, ->rules('required') can behave unexpectedly if the component only sets state after upload completes. Prefer a server-side rule with a clear message, and verify with a deliberately failed upload.
Extension points to look for in the source: a storeAs/folder/transformation fluent setter, a disk/storage option (some packages support S3-backed Cloudinary fetch), prunable/deletable hooks, and any published config for default presets. If the field exposes none of these, wrap it: subclass the field and add setters rather than forking the repo, so a future (unlikely) upstream fix is still mergeable.
Maintenance risk. Score 0, no stars, no dependents, Bitbucket-hosted, no visible docs — assume no upstream support. Budget for a small internal fork, pin the exact commit in composer.json ("day4/cloudinary": "dev-master#<sha>") or vendor it into packages/, and keep the Cloudinary PHP SDK at a version you control. If the field breaks against a Nova upgrade, the realistic fallback is a Text field plus a small custom Vue component doing a signed upload through your own endpoint — plan for that exit path before you ship it widely.
How can I help you explore Laravel packages today?