mahdimajidzadeh / laravel-unsplash
Laravel package for the Unsplash API
Package info
github.com/MahdiMajidzadeh/Laravel-Unsplash
pkg:composer/mahdimajidzadeh/laravel-unsplash
Requires
- php: ~5.6|~7.0|^8.0
- guzzlehttp/guzzle: ~6.0
- illuminate/support: >=5.1
This package is auto-updated.
Last update: 2026-07-27 12:56:09 UTC
README
A Laravel package covering the whole Unsplash API: photos, users, the logged-in user, collections, topics, search, stats and the user authentication (OAuth) workflow.
Install
Via Composer
$ composer require mahdimajidzadeh/laravel-unsplash
If you do not run Laravel 5.5 (or higher), then add the service provider and the
facade alias in config/app.php:
'providers' => [
MahdiMajidzadeh\LaravelUnsplash\LaravelUnsplashServiceProvider::class,
],
'aliases' => [
'Unsplash' => MahdiMajidzadeh\LaravelUnsplash\Facades\Unsplash::class,
],
On Laravel 5.5+ package auto-discovery takes care of both.
Publishing the configuration is optional — the package ships with defaults — but useful if you want to tweak them:
$ php artisan vendor:publish --tag=unsplash-config
That copies the defaults to config/unsplash.php.
Configuration
Register an application at unsplash.com/oauth/applications
and add your keys to .env:
UNSPLASH_ACCESS_KEY=your-access-key
# Only needed for the user authentication (OAuth) workflow
UNSPLASH_SECRET_KEY=your-secret-key
UNSPLASH_REDIRECT_URI=https://your-app.test/unsplash/callback
# Optional: act on behalf of a single user on every request
UNSPLASH_ACCESS_TOKEN=
The legacy ApplicationID configuration key is still honoured, so
configuration files published by older versions of this package keep working.
Usage
Everything is reachable from the Unsplash facade:
use MahdiMajidzadeh\LaravelUnsplash\Facades\Unsplash;
$photos = Unsplash::photos()->photos(['per_page' => 30])->get();
You can also resolve MahdiMajidzadeh\LaravelUnsplash\Unsplash from the
container, or instantiate a single resource directly — as in previous versions
of this package:
$unsplash = new MahdiMajidzadeh\LaravelUnsplash\Photo();
$photos = $unsplash->photos()->get();
Every endpoint method performs the request and returns the resource, so the result can be read with:
| Method | Returns |
|---|---|
get() |
the decoded body (stdClass or array of stdClass) |
getArray() |
the decoded body cast to an array |
toArray() |
the decoded body as a nested array |
raw() |
the raw JSON body |
status() |
the HTTP status code |
headers() |
all response headers |
totalItems() |
total number of items available |
totalPages() |
total number of pages available |
links() |
the parsed Link header (first, prev, next, last) |
rateLimit() |
requests allowed per hour |
rateLimitRemaining() |
requests left for the current hour |
response() |
the response object, for anything else |
$photos = Unsplash::photos()->photos(['page' => 2, 'per_page' => 30]);
$photos->get(); // the photos
$photos->totalPages(); // 1234
$photos->response()->nextPage(); // 3
$photos->rateLimitRemaining(); // 987
See the Unsplash documentation for the
parameters accepted by each endpoint; they are passed straight through as the
$params array, with a few conveniences:
nullvalues are dropped, so you can pass optional parameters unconditionally.- Booleans are sent as the
true/falsestrings Unsplash expects. - Lists are sent as the comma separated values the API documents for multi value
parameters, so
['collections' => [123, 456]]becomescollections=123,456. An empty list is left out entirely. - Nested parameters such as
locationandexifon a photo update keep their keys.
Photos
$photos = Unsplash::photos();
$photos->photos($params)->get(); // GET /photos
$photos->single($id, $params)->get(); // GET /photos/:id
$photos->random($params)->get(); // GET /photos/random
$photos->statistics($id, $params)->get(); // GET /photos/:id/statistics
$photos->download($id); // GET /photos/:id/download — returns the URL
$photos->trackDownload($id)->get(); // GET /photos/:id/download — chainable
$photos->update($id, $params)->get(); // PUT /photos/:id (write_photos)
$photos->like($id)->get(); // POST /photos/:id/like (write_likes)
$photos->unlike($id); // DELETE /photos/:id/like (write_likes)
all() is an alias of photos(), find() of single() and statistic() of
statistics().
Unsplash requires download($id) (or trackDownload($id)) to be called
whenever your application downloads a photo, so the photographer gets credited.
getID() and getURL() are available on any response holding photos — a
single photo, a list, or search results:
Unsplash::photos()->random()->getID(); // WLUHO9A_xik
Unsplash::photos()->random()->getURL(); // 1600x900, cropped
Unsplash::photos()->random()->getURL(800, 600);
// https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?ixid=...&w=800&h=600&fit=crop
Unsplash serves its images through Imgix, so getURL() takes the raw url from the
api response and appends the sizing parameters to it. Use getSizedURL() to pick one
of the sizes Unsplash returns as-is, or to pass your own Imgix parameters:
Unsplash::photos()->random()->getSizedURL('regular'); // raw, full, regular, small, thumb
Unsplash::photos()->random()->getSizedURL('raw', ['w' => 800]);
All three methods return null when the response holds no photo.
Users
$users = Unsplash::users();
$users->single($username, $params)->get(); // GET /users/:username
$users->portfolio($username); // GET /users/:username/portfolio — returns the URL
$users->photos($username, $params)->get(); // GET /users/:username/photos
$users->likes($username, $params)->get(); // GET /users/:username/likes
$users->collections($username, $params)->get(); // GET /users/:username/collections
$users->statistics($username, $params)->get(); // GET /users/:username/statistics
find() is an alias of single() and statistic() of statistics().
Current user
These endpoints need a bearer token — see User authentication.
$me = Unsplash::withAccessToken($token)->me();
$me->profile()->get(); // GET /me (read_user)
$me->update($params)->get(); // PUT /me (write_user)
Collections
$collections = Unsplash::collections();
$collections->collections($params)->get(); // GET /collections
$collections->single($id, $params)->get(); // GET /collections/:id
$collections->photos($id, $params)->get(); // GET /collections/:id/photos
$collections->related($id)->get(); // GET /collections/:id/related
$collections->create($params)->get(); // POST /collections (write_collections)
$collections->update($id, $params)->get(); // PUT /collections/:id (write_collections)
$collections->delete($id); // DELETE /collections/:id (write_collections)
$collections->addPhoto($id, $photoId)->get(); // POST /collections/:collection_id/add (write_collections)
$collections->removePhoto($id, $photoId); // DELETE /collections/:collection_id/remove (write_collections)
create() also accepts the title directly:
Unsplash::withAccessToken($token)->collections()->create('Good dogs', 'A description', false);
all() is an alias of collections() and find() of single().
Topics
$topics = Unsplash::topics();
$topics->topics($params)->get(); // GET /topics
$topics->single($idOrSlug, $params)->get(); // GET /topics/:id_or_slug
$topics->photos($idOrSlug, $params)->get(); // GET /topics/:id_or_slug/photos
all() is an alias of topics() and find() of single().
Search
$search = Unsplash::search();
$search->photo($query, $params)->get(); // GET /search/photos
$search->collection($query, $params)->get(); // GET /search/collections
$search->user($query, $params)->get(); // GET /search/users
photos(), collections() and users() are aliases of the above. Search
responses wrap their matches, so results() returns them without the
surrounding counters:
$search = Unsplash::search()->photo('dogs', ['orientation' => 'landscape']);
$search->results(); // the photos
$search->totalItems(); // 1337
$search->totalPages(); // 134
Stats
$stats = Unsplash::stats();
$stats->total()->get(); // GET /stats/total
$stats->month()->get(); // GET /stats/month
User authentication
Public requests are authenticated with your access key. To read private data or act on behalf of a user, send them through the OAuth workflow and use the resulting bearer token.
use MahdiMajidzadeh\LaravelUnsplash\Facades\Unsplash;
// 1. Send the user to Unsplash to authorize your application
Route::get('unsplash/redirect', function () {
return Unsplash::oauth()->redirect(['read_user', 'write_likes']);
// or build the URL yourself:
// return redirect(Unsplash::oauth()->authorizeUrl(['read_user'], null, $state));
});
// 2. Exchange the code Unsplash sends back for an access token
Route::get('unsplash/callback', function (Illuminate\Http\Request $request) {
$token = Unsplash::oauth()->accessToken($request->query('code'));
// 3. Use it — access tokens do not expire
return Unsplash::withAccessToken($token)->me()->profile()->get();
});
requestToken($code) gives you the whole token response (access_token,
token_type, scope, created_at) instead of just the token.
Available scopes are listed in OAuth::SCOPES: public, read_user,
write_user, read_photos, write_photos, write_likes,
write_followers, read_collections, write_collections. The default scopes
used by redirect() and authorizeUrl() come from the unsplash.scopes
configuration entry.
withAccessToken($token) returns a copy, so the shared instance keeps using
your access key. It is available on the Unsplash entry point and on every
resource:
$collections = Unsplash::collections()->withAccessToken($token);
$collections->create('Good dogs');
Errors
Non 2xx responses throw an exception carrying the status code and the messages Unsplash returned:
use MahdiMajidzadeh\LaravelUnsplash\Exceptions\NotFoundException;
use MahdiMajidzadeh\LaravelUnsplash\Exceptions\UnsplashException;
try {
Unsplash::photos()->single('does-not-exist')->get();
} catch (NotFoundException $e) {
$e->status(); // 404
$e->errors(); // ['Couldn't find Photo']
} catch (UnsplashException $e) {
// any other API error
}
UnauthorizedException (401), ForbiddenException (403), NotFoundException
(404), ValidationException (422) and RateLimitException (429) all extend
UnsplashException, which extends RuntimeException.
Requests that never reach the API — DNS failures, timeouts, refused connections
— throw a ConnectionException, which extends UnsplashException too. Catching
UnsplashException therefore covers every failure mode, and
$e->getPrevious() gives you the underlying Guzzle exception.
Testing
Unsplash::fake() answers from a queue of responses instead of calling the API.
When the container is booted it replaces the bound instance, so the facade and
anything type hinting Unsplash receive the fake too:
use MahdiMajidzadeh\LaravelUnsplash\Facades\Unsplash;
public function test_it_shows_a_random_photo()
{
$unsplash = Unsplash::fake([
['id' => 'abc123', 'urls' => ['raw' => 'https://images.unsplash.com/photo-1']],
]);
$this->get('/')->assertSee('abc123');
// Every request made against the fake is recorded.
$this->assertSame(1, $unsplash->recordedCount());
$this->assertSame('/photos/random', $unsplash->recordedRequest()->getUri()->getPath());
}
Plain arrays become 200 responses. Use FakeResponse when you need to control
the status code, the headers or the pagination metadata:
use MahdiMajidzadeh\LaravelUnsplash\Testing\FakeResponse;
Unsplash::fake([
FakeResponse::make(['id' => 'abc123'], 200, ['X-Ratelimit-Remaining' => '12']),
FakeResponse::error("Couldn't find Photo", 404),
FakeResponse::paginated([['id' => 'a'], ['id' => 'b']], 40, 10),
]);
Responses are returned in order, and Unsplash::fake() returns the fake so you
can inspect it. Requests are recorded as PSR-7 request objects, readable with
recorded(), recordedRequest($index) and recordedCount().
Deprecated endpoints
Photo::curated(), Collection::curated() and Collection::featured() are
kept for backwards compatibility, but Unsplash retired those endpoints — use
the topic endpoints instead. The Unsplush base class is likewise kept as an
alias of Endpoint.
Contributing
Run the test suite with:
$ composer test
Code style is enforced with Pint. It needs PHP 8.2+, so it is not a development dependency of this package — install it globally and run it from the package root:
$ composer global require laravel/pint
$ pint --test
Changelog
See CHANGELOG.md.
License
The MIT License (MIT). See LICENSE for details.