marksitko/laravel-unsplash
Laravel Unsplash is a clean wrapper around the Unsplash API for Laravel apps. Search and fetch photos, collections, users, and topics, track downloads, and use a fluent facade with config-driven credentials to add stock imagery to any project in minutes.
Install the wrapper and publish its config:
composer require marksitko/laravel-unsplash
php artisan vendor:publish --tag=unsplash-config
Then set your Unsplash Access Key (not the Secret Key — the secret is only needed for OAuth flows you almost certainly don't need):
UNSPLASH_ACCESS_KEY=your-access-key
Look at these three things first, in this order:
config/unsplash.php — the access key binding and any default query params the package merges into requests.Unsplash facade (and its underlying client class) — this is the entire public surface area; skim method signatures before reading the README examples.First real use case — populating a media picker or seed data with a search:
use Marksitko\Unsplash\Unsplash; // or the registered facade alias
$results = Unsplash::searchPhotos('mountain sunrise', [
'per_page' => 12,
'page' => 1,
'lang' => 'de', // new in v3.1.0
]);
v3.1.0 relevance: the only change is a new lang query parameter on photo search. If you previously hard-coded en-US behaviour or worked around locale by appending terms to the query string, you can drop that workaround. No breaking changes, no config migration — this is a drop-in minor bump.
Wrap the facade in a small service, not in controllers. The facade is convenient but untestable-by-default and easy to scatter across the codebase. One UnsplashService (or a repository) that owns caching, retries, and parameter normalisation pays for itself the first time you need to swap an image source.
Drive lang from the application locale. The new parameter is most valuable when it's derived, not typed:
$lang = substr(app()->getLocale(), 0, 2); // 'en', 'de', 'fr'
$photos = Unsplash::searchPhotos($term, [
'per_page' => 20,
'lang' => $lang,
]);
Clamp it to the languages Unsplash actually supports (a two-letter ISO-639-1 code) and fall back to en — an unsupported code returns fewer or less-relevant results rather than an error, so failures here are silent.
Treat search as a cacheable, rate-limited read. Unsplash's demo tier is famously tight (tens of requests per hour). Cache result sets keyed on a hash of query + params, with a short TTL (minutes to an hour) for search and a much longer one for individual photo lookups, which never change:
$key = 'unsplash:search:' . md5($term . serialize($params));
return Cache::remember($key, now()->addMinutes(30), fn () => Unsplash::searchPhotos($term, $params));
Build the query array, don't concatenate strings. Pass lang, page, per_page, orientation, color, order_by as an associative array. Keeping parameters structured means the cache key, the log context, and the request all derive from one source of truth — and it's what makes adding lang a one-line change rather than a refactor.
Use it behind a queued job for anything bulk. Seeding a catalogue of images, warming a cache, or syncing a collection should be a queued job with rateLimit/backoff configured. Synchronous loops over search results in a request cycle will hit the rate limit and take your page down with it.
Fake the transport in tests. Bind an Http::fake() response (or swap the client in the container) rather than hitting the live API. Assert on the outgoing URL to prove lang is being forwarded correctly — that's the one thing a v3.1.0 upgrade could silently regress if you were passing params positionally.
lang affects search relevance and metadata, not image content. It does not translate titles you display; the alt_description/description fields come back in whatever language Unsplash has for that photo. Don't promise localised captions to stakeholders.lang is a photo-search parameter. Passing it to unrelated endpoints (random photo, collection listing, photo lookup) is a no-op at best and an unexpected 400 at worst. Keep it scoped to the search call.config/unsplash.php and your config:cache state first.UNSPLASH_ACCESS_KEY and it's still null in production, you forgot php artisan config:cache. Same trap applies if you override defaults in the config file.X-Ratelimit-Remaining header and log it so you notice degradation before users do.per_page has a hard maximum (30 for search on most tiers) and very deep pages stop being useful. Don't build an infinite-scroll UI that assumes unbounded page values.Http::withHeaders() client. Prefer this over editing vendor code when you need custom headers or a proxy.^3.1 rather than ^3.0 if lang support is load-bearing for a localised feature — 3.0 would silently accept and ignore the parameter.How can I help you explore Laravel packages today?