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 Google Photos Laravel Package

revolution/laravel-google-photos

Laravel wrapper for the Google Photos Library API. Browse, search, upload, and manage photos and albums from your Laravel app using a simple, expressive client. Includes a service provider, publishable config, OAuth credential handling, and a familiar facade for quick setup in any project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require revolution/laravel-google-photos
php artisan vendor:publish --tag=google-photos-config

The published config/google-photos.php is the first file to open — it holds client_id, client_secret, and refresh_token. Everything else in the package is a thin wrapper over the Google Photos Library API v1, so the config plus src/ (and src/Facades/) is the whole surface area.

Get credentials before writing code

  1. Google Cloud Console → enable Photos Library API.
  2. OAuth client (type: Web) → client ID/secret.
  3. Obtain a refresh token once, out-of-band. The fastest path is a throwaway route or an artisan command using google/apiclient, or the OAuth 2.0 Playground with scope https://www.googleapis.com/auth/photoslibrary.appendonly (add photoslibrary.readonly only if your app has been approved for restricted scopes — see Gotchas).
GOOGLE_PHOTOS_CLIENT_ID=...
GOOGLE_PHOTOS_CLIENT_SECRET=...
GOOGLE_PHOTOS_REFRESH_TOKEN=...

First use case: append a directory of images to a known album

This is what the package is actually good at. Not browsing a user's library — writing to an album your app owns.

use Revolution\Google\Photos\Facades\Photos; // confirm exact alias in src/Facades

$album = Photos::albums()->create(['title' => 'Product shots 2024']);

// store $album->id in your DB — never re-create, never hardcode

Then confirm the facade resolves and the token refreshes with php artisan tinker:

Photos::albums()->listAlbums(['pageSize' => 10]);

If that returns albums, the client, token, and scopes are all correct and you can build from there.

Where to look first

  • config/google-photos.php — token strategy.
  • src/Facades/*.php — the real method names; the README lags the code in archived packages.
  • The Google Photos Library API reference — the package passes arrays straight through, so the upstream request shape is your documentation.

Implementation Patterns

Treat the facade as a raw API client, not a repository

The package does almost no abstraction. Design your own thin service around it so the rest of the app never imports Revolution\...:

final class PhotoAlbum
{
    public function __construct(private string $albumId) {}

    public function upload(string $path): string
    {
        $token = Photos::uploads()->bytes(
            file_get_contents($path),
            ['fileName' => basename($path)]
        )->uploadToken;

        $item = Photos::mediaItems()->batchCreate([
            'albumId' => $this->albumId,
            'newMediaItems' => [[
                'simpleMediaItem' => ['uploadToken' => $token, 'fileName' => basename($path)],
            ]],
        ]);

        return $item->newMediaItemResults[0]->mediaItem->id;
    }
}

Two-step upload (bytes → uploadToken → batchCreate) is the core primitive. Everything else is bookkeeping.

Store the IDs, not the URLs

Persist album_id, media_item_id, and optionally productUrl. Never persist baseUrl as a permanent link — it expires and needs a sizing suffix (=w2048-h1024) appended at render time. If you need durable images, download to S3 and serve your own copy.

Model the token per-tenant when it isn't a single account

The env-based refresh token is fine for a single-account marketing site. For multi-user, add google_photos_refresh_token (encrypted cast) to users and call the token setter before each operation:

protected $casts = ['google_photos_refresh_token' => 'encrypted'];

Photos::setToken($user->google_photos_refresh_token);

Wrap that in a job or a resolvePhotosFor($user) helper — mixing global config and per-user tokens in one request is the #1 source of "wrong user's album" bugs.

Push every write into a queued job

final class SyncAlbumPhotos implements ShouldQueue
{
    use InteractsWithQueue, Queueable;
    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function handle(): void { /* upload + batchCreate */ }

    public function middleware(): array
    {
        return [new RateLimited('google-photos')];
    }
}

batchCreate accepts up to 50 items per call. Chunk with collect($paths)->chunk(50) rather than looping one item per request — it's the difference between 10 requests and 500.

