mahdimajidzadeh/laravel-unsplash

Laravel package for the Unsplash API

Maintainers

Package info

github.com/MahdiMajidzadeh/Laravel-Unsplash

Issues

pkg:composer/mahdimajidzadeh/laravel-unsplash

Transparency log

Statistics

Installs: 11 363

Dependents: 0

Suggesters: 0

Stars: 11

v0.1.5 2022-03-19 07:16 UTC

README

tests packagist license

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:

  • null values are dropped, so you can pass optional parameters unconditionally.
  • Booleans are sent as the true/false strings Unsplash expects.
  • Lists are sent as the comma separated values the API documents for multi value parameters, so ['collections' => [123, 456]] becomes collections=123,456. An empty list is left out entirely.
  • Nested parameters such as location and exif on 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.