google/photos-library
A PHP client for the Google Photos Library API that lets server-side apps authenticate, list and search albums and media items, upload photos and videos, and apply filters. Generated from Google’s API definitions for straightforward Composer installation.
The Composer package is google/photos-library (repo google/php-photoslibrary). It is archived — pin it explicitly and treat every version as the last one you'll get:
composer require google/photos-library:^1.0
First things to read, in order:
samples/ in the repo — the OAuth bootstrap, upload.php, list_albums.php, create_album.php are the fastest path to a working call.src/PhotosLibrary/V1/PhotosLibraryClient.php — the generated client; its method signatures are the real documentation (they drift from the public REST docs).src/PhotosLibrary/V1/PhotosLibraryResourceFactory.php — the only pleasant part of the API; use it instead of hand-building protos.Minimal first use case (list albums with a service-account/user credentials file):
use Google\Photos\Library\V1\PhotosLibraryClient;
$client = new PhotosLibraryClient([
'credentials' => '/path/to/credentials.json',
// 'transport' => 'rest', // handy when debugging with curl
]);
$response = $client->listAlbums(['pageSize' => 50]);
foreach ($response->getAlbums() ?: [] as $album) {
printf("%s (%s)\n", $album->getTitle(), $album->getId());
}
Note that photoslibrary.* scopes are user scopes — a plain service account will not work against a real Photos library. You need an OAuth2 refresh token (or an interactive consent flow) for a human account. Get that working before writing any feature code; it is the single biggest time sink in this package.
Wrap the client, don't inject it raw. The generated client is hard to mock and its constructor does credential resolution eagerly. Put it behind your own interface (PhotoLibrary::listAlbums(), PhotoLibrary::upload()), construct the PhotosLibraryClient lazily inside a factory, and register that factory in the container. This is also your escape hatch if you ever migrate off the archived lib.
Upload → factory → batchCreate is the only write path. There is no "add one photo" call. The flow is always:
use Google\Photos\Library\V1\PhotosLibraryResourceFactory;
$token = $client->uploadMediaItem($absolutePath); // bytes go up here
$item = PhotosLibraryResourceFactory::newMediaItem($description, $token);
$res = $client->batchCreateMediaItems([
'albumId' => $albumId, // omit to add to library root
'newMediaItems' => [$item],
]);
Wrap the last two calls in a single method so callers can't accidentally upload without consuming the token. For bulk imports, upload N files, collect N tokens, then send one batchCreateMediaItems with up to 50 items — that's the hard cap per request.
Pagination is manual everywhere. Every list/search method returns a response with getNextPageToken(); loop until it's empty:
$token = null;
do {
$page = $client->searchMediaItems([
'albumId' => $albumId,
'pageSize' => 100,
'pageToken'=> $token,
]);
foreach ($page->getMediaItems() ?: [] as $item) { /* ... */ }
$token = $page->getNextPageToken();
} while ($token);
Always coalesce ?: [] — empty result sets come back as null from the proto getters, and foreach (null) is a fatal error.
Search filters are proto objects, not arrays of strings. Date ranges need Google\Type\Date instances:
use Google\Photos\Library\V1\Filters;
use Google\Photos\Library\V1\DateFilter;
use Google\Type\Date;
$filters = new Filters();
$filters->setDateFilter(
(new DateFilter())->setRanges([
(new \Google\Photos\Library\V1\DateRange())
->setStartDate(new Date(['year' => 2024, 'month' => 1, 'day' => 1]))
->setEndDate(new Date(['year' => 2024, 'month' => 12, 'day' => 31])),
])
);
$client->searchMediaItems(['albumId' => $albumId, 'filters' => $filters]);
Date validates on construction — a bad month throws, so build these in a small value-object helper and unit-test it.
Album creation + population is the canonical workflow. PhotosLibraryResourceFactory::newAlbum($title) → $client->createAlbum($album) → use $album->getId() from the response for all subsequent batchCreateMediaItems calls. The returned album is a new instance; the one you passed in has no ID.
Integration tip — keep the credential path in config, the token in the DB. Store the refresh token per user, and build the client with:
new PhotosLibraryClient([
'credentials' => [
'client_id' => config('services.google.client_id'),
'client_secret' => config('services.google.client_secret'),
'refresh_token' => $user->google_refresh_token,
],
]);
The library accepts an array, a path, or a Google\Auth\FetchAuthTokenInterface — passing your own token fetcher is the cleanest way to handle per-user tokens and refresh-token rotation.
google/apiclient or raw HTTP if this is production-critical.searchMediaItems requires either an albumId or a filters object; and since the 2025 platform changes, apps generally only see media they created (photoslibrary.readonly.appcreateddata). Design around "albums our app owns," not "the user's photo library."getBaseUrl() gives you a short-lived URL (roughly an hour) that you must suffix yourself: $baseUrl . '=w2048-h1024' for a resized image, =d to download the original. Cache the download server-side; never store the base URL in a database.batchCreateMediaItems fails, re-upload — don't retry with the old token. This makes retry logic subtly expensive; log upload failures separately.joinSharedAlbum/leaveSharedAlbum and read, but batchCreateMediaItems with a shared album ID fails. Check album ownership before offering upload UI.Google\ApiCore\ApiException, not Guzzle exceptions. Use $e->getStatus() (RESOURCE_EXHAUSTED for quota, PERMISSION_DENIED, INVALID_ARGUMENT) and $e->getBasicMessage(). Catching \Exception broadly will hide the quota signal you actually need to handle.pageSize to 100 and cache results.credentials option is passed straight to Google\Auth; relative paths resolve from the process CWD, not the app root. GOOGLE_APPLICATION_CREDENTIALS env var works as a fallback, which can silently mask a bad config value in tests.'transport' => 'rest' in the constructor options — messages become readable JSON errors instead of gRPC-style ones, and you can enableDebugOutput() via the underlying Google\ApiCore options if you need the raw request.photoslibrary.sharing later means every user must re-consent. Decide the full scope set on day one.PhotosLibraryClient is generated and regenerating/updating will clobber edits; wrap it.How can I help you explore Laravel packages today?