Filter and paginate on the read side

$result = Photos::mediaItems()->search([
    'albumId' => $albumId,
    'pageSize' => 100,
    'filters' => [
        'mediaTypeFilter' => ['mediaTypes' => ['PHOTO']],
        'dateFilter' => ['ranges' => [[
            'startDate' => ['year' => 2024, 'month' => 1, 'day' => 1],
        ]]],
    ],
]);

pageSize maxes at 100 for search and 50 for albums, and there's almost always a nextPageToken. Write one paginator helper and reuse it, or you'll silently process only the first page — a very common bug that looks like "the sync worked, but only for 50 photos."

Testing

The package sits on top of an HTTP client, so Http::fake() usually works if it uses Laravel's client; otherwise stub the facade:

Photos::shouldReceive('mediaItems->batchCreate')->once()->andReturn($fake);

Better: inject your own PhotoAlbum service and fake that. Keeps the test independent of which package version you're pinned to.

Extension points

  • Rebind the client in a service provider ($this->app->singleton(...)) to swap in a wrapped version with logging or retry.
  • The facade is a standard Laravel facade, so Photos::swap($fake) works for integration tests.
  • Most request arrays pass through untouched, so anything the package "doesn't support" is usually just a key you can add to the payload.

Gotchas and Tips

The package is archived — and so is the API surface it targets.

The biggest risk isn't the code, it's the platform. Google restricted the broader photoslibrary.readonly scope on 31 March 2025; third-party apps that read a user's whole library need approval, and new integrations are steered to the Photos Picker API instead. Apps that only upload to their own albums are in a much better place. Before you build anything read-heavy, validate against current Google policy — no package upgrade will fix a scope you can't get.

Pin the version and check Laravel compatibility.

Zero dependents plus archived status means nobody is filing the Laravel 11/12 issue for you. Inspect composer.json for illuminate/support constraints; if it caps below your framework version, either fork with a broadened constraint or vendor the class files into app/. Don't expect a patch release.

Token expiry masquerading as an auth bug.

If the OAuth consent screen is in "Testing" mode, refresh tokens expire after 7 days. Every week your sync dies and you blame the package. Publish the consent screen (or use a Workspace-internal app) or you'll be re-authing on a cron.

Upload tokens are single-use and short-lived.

A batchCreate fails → your retry logic re-sends the same uploadToken → it fails again with a confusing 400. Retry the upload, not just the create. Treat uploadToken as a value that never leaves the function that created it.

Large files need resumable upload.

Simple byte uploads cap out around 50 MB per request. Videos and RAW files need the resumable endpoint, which this wrapper likely doesn't implement — drop to google/apiclient or raw Guzzle for those and keep the package for stills.

429s are normal, not exceptional.

Quotas are per-project and low (a few thousand requests/day, tens per minute, historically). RateLimited middleware plus backoff is not optional for a bulk import. Log the response body on failure — Google puts the actual reason (RESOURCE_EXHAUSTED, INVALID_ARGUMENT) in error.message, not in the exception message.

Config caching.

php artisan config:cache freezes env() at the config layer. Changing GOOGLE_PHOTOS_REFRESH_TOKEN in .env and seeing no effect means you forgot config:clear. Standard Laravel trap, hits this package constantly because credentials are the whole config file.

Don't log the payloads.

Upload bodies and uploadToken values are effectively credentials in flight. Scrub them in your HTTP client wrapper before anything reaches the log channel.

Album ownership matters more than you think.

Media items your app created are reliably the ones you can manage later; items a user added manually are a different story. Design around "this app owns this album" rather than "this app manages the user's library" — it's both a permissions and a UX boundary, and it's the assumption the surviving API behaviour rewards.

When to walk away.

For a one-off internal sync of a few hundred images to a fixed album, this package saves an afternoon and is fine. For anything user-facing, multi-tenant, or long-lived, spend the extra day on google/apiclient or direct Guzzle against the current API — you'll need the flexibility anyway, and an archived wrapper is one Laravel upgrade away from a rewrite.

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.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support