├── 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 | [![Maintainability](https://qlty.sh/badges/70d6e01e-6c2d-40ba-985b-769820516ea7/maintainability.svg)](https://qlty.sh/gh/invokable/projects/laravel-google-photos) 4 | [![Code Coverage](https://qlty.sh/badges/70d6e01e-6c2d-40ba-985b-769820516ea7/test_coverage.svg)](https://qlty.sh/gh/invokable/projects/laravel-google-photos) 5 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 | --------------------------------------------------------------------------------