Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Unsplash Wrapper Laravel Package

shweshi/laravel-unsplash-wrapper

A Laravel wrapper for the Unsplash API that provides a clean, fluent interface for searching photos, collections, and users. Easily integrate Unsplash images and data into your Laravel/PHP applications with simple configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require shweshi/laravel-unsplash-wrapper
php artisan vendor:publish --provider="Shweshi\LaravelUnsplash\LaravelUnsplashServiceProvider"

The package relies on Laravel's package auto-discovery (5.5+), so no manual provider registration is normally needed. If you're on an older skeleton, or if discovery is disabled in composer.json, add Shweshi\LaravelUnsplash\LaravelUnsplashServiceProvider to config/app.php and register the Unsplash facade alias.

Get credentials first. Create an application at https://unsplash.com/oauth/applications, then put the keys in .env:

UNSPLASH_ACCESS_KEY=...
UNSPLASH_SECRET_KEY=...
UNSPLASH_CALLBACK_URL=https://yourapp.test/unsplash/callback

Publish config and confirm the keys land in config/unsplash.php. Then php artisan config:clear — the config is cached in production and a stale key is the number-one cause of "it just returns 401" reports.

Where to look first. The README is the only real documentation; the source lives in src/LaravelUnsplash.php and is small enough to read end to end. Skim it before writing code — method names map almost one-to-one onto Unsplash REST endpoints (searchPhotos, getPhoto, randomPhoto, getUser, getUserPhotos, getCollections, getCollectionPhotos, trackPhotoDownload).

First use case. The fastest win is a random hero/placeholder image or a search-backed image picker in an admin form:

use Shweshi\LaravelUnsplash\Facades\Unsplash;

$photo = Unsplash::randomPhoto();          // returns photo payload
$results = Unsplash::searchPhotos('mountains', 12, 1);

echo $results['results'][0]['urls']['regular'];

Verify the exact return shape in tinker before wiring it into a view — the wrapper generally passes through decoded JSON, so you get arrays with results, total, total_pages, etc.

Implementation Patterns

Wrap it, don't call it from controllers. Treat the facade as a transport, not a domain service. Bind a small ImageProvider interface in a service provider and implement it with the wrapper. This is what makes the package swappable later (see Gotchas) and keeps tests clean.

final class UnsplashProvider implements ImageProvider
{
    public function searchPhotos(string $q, int $page = 1): array
    {
        return Cache::remember("unsplash:{$q}:{$page}", now()->addHour(), fn () =>
            Unsplash::searchPhotos($q, 30, $page)['results'] ?? []
        );
    }
}

Always cache. Search results and photo metadata change slowly. A 1-hour Cache::remember per query/page is the difference between a usable gallery and a 403 wall. Cache the metadata, store only the URLs you actually need.

Paginate at the boundary. Unsplash caps per_page at 30 and exposes total_pages. Expose your own paginator to the UI so page-size changes don't require touching call sites:

$page = (int) request('page', 1);
$data = Unsplash::searchPhotos($query, 30, $page);
$paginator = new LengthAwarePaginator($data['results'], $data['total'], 30, $page);

Handle the download-tracking lifecycle explicitly. Unsplash requires you to ping the download endpoint when a user actually downloads/saves an image, not when the page renders. Wire trackPhotoDownload($photo['links']['download_location']) into the download controller and keep it out of the render path.

Use it in seeders and factories. For demo data, randomPhoto() or searchPhotos($category) in a seeder beats checked-in JPEGs:

User::factory()->count(50)->create()->each(function ($user) {
    $user->update(['avatar' => Unsplash::randomPhoto()['urls']['small']]);
});

Guard seeders behind a --class invocation so CI doesn't hit the live API.

Build a fallback path. Every call site should degrade to a local placeholder (or a previously cached URL) on exception, not just on 200. Combine with Http::retry-style backoff if your wrapper exposes the underlying client.

Gotchas and Tips

Rate limits are the whole game. New applications run in demo mode: 50 requests/hour. That is exhausted by one developer refreshing a gallery page. Apply for production approval on the Unsplash developer dashboard early — you cannot ship real traffic on demo limits regardless of how good your caching is. Log X-Ratelimit-Remaining from the response headers if the wrapper surfaces them.

Attribution and hotlinking are contractual, not optional. Unsplash's API guidelines require linking back to Unsplash, crediting the photographer (with a UTM-tagged profile link), and hotlinking images.unsplash.com URLs rather than rehosting. Add a small utm_source/utm_medium to your photo links; many apps miss this and get their key revoked.

Maintenance risk is real. Last release was September 2021, and the repo has ~19 stars. Expect:

  • No typed exceptions or DTOs — you'll be null-coalescing raw array responses.
  • Endpoints added to Unsplash since 2021 (topics, search filters, orientation/color params) are likely unexposed. Check src/ before assuming a parameter exists; if it's missing you'll be editing a fork.
  • Dependency constraints may conflict with newer Laravel versions. Pin the package and test upgrades deliberately.

Testing friction. If the wrapper constructs its own Guzzle client internally, Http::fake() will not intercept it — you'll get real network calls in tests. Two fixes: bind your provider interface to a fake in TestCase, or fork/bind the client. Prefer the interface approach; it also protects you if the package is abandoned.

Config caching bites in production. Changing .env on a deployed box does nothing until php artisan config:clear && php artisan config:cache. Symptom is a package that works locally and 401s in staging.

URL encoding and query quirks. Search terms with spaces, ampersands, or non-ASCII need encoding; pass plain strings and let the package (or your wrapper) encode once — double-encoding produces zero results rather than an error, which is painful to debug. Empty result sets return a valid 200 with results: [], so distinguish "no matches" from "request failed".

Facade accessor drift. A handful of small wrappers ship a facade whose getFacadeAccessor() returns a string binding missing from the provider. If Unsplash:: throws "A facade root has not been set," instantiate new LaravelUnsplash() via the container (app(LaravelUnsplash::class)) or fix the binding in your own provider.

Extension point. The cleanest long-term move is to leave the package in place but never call it outside one adapter class. When the wrapper lags the API or you need OAuth token refresh, you rewrite the adapter against the Unsplash HTTP docs and the rest of the app is untouched.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator