├── src
├── Facades
│ ├── Picker.php
│ └── Photos.php
├── Providers
│ └── PhotosServiceProvider.php
├── Traits
│ └── PhotosLibrary.php
├── Support
│ └── Token.php
├── Contracts
│ └── Factory.php
├── PickerClient.php
└── PhotosClient.php
├── docs
├── macro.md
├── upload.md
├── oauth.md
├── trait.md
└── picker.md
├── UPGRADING.md
├── LICENSE
├── composer.json
└── README.md
/src/Facades/Picker.php:
--------------------------------------------------------------------------------
1 | app->scoped(Factory::class, PhotosClient::class);
18 | }
19 |
20 | /**
21 | * Get the services provided by the provider.
22 | */
23 | public function provides(): array
24 | {
25 | return [Factory::class];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Traits/PhotosLibrary.php:
--------------------------------------------------------------------------------
1 | tokenForPhotoLibrary();
20 |
21 | return Container::getInstance()->make(Factory::class)->withToken($token);
22 | }
23 |
24 | /**
25 | * @return string|array{client_id: string, client_secret: string, refresh_token: string}
26 | */
27 | abstract protected function tokenForPhotoLibrary(): array|string;
28 | }
29 |
--------------------------------------------------------------------------------
/docs/macro.md:
--------------------------------------------------------------------------------
1 | # Macroable
2 |
3 | Extend any method by your self.
4 |
5 | The methods in PhotosLibraryClient are automatically delegated without the need to use macro.
6 |
7 | ## Register in AppServiceProvider.php
8 |
9 | ```php
10 | use Revolution\Google\Photos\Facades\Photos;
11 | use Google\Photos\Types\MediaItem;
12 |
13 | public function boot()
14 | {
15 | Photos::macro('updateMediaItemDescription', function (string $mediaItemId, string $newDescription, array $optionalArgs = []): MediaItem {
16 | return $this->getService()->updateMediaItemDescription($mediaItemId, $newDescription, $optionalArgs);
17 | });
18 | }
19 | ```
20 |
21 | ## Use somewhere
22 | ```php
23 | use Revolution\Google\Photos\Facades\Photos;
24 |
25 | $item = Photos::updateMediaItemDescription($mediaItemId, $newDescription);
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/upload.md:
--------------------------------------------------------------------------------
1 | # Upload
2 |
3 | - https://developers.google.com/photos/library/guides/upload-media
4 | - https://developers.google.com/photos/library/guides/api-limits-quotas#photo-storage-quality
5 |
6 | ## two-step process
7 | 1. Upload file.
8 | 2. Add to mediaItems by batchCreate.
9 |
10 | ```php
11 | use Illuminate\Http\Request;
12 |
13 | public function __invoke(Request $request)
14 | {
15 | if (!$request->hasFile('file')) {
16 | return redirect('home');
17 | }
18 |
19 | $photos = $request->user()->photos();
20 |
21 | $file = $request->file('file');
22 |
23 | $uploadToken = $photos->upload(
24 | $file->getContent(),
25 | $file->getClientOriginalName(),
26 | );
27 |
28 | $result = $photos->batchCreate([$uploadToken]);
29 |
30 | return redirect('home')->with('status', 'Upload success');
31 | }
32 | ```
33 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | # UPGRADING
2 |
3 | ## from 5.x to 6.0
4 | Contains many breaking changes due to major changes to the Photos API.
5 | https://developers.googleblog.com/en/google-photos-picker-api-launch-and-library-api-updates/
6 |
7 | - Dependent package changed to `google/photos-library`.
8 | - PhotosClient: Renamed from `setAccessToken()` to `withToken()`. Pass `refresh_token` to withToken.
9 | - PhotosLibrary trait: Renamed from `photosAccessToken()` to `tokenForPhotoLibrary()`.
10 | - Many method arguments and return values have been changed.
11 | - `config/google.php` is still used.
12 | - share: Deleted.
13 | - Use the [Picker API](./docs/picker.md) to handle files other than those uploaded via the API
14 |
15 | > you can only access and manage content that was created by your application.
16 | > https://developers.google.com/photos/library/guides/get-started-library
17 |
--------------------------------------------------------------------------------
/src/Facades/Photos.php:
--------------------------------------------------------------------------------
1 | scopes(config('google.scopes'))
8 | ->with([
9 | 'access_type' => config('google.access_type'),
10 | 'prompt' => config('google.prompt'),
11 | ])
12 | ->redirect();
13 | }
14 |
15 | public function callback()
16 | {
17 | if (request()->missing('code')) {
18 | return redirect('/');
19 | }
20 |
21 | /**
22 | * @var \Laravel\Socialite\Two\User $user
23 | */
24 | $user = Socialite::driver('google')->user();
25 |
26 | /**
27 | * @var \App\User $loginUser
28 | */
29 | $loginUser = User::updateOrCreate(
30 | [
31 | 'email' => $user->email,
32 | ],
33 | [
34 | 'name' => $user->name,
35 | 'refresh_token' => $user->refreshToken,
36 | ]);
37 |
38 | auth()->login($loginUser, false);
39 |
40 | return redirect('/home');
41 | }
42 | ```
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 kawax
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "revolution/laravel-google-photos",
3 | "description": "Google Photos API for Laravel",
4 | "keywords": [
5 | "google",
6 | "photos",
7 | "laravel"
8 | ],
9 | "license": "MIT",
10 | "require": {
11 | "php": "^8.2",
12 | "illuminate/support": "^11.0||^12.0",
13 | "revolution/laravel-google-sheets": "^7.0",
14 | "google/photos-library": "^1.7"
15 | },
16 | "require-dev": {
17 | "orchestra/testbench": "^10.0",
18 | "laravel/pint": "^1.0"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Revolution\\Google\\Photos\\": "src/"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "Tests\\": "tests/"
28 | }
29 | },
30 | "authors": [
31 | {
32 | "name": "kawax",
33 | "email": "kawaxbiz@gmail.com"
34 | }
35 | ],
36 | "scripts": {
37 | "pre-autoload-dump": [
38 | "Google\\Task\\Composer::cleanup"
39 | ]
40 | },
41 | "extra": {
42 | "laravel": {
43 | "providers": [
44 | "Revolution\\Google\\Photos\\Providers\\PhotosServiceProvider"
45 | ]
46 | },
47 | "google/apiclient-services": ["Drive"]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/docs/trait.md:
--------------------------------------------------------------------------------
1 | # PhotosLibrary Trait
2 |
3 | Add `PhotosLibrary` trait to User model.
4 |
5 | ```php
6 | use Revolution\Google\Photos\Traits\PhotosLibrary;
7 |
8 | class User extends Authenticatable
9 | {
10 | use PhotosLibrary;
11 |
12 | protected function tokenForPhotoLibrary(): array|string
13 | {
14 | // Returns the refresh_token string
15 | return $this->refresh_token;
16 |
17 | // Or if you want to use a different id and secret, return an array containing the client id and secret
18 | return [
19 | 'client_id' => $this->client_id,
20 | 'client_secret' => $this->client_secret,
21 | 'refresh_token' => $this->refresh_token,
22 | ];
23 | }
24 | }
25 | ```
26 |
27 | Add `tokenForPhotoLibrary()`(abstract) for token.
28 |
29 | Trait has `photos()` that returns `Photos` instance.
30 |
31 | ```php
32 | public function __invoke(Request $request)
33 | {
34 | // Facade
35 | // $albums = Photos::withToken($request->user()->refresh_token)
36 | // ->listAlbums();
37 |
38 | // PhotosLibrary Trait
39 | $albums = $request->user()
40 | ->photos()
41 | ->listAlbums();
42 |
43 | return view('albums.index')->with(compact('albums'));
44 | }
45 | ```
46 |
47 | ## Already photos() exists
48 |
49 | ```php
50 | use PhotosLibrary {
51 | PhotosLibrary::photos as googlephotos;
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/src/Support/Token.php:
--------------------------------------------------------------------------------
1 | fetchAuthToken()['access_token'] ?? '';
26 | // @codeCoverageIgnoreEnd
27 | }
28 |
29 | /**
30 | * @param string|array{client_id: string, client_secret: string, refresh_token: string} $refresh_token
31 | */
32 | public static function refreshArray(string|array $refresh_token): array
33 | {
34 | if (is_string($refresh_token)) {
35 | $refresh_token = [
36 | 'client_id' => config('google.client_id'),
37 | 'client_secret' => config('google.client_secret'),
38 | 'refresh_token' => $refresh_token,
39 | ];
40 | }
41 |
42 | return $refresh_token;
43 | }
44 |
45 | /**
46 | * Set fake token for testing.
47 | */
48 | public static function fake(?string $token = 'token'): void
49 | {
50 | static::$fake_token = $token;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Contracts/Factory.php:
--------------------------------------------------------------------------------
1 | $uploadTokens
45 | * @param array{albumId?: string, albumPosition?: AlbumPosition, retrySettings?: RetrySettings|array} $optionalArgs
46 | *
47 | * @throws ApiException
48 | */
49 | public function batchCreate(array $uploadTokens, array $optionalArgs = []): BatchCreateMediaItemsResponse;
50 | }
51 |
--------------------------------------------------------------------------------
/src/PickerClient.php:
--------------------------------------------------------------------------------
1 | token = $access_token;
21 |
22 | return $this;
23 | }
24 |
25 | /**
26 | * sessions.create.
27 | *
28 | * @return array{id: string, pickerUri: string, pollingConfig: array{pollInterval: string, timeoutIn: string}, mediaItemsSet: bool}
29 | */
30 | public function create(): array
31 | {
32 | return Http::withToken($this->token)
33 | ->post($this->endpoint.'/sessions', [
34 | 'id' => '',
35 | ])->json();
36 | }
37 |
38 | /**
39 | * sessions.get.
40 | *
41 | * @param string $id SessionID
42 | * @return array{id: string, pickerUri: string, pollingConfig: array{pollInterval: string, timeoutIn: string}, mediaItemsSet: bool}
43 | */
44 | public function get(string $id): array
45 | {
46 | return Http::withToken($this->token)
47 | ->get($this->endpoint.'/sessions/'.$id)
48 | ->json();
49 | }
50 |
51 | /**
52 | * sessions.delete.
53 | *
54 | * @param string $id SessionID
55 | */
56 | public function delete(string $id): mixed
57 | {
58 | return Http::withToken($this->token)
59 | ->delete($this->endpoint.'/sessions/'.$id)
60 | ->json();
61 | }
62 |
63 | /**
64 | * mediaItems.list.
65 | *
66 | * @param string $id SessionID
67 | * @return array{mediaItems: array, nextPageToken?: string}
68 | */
69 | public function list(string $id, ?int $pageSize = null, ?string $pageToken = null): array
70 | {
71 | return Http::withToken($this->token)
72 | ->get($this->endpoint.'/mediaItems', [
73 | 'sessionId' => $id,
74 | 'pageSize' => $pageSize,
75 | 'pageToken' => $pageToken,
76 | ])
77 | ->json();
78 | }
79 |
80 | public function endpoint(string $endpoint): static
81 | {
82 | $this->endpoint = $endpoint;
83 |
84 | return $this;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/docs/picker.md:
--------------------------------------------------------------------------------
1 | # Picker API
2 |
3 | To handle files other than those uploaded via the API, you must use the Picker API.
4 |
5 | - https://developers.google.com/photos/picker/guides/get-started-picker
6 |
7 | ## Picker API requires an Access Token
8 |
9 | Get an access token from the refresh token
10 |
11 | ```php
12 | use Revolution\Google\Photos\Support\Token;
13 |
14 | $token = Token::toAccessToken('refresh_token');
15 |
16 | session(['picker_token' => $token]);
17 | ```
18 |
19 | ### Mock `Token::toAccessToken()` in testing
20 |
21 | You can use `Token::fake()` to return a fixed token from `Token::toAccessToken()`.
22 |
23 | ```php
24 | use Revolution\Google\Photos\Support\Token;
25 |
26 | Token::fake(token: 'test');
27 |
28 | $token = Token::toAccessToken('refresh_token');
29 | // test
30 | ```
31 |
32 | Reset mock
33 |
34 | ```php
35 | use Revolution\Google\Photos\Support\Token;
36 |
37 | Token::fake(token: null);
38 | ```
39 |
40 | ## Session create
41 |
42 | ```php
43 | use Revolution\Google\Photos\Facades\Picker;
44 |
45 | $picker = Picker::withToken(session('picker_token'))->create();
46 |
47 | session(['picker' => $picker]);
48 |
49 | dump($picker);
50 | // PickingSession
51 | //[
52 | // 'id' => '...',
53 | // 'pickerUri' => 'https://',
54 | // 'pollingConfig' => [],
55 | // 'mediaItemsSet' => false,
56 | //]
57 | ```
58 |
59 | `pickerUri` opens a new browser and the user can select photos.
60 |
61 | ```html
62 |
63 | ```
64 |
65 | There is no way to return to the original URL from Google Photos.
66 |
67 | ## Session get
68 | Since it does not return to the original URL, you have no choice but to check it on the Laravel side.
69 |
70 | ```php
71 | use Revolution\Google\Photos\Facades\Picker;
72 |
73 | $picker = Picker::withToken(session('picker_token'))->get(session('picker.id'));
74 |
75 | session(['picker' => $picker]);
76 |
77 | dump($picker);
78 | // PickingSession
79 | //[
80 | // 'id' => '...',
81 | // 'pickerUri' => 'https://',
82 | // 'pollingConfig' => [],
83 | // 'mediaItemsSet' => true,
84 | //]
85 | ```
86 |
87 | If `mediaItemsSet` is true, the user has finished selecting photos.
88 |
89 | ## MediaItems list
90 |
91 | Finally, you can get a list of photos selected by the user.
92 |
93 | ```php
94 | use Revolution\Google\Photos\Facades\Picker;
95 |
96 | $list = Picker::withToken(session('picker_token'))->list(session('picker.id'));
97 |
98 | dump($list);
99 |
100 | //[
101 | // 'mediaItems' => [],
102 | // 'nextPageToken' => '...',
103 | //]
104 | ```
105 |
--------------------------------------------------------------------------------
/src/PhotosClient.php:
--------------------------------------------------------------------------------
1 | service = $service;
35 |
36 | return $this;
37 | }
38 |
39 | public function getService(): PhotosLibraryClient
40 | {
41 | if (is_null($this->service)) {
42 | throw new RuntimeException('Missing token. Set the token using withToken().');
43 | }
44 |
45 | return $this->service;
46 | }
47 |
48 | /**
49 | * Set token.
50 | *
51 | * @param string|array{client_id: string, client_secret: string, refresh_token: string} $token
52 | *
53 | * @throws ValidationException
54 | */
55 | public function withToken(string|array $token): static
56 | {
57 | $token = Token::refreshArray($token);
58 |
59 | $credentials = new UserRefreshCredentials(config('google.scopes'), $token);
60 |
61 | $client = new PhotosLibraryClient(['credentials' => $credentials]);
62 |
63 | return $this->setService($client);
64 | }
65 |
66 | /**
67 | * mediaItems.search.
68 | *
69 | * @param array{albumId?: string, pageSize?: int, pageToken?: string, filters?: Filters, orderBy?: string, retrySettings?: RetrySettings|array} $optionalArgs
70 | *
71 | * @throws ApiException
72 | */
73 | public function search(array $optionalArgs = []): PagedListResponse
74 | {
75 | return $this->getService()->searchMediaItems($optionalArgs);
76 | }
77 |
78 | /**
79 | * mediaItems.batchCreate.
80 | *
81 | * @param array $uploadTokens
82 | * @param array{albumId?: string, albumPosition?: AlbumPosition, retrySettings?: RetrySettings|array} $optionalArgs
83 | *
84 | * @throws ApiException
85 | */
86 | public function batchCreate(array $uploadTokens, array $optionalArgs = []): BatchCreateMediaItemsResponse
87 | {
88 | $newMediaItems = [];
89 |
90 | foreach ($uploadTokens as $token) {
91 | $newMediaItems[] = PhotosLibraryResourceFactory::newMediaItem($token);
92 | }
93 |
94 | return $this->getService()->batchCreateMediaItems($newMediaItems, $optionalArgs);
95 | }
96 |
97 | /**
98 | * @param string $method
99 | * @param array $parameters
100 | * @return mixed
101 | */
102 | public function __call($method, $parameters)
103 | {
104 | if (static::hasMacro($method)) {
105 | return $this->macroCall($method, $parameters);
106 | }
107 |
108 | return $this->forwardCallTo($this->getService(), $method, $parameters);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google Photos API for Laravel
2 |
3 | [](https://qlty.sh/gh/invokable/projects/laravel-google-photos)
4 | [](https://qlty.sh/gh/invokable/projects/laravel-google-photos)
5 | [](https://deepwiki.com/invokable/laravel-google-photos)
6 |
7 | A Laravel package providing seamless integration with the Google Photos Library API and Google Photos Picker API. Upload photos, manage albums, and interact with Google Photos directly from your Laravel applications.
8 |
9 | ## Overview
10 |
11 | This package enables Laravel applications to:
12 |
13 | - **Upload photos** to Google Photos with automatic media item creation
14 | - **Manage albums** - create, list, and update album information
15 | - **List media items** and search through uploaded content
16 | - **Use Google Photos Picker** to let users select photos from their Google Photos library
17 | - **OAuth 2.0 authentication** with proper token management and refresh handling
18 |
19 | **Important**: Due to Google Photos API limitations, you can only access and manage content that was uploaded via your application. For accessing existing user photos, use the Google Photos Picker API.
20 |
21 | ## Quick Start
22 |
23 | ### 1. Install the Package
24 |
25 | ```bash
26 | composer require revolution/laravel-google-photos
27 |
28 | php artisan vendor:publish --tag="google-config"
29 | ```
30 |
31 | ### 2. Get Google API Credentials
32 |
33 | 1. Visit the [Google Cloud Console](https://developers.google.com/console)
34 | 2. Enable **Photos Library API** and **Google Photos Picker API**
35 | ⚠️ Be careful not to select "Google Picker API" (different from Photos Picker API)
36 | 3. Create OAuth 2.0 client credentials
37 | 4. Set authorized redirect URIs for your application
38 |
39 | ### 3. Configure Laravel
40 |
41 | Add to your `config/services.php`:
42 |
43 | ```php
44 | 'google' => [
45 | 'client_id' => env('GOOGLE_CLIENT_ID', ''),
46 | 'client_secret' => env('GOOGLE_CLIENT_SECRET', ''),
47 | 'redirect' => env('GOOGLE_REDIRECT', ''),
48 | ],
49 | ```
50 |
51 | Edit `config/google.php`:
52 |
53 | ```php
54 | env('GOOGLE_CLIENT_ID', ''),
58 | 'client_secret' => env('GOOGLE_CLIENT_SECRET', ''),
59 | 'redirect_uri' => env('GOOGLE_REDIRECT', ''),
60 | 'scopes' => [
61 | 'https://www.googleapis.com/auth/photoslibrary.appendonly',
62 | 'https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata',
63 | 'https://www.googleapis.com/auth/photoslibrary.edit.appcreateddata',
64 | 'https://www.googleapis.com/auth/photospicker.mediaitems.readonly',
65 | ],
66 | 'access_type' => 'offline',
67 | 'prompt' => 'consent select_account',
68 | ];
69 | ```
70 |
71 | Add to your `.env`:
72 |
73 | ```env
74 | GOOGLE_CLIENT_ID=your_client_id_here
75 | GOOGLE_CLIENT_SECRET=your_client_secret_here
76 | GOOGLE_REDIRECT=https://yourapp.com/auth/google/callback
77 | ```
78 |
79 | ### 4. Quick Example
80 |
81 | ```php
82 | use Revolution\Google\Photos\Facades\Photos;
83 |
84 | // Upload a photo (two-step process)
85 | $uploadToken = Photos::withToken($user->refresh_token)
86 | ->upload($fileContent, $fileName);
87 |
88 | $result = Photos::batchCreate([$uploadToken]);
89 | ```
90 |
91 | ## Requirements
92 |
93 | - PHP >= 8.2
94 | - Laravel >= 11.0
95 |
96 | ## Installation
97 |
98 | ```bash
99 | composer require revolution/laravel-google-photos
100 |
101 | php artisan vendor:publish --tag="google-config"
102 | ```
103 |
104 | ## Authentication
105 |
106 | ### OAuth 2.0 Only
107 |
108 | **This package ONLY supports OAuth 2.0 authentication.**
109 |
110 | ❌ **Service Account authentication is NOT supported**
111 | ❌ **API Key authentication is NOT supported**
112 |
113 | **Why OAuth 2.0 Only?**
114 | The Google Photos API is designed for user-centric applications and requires user consent to access personal photo libraries. Google Photos API does not support Service Account or API Key authentication methods because:
115 |
116 | 1. **Privacy by Design**: Photos are personal data requiring explicit user consent
117 | 2. **Google API Limitation**: The Photos Library API only accepts OAuth 2.0 tokens
118 | 3. **User Context Required**: All operations need to be performed on behalf of a specific user
119 |
120 | ### OAuth Setup Guide
121 |
122 | #### Step 1: Google Cloud Console Setup
123 |
124 | 1. Go to [Google Cloud Console](https://developers.google.com/console)
125 | 2. Create a new project or select existing one
126 | 3. Enable these APIs:
127 | - **Photos Library API** (for uploading and managing photos)
128 | - **Google Photos Picker API** (for photo selection interface)
129 | 4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client ID"
130 | 5. Choose "Web application"
131 | 6. Add your authorized redirect URIs (e.g., `https://yourapp.com/auth/google/callback`)
132 |
133 | #### Step 2: Laravel Configuration
134 |
135 | The `'access_type' => 'offline'` setting is required to obtain refresh tokens for long-term access.
136 |
137 | #### Step 3: OAuth Implementation with Laravel Socialite
138 |
139 | ```php
140 | // routes/web.php
141 | Route::get('/auth/google', [AuthController::class, 'redirect']);
142 | Route::get('/auth/google/callback', [AuthController::class, 'callback']);
143 |
144 | // AuthController.php
145 | use Laravel\Socialite\Facades\Socialite;
146 |
147 | public function redirect()
148 | {
149 | return Socialite::driver('google')
150 | ->scopes(config('google.scopes'))
151 | ->with([
152 | 'access_type' => config('google.access_type'),
153 | 'prompt' => config('google.prompt'),
154 | ])
155 | ->redirect();
156 | }
157 |
158 | public function callback()
159 | {
160 | $user = Socialite::driver('google')->user();
161 |
162 | $loginUser = User::updateOrCreate(
163 | ['email' => $user->email],
164 | [
165 | 'name' => $user->name,
166 | 'refresh_token' => $user->refreshToken, // Store this!
167 | ]
168 | );
169 |
170 | auth()->login($loginUser);
171 | return redirect('/home');
172 | }
173 | ```
174 |
175 | ## Usage Examples
176 |
177 | ### Upload Photos
178 |
179 | Photos must be uploaded using a two-step process:
180 |
181 | ```php
182 | use Revolution\Google\Photos\Facades\Photos;
183 | use Illuminate\Http\Request;
184 |
185 | public function uploadPhoto(Request $request)
186 | {
187 | $file = $request->file('photo');
188 |
189 | // Step 1: Upload file content and get upload token
190 | $uploadToken = Photos::withToken($request->user()->refresh_token)
191 | ->upload($file->getContent(), $file->getClientOriginalName());
192 |
193 | // Step 2: Create media item from upload token
194 | $result = Photos::batchCreate([$uploadToken]);
195 |
196 | return response()->json($result);
197 | }
198 | ```
199 |
200 | ### List Media Items
201 |
202 | ```php
203 | use Revolution\Google\Photos\Facades\Photos;
204 |
205 | public function listPhotos()
206 | {
207 | $mediaItems = Photos::withToken(auth()->user()->refresh_token)
208 | ->listMediaItems();
209 |
210 | foreach ($mediaItems as $item) {
211 | echo $item->getBaseUrl() . "\n";
212 | echo $item->getFilename() . "\n";
213 | }
214 | }
215 | ```
216 |
217 | ### Create Album
218 |
219 | ```php
220 | use Revolution\Google\Photos\Facades\Photos;
221 | use Google\Photos\Library\V1\PhotosLibraryResourceFactory;
222 |
223 | public function createAlbum()
224 | {
225 | $newAlbum = Photos::withToken(auth()->user()->refresh_token)
226 | ->createAlbum(PhotosLibraryResourceFactory::album('My New Album'));
227 |
228 | return [
229 | 'id' => $newAlbum->getId(),
230 | 'title' => $newAlbum->getTitle(),
231 | 'url' => $newAlbum->getProductUrl(),
232 | ];
233 | }
234 | ```
235 |
236 | ### List Albums
237 |
238 | ```php
239 | use Revolution\Google\Photos\Facades\Photos;
240 |
241 | public function listAlbums()
242 | {
243 | $albums = Photos::withToken(auth()->user()->refresh_token)
244 | ->listAlbums();
245 |
246 | foreach ($albums as $album) {
247 | echo "Album: " . $album->getTitle() . "\n";
248 | echo "ID: " . $album->getId() . "\n";
249 | echo "Items: " . $album->getMediaItemsCount() . "\n";
250 | }
251 | }
252 | ```
253 |
254 | ### Update Album Title
255 |
256 | ```php
257 | use Revolution\Google\Photos\Facades\Photos;
258 |
259 | public function updateAlbumTitle($albumId, $newTitle)
260 | {
261 | $album = Photos::withToken(auth()->user()->refresh_token)
262 | ->updateAlbumTitle($albumId, $newTitle);
263 |
264 | return $album;
265 | }
266 | ```
267 |
268 | ### Using with User Model Trait
269 |
270 | Add the `PhotosLibrary` trait to your User model for cleaner syntax:
271 |
272 | ```php
273 | use Revolution\Google\Photos\Traits\PhotosLibrary;
274 |
275 | class User extends Authenticatable
276 | {
277 | use PhotosLibrary;
278 |
279 | protected function tokenForPhotoLibrary(): array|string
280 | {
281 | return $this->refresh_token;
282 | }
283 | }
284 |
285 | // Usage with trait
286 | $albums = $user->photos()->listAlbums();
287 | $uploadToken = $user->photos()->upload($content, $filename);
288 | ```
289 |
290 | ### Google Photos Picker API
291 |
292 | Use the Picker API to let users select existing photos from their Google Photos library:
293 |
294 | ```php
295 | use Revolution\Google\Photos\Facades\Picker;
296 | use Revolution\Google\Photos\Support\Token;
297 |
298 | // Get access token from refresh token
299 | $accessToken = Token::toAccessToken(auth()->user()->refresh_token);
300 |
301 | // Create picker session
302 | $picker = Picker::withToken($accessToken)->create();
303 |
304 | // Redirect user to picker
305 | return redirect($picker['pickerUri']);
306 |
307 | // Later, check if user finished selecting
308 | $session = Picker::withToken($accessToken)->get($picker['id']);
309 | if ($session['mediaItemsSet']) {
310 | // Get selected media items
311 | $mediaItems = Picker::withToken($accessToken)->list($picker['id']);
312 | }
313 | ```
314 |
315 | ## Advanced Usage
316 |
317 | ### PhotosLibraryClient Methods
318 |
319 | This package delegates to the Google Photos Library PHP client and supports all its methods:
320 |
321 | ```php
322 | use Revolution\Google\Photos\Facades\Photos;
323 |
324 | // Get specific album
325 | $album = Photos::withToken($token)->getAlbum($albumId);
326 |
327 | // Search media items
328 | $searchResults = Photos::withToken($token)->searchMediaItems($searchRequest);
329 |
330 | // Add media items to album
331 | $photos = Photos::withToken($token)->batchAddMediaItemsToAlbum($albumId, $mediaItemIds);
332 | ```
333 |
334 | ### PagedListResponse
335 |
336 | Methods like `listMediaItems()` and `listAlbums()` return a `PagedListResponse` for handling large result sets:
337 |
338 | ```php
339 | use Revolution\Google\Photos\Facades\Photos;
340 | use Google\ApiCore\PagedListResponse;
341 |
342 | $items = Photos::withToken($token)->listMediaItems();
343 |
344 | // Iterate through all items (handles pagination automatically)
345 | foreach ($items as $item) {
346 | echo $item->getBaseUrl() . "\n";
347 | }
348 |
349 | // Or access page information
350 | $page = $items->getPage();
351 | echo "Page token: " . $page->getNextPageToken();
352 | ```
353 |
354 | ### Error Handling
355 |
356 | ```php
357 | use Revolution\Google\Photos\Facades\Photos;
358 | use Google\ApiCore\ApiException;
359 |
360 | try {
361 | $result = Photos::withToken($token)->upload($content, $filename);
362 | } catch (ApiException $e) {
363 | Log::error('Google Photos API error: ' . $e->getMessage());
364 | return response()->json(['error' => 'Upload failed'], 500);
365 | }
366 | ```
367 |
368 | ### Extending with Macros
369 |
370 | You can extend the Photos facade with custom methods:
371 |
372 | ```php
373 | // In AppServiceProvider::boot()
374 | use Revolution\Google\Photos\Facades\Photos;
375 |
376 | Photos::macro('customUpload', function ($file) {
377 | $uploadToken = $this->upload($file->getContent(), $file->getClientOriginalName());
378 | return $this->batchCreate([$uploadToken]);
379 | });
380 |
381 | // Usage
382 | $result = Photos::withToken($token)->customUpload($file);
383 | ```
384 |
385 | ## FAQ
386 |
387 | ### Can I use a Service Account?
388 |
389 | **No.** Service Account authentication is not supported by the Google Photos API. The API requires OAuth 2.0 user consent because it deals with personal photo data.
390 |
391 | ### Can I use an API Key?
392 |
393 | **No.** API Key authentication is not supported by the Google Photos API. Only OAuth 2.0 authentication is available.
394 |
395 | ### Why only OAuth 2.0?
396 |
397 | Google Photos API is designed exclusively for user-centric applications. Since photos are personal data, Google requires explicit user consent through OAuth 2.0. This ensures users maintain control over their photo data and can revoke access at any time.
398 |
399 | ### Can I access existing photos in a user's Google Photos library?
400 |
401 | **No, not directly.** The Google Photos Library API only allows access to photos uploaded via your application. To work with existing user photos, you must use the **Google Photos Picker API** included in this package.
402 |
403 | ### Do I need review for production use?
404 |
405 | If you're only uploading to your own account during development, no review is needed. However, for production use with other users' accounts, Google requires app review for sensitive scopes.
406 |
407 | ### What are the API limits?
408 |
409 | Google Photos API has rate limits and quotas. See the [official documentation](https://developers.google.com/photos/library/guides/api-limits-quotas) for current limits.
410 |
411 | ## Related Packages
412 |
413 | - **[laravel-google-sheets](https://github.com/invokable/laravel-google-sheets)** - Google Sheets API integration
414 | - **[laravel-google-searchconsole](https://github.com/invokable/laravel-google-searchconsole)** - Google Search Console API integration
415 |
416 | ## Resources
417 |
418 | - [Google Photos Library API Documentation](https://developers.google.com/photos/library/guides/get-started-library)
419 | - [Google Photos Picker API Documentation](https://developers.google.com/photos/picker/guides/get-started-picker)
420 | - [Laravel Socialite Documentation](https://laravel.com/docs/socialite)
421 | - [Package Documentation](./docs/)
422 |
423 | ## License
424 |
425 | MIT
426 |
427 | ---
428 |
429 | **⚠️ Important Note**: This package can only access photos uploaded via your application. For accessing existing user photos, use the Google Photos Picker API functionality included in this package.
430 |
--------------------------------------------------------------------------------