├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── art └── example.png ├── composer.json ├── config └── gemini.php ├── phpstan.neon ├── phpunit.xml ├── resources └── views │ └── components │ ├── badge.php │ ├── new-line.php │ └── two-column-detail.php ├── src ├── Commands │ └── InstallCommand.php ├── Exceptions │ └── MissingApiKey.php ├── Facades │ └── Gemini.php ├── ServiceProvider.php ├── Support │ └── View.php └── Testing │ └── GeminiFake.php └── tests ├── Arch.php ├── Facades └── Gemini.php └── ServiceProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /.php-cs-fixer.cache 3 | /.php-cs-fixer.php 4 | /composer.lock 5 | /vendor/ 6 | .idea 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## v1.0.1 (2024-02-11) 8 | ### Fixed 9 | - Updated docblocs 10 | 11 | ## v1.0.0 (2024-02-11) 12 | ### Added 13 | - First version -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are welcome, and are accepted via pull requests. 4 | Please review these guidelines before submitting any pull requests. 5 | 6 | ## Process 7 | 8 | 1. Fork the project 9 | 1. Create a new branch 10 | 1. Code, test, commit and push 11 | 1. Open a pull request detailing your changes. Make sure to follow the [template](.github/PULL_REQUEST_TEMPLATE.md) 12 | 13 | ## Guidelines 14 | 15 | * Please ensure the coding style running `composer lint`. 16 | * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. 17 | * You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. 18 | * Please remember that we follow [SemVer](http://semver.org/). 19 | 20 | ## Setup 21 | 22 | Clone your fork, then install the dev dependencies: 23 | ```bash 24 | composer install 25 | ``` 26 | 27 | ## Lint 28 | 29 | Lint your code: 30 | ```bash 31 | composer lint 32 | ``` 33 | 34 | ## Tests 35 | 36 | Run all tests: 37 | ```bash 38 | composer test 39 | ``` 40 | 41 | Check types: 42 | ```bash 43 | composer test:types 44 | ``` 45 | 46 | Unit tests: 47 | ```bash 48 | composer test:unit 49 | ``` 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Google Gemini PHP 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Google Gemini PHP for Laravel 3 |

4 | Latest Version 5 | License 6 |

7 |

8 | 9 | ------ 10 | 11 | **Gemini PHP** for Laravel is a community-maintained PHP API client that allows you to interact with the Gemini AI API. 12 | 13 | - Fatih AYDIN [github.com/aydinfatih](https://github.com/aydinfatih) 14 | - Vytautas Smilingis [github.com/Plytas](https://github.com/Plytas) 15 | 16 | For more information, take a look at the [google-gemini-php/client](https://github.com/google-gemini-php/client) repository. 17 | 18 | ## Table of Contents 19 | - [Prerequisites](#prerequisites) 20 | - [Setup](#setup) 21 | - [Installation](#installation) 22 | - [Setup your API key](#setup-your-api-key) 23 | - [Upgrade to 2.0](#upgrade-to-20) 24 | - [Usage](#usage) 25 | - [Chat Resource](#chat-resource) 26 | - [Text-only Input](#text-only-input) 27 | - [Text-and-image Input](#text-and-image-input) 28 | - [File Upload](#file-upload) 29 | - [Text-and-video Input](#text-and-video-input) 30 | - [Multi-turn Conversations (Chat)](#multi-turn-conversations-chat) 31 | - [Stream Generate Content](#stream-generate-content) 32 | - [Structured Output](#structured-output) 33 | - [Function calling](#function-calling) 34 | - [Count tokens](#count-tokens) 35 | - [Configuration](#configuration) 36 | - [Embedding Resource](#embedding-resource) 37 | - [Models](#models) 38 | - [List Models](#list-models) 39 | - [Get Model](#get-model) 40 | - [Testing](#testing) 41 | 42 | 43 | ## Prerequisites 44 | To complete this quickstart, make sure that your development environment meets the following requirements: 45 | 46 | - Requires [PHP 8.1+](https://php.net/releases/) 47 | - Requires [Laravel 9,10,11,12](https://laravel.com/) 48 | 49 | ## Setup 50 | 51 | ### Installation 52 | 53 | First, install Gemini via the [Composer](https://getcomposer.org/) package manager: 54 | 55 | ```bash 56 | composer require google-gemini-php/laravel 57 | ``` 58 | 59 | Next, execute the install command: 60 | 61 | ```bash 62 | php artisan gemini:install 63 | ``` 64 | 65 | This will create a config/gemini.php configuration file in your project, which you can modify to your needs using environment variables. Blank environment variables for the Gemini API key is already appended to your .env file. 66 | 67 | ``` 68 | GEMINI_API_KEY= 69 | ``` 70 | 71 | You can also define the following environment variables. 72 | ``` 73 | GEMINI_BASE_URL= 74 | GEMINI_REQUEST_TIMEOUT= 75 | ``` 76 | 77 | 78 | ### Setup your API key 79 | To use the Gemini API, you'll need an API key. If you don't already have one, create a key in Google AI Studio. 80 | 81 | [Get an API key](https://aistudio.google.com/app/apikey) 82 | 83 | ### Upgrade to 2.0 84 | 85 | Starting 2.0 release this package will work only with Gemini v1beta API ([see API versions](https://ai.google.dev/gemini-api/docs/api-versions)). 86 | 87 | To update, run this command: 88 | 89 | ```bash 90 | composer require google-gemini-php/laravel:^2.0 91 | ``` 92 | 93 | This release introduces support for new features: 94 | * Structured output 95 | * System instructions 96 | * File uploads 97 | * Function calling 98 | * Code execution 99 | * Grounding with Google Search 100 | * Cached content 101 | * Thinking model configuration 102 | * Speech model configuration 103 | 104 | `\Gemini\Enums\ModelType` enum has been deprecated and will be removed in next major version. Together with this `Gemini::geminiPro()` and `Gemini::geminiFlash()` methods have been removed. 105 | We suggest using `Gemini::generativeModel()` method and pass in the model string directly. All methods that had previously accepted `ModelType` enum now accept a `BackedEnum`. We recommend implementing your own enum for convenience. 106 | 107 | There may be other breaking changes not listed here. If you encounter any issues, please submit an issue or a pull request. 108 | 109 | ## Usage 110 | 111 | Interact with Gemini's API: 112 | 113 | ```php 114 | use Gemini\Laravel\Facades\Gemini; 115 | 116 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash')->generateContent('Hello'); 117 | 118 | $result->text(); // Hello! How can I assist you today? 119 | ``` 120 | 121 | ### Chat Resource 122 | 123 | For a complete list of supported input formats and methods in Gemini API v1, see the [models documentation](https://ai.google.dev/gemini-api/docs/models). 124 | 125 | #### Text-only Input 126 | Generate a response from the model given an input message. 127 | 128 | ```php 129 | use Gemini\Laravel\Facades\Gemini; 130 | 131 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash')->generateContent('Hello'); 132 | 133 | $result->text(); // Hello! How can I assist you today? 134 | ``` 135 | 136 | #### Text-and-image Input 137 | Generate responses by providing both text prompts and images to the Gemini model. 138 | 139 | ```php 140 | use Gemini\Data\Blob; 141 | use Gemini\Enums\MimeType; 142 | use Gemini\Laravel\Facades\Gemini; 143 | 144 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash') 145 | ->generateContent([ 146 | 'What is this picture?', 147 | new Blob( 148 | mimeType: MimeType::IMAGE_JPEG, 149 | data: base64_encode( 150 | file_get_contents('https://storage.googleapis.com/generativeai-downloads/images/scones.jpg') 151 | ) 152 | ) 153 | ]); 154 | 155 | $result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table. 156 | ``` 157 | 158 | #### File Upload 159 | To reference larger files and videos with various prompts, upload them to Gemini storage. 160 | 161 | ```php 162 | use Gemini\Enums\FileState; 163 | use Gemini\Enums\MimeType; 164 | use Gemini\Laravel\Facades\Gemini; 165 | 166 | $files = Gemini::files(); 167 | echo "Uploading\n"; 168 | $meta = $files->upload( 169 | filename: 'video.mp4', 170 | mimeType: MimeType::VIDEO_MP4, 171 | displayName: 'Video' 172 | ); 173 | echo "Processing"; 174 | do { 175 | echo "."; 176 | sleep(2); 177 | $meta = $files->metadataGet($meta->uri); 178 | } while (!$meta->state->complete()); 179 | echo "\n"; 180 | 181 | if ($meta->state == FileState::Failed) { 182 | die("Upload failed:\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT)); 183 | } 184 | 185 | echo "Processing complete\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT); 186 | echo "\n{$meta->uri}"; 187 | ``` 188 | 189 | #### Text-and-video Input 190 | Process video content and get AI-generated descriptions using the Gemini API with an uploaded video file. 191 | 192 | ```php 193 | use Gemini\Data\UploadedFile; 194 | use Gemini\Enums\MimeType; 195 | use Gemini\Laravel\Facades\Gemini; 196 | 197 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash') 198 | ->generateContent([ 199 | 'What is this video?', 200 | new UploadedFile( 201 | fileUri: '123-456', // accepts just the name or the full URI 202 | mimeType: MimeType::VIDEO_MP4 203 | ) 204 | ]); 205 | 206 | $result->text(); // The picture shows a table with a white tablecloth. On the table are two cups of coffee, a bowl of blueberries, a silver spoon, and some flowers. There are also some blueberry scones on the table. 207 | ``` 208 | 209 | #### Multi-turn Conversations (Chat) 210 | Using Gemini, you can build freeform conversations across multiple turns. 211 | 212 | ```php 213 | use Gemini\Data\Content; 214 | use Gemini\Enums\Role; 215 | use Gemini\Laravel\Facades\Gemini; 216 | 217 | $chat = Gemini::chat(model: 'gemini-2.0-flash') 218 | ->startChat(history: [ 219 | Content::parse(part: 'The stories you write about what I have to say should be one line. Is that clear?'), 220 | Content::parse(part: 'Yes, I understand. The stories I write about your input should be one line long.', role: Role::MODEL) 221 | ]); 222 | 223 | $response = $chat->sendMessage('Create a story set in a quiet village in 1600s France'); 224 | echo $response->text(); // Amidst rolling hills and winding cobblestone streets, the tranquil village of Beausoleil whispered tales of love, intrigue, and the magic of everyday life in 17th century France. 225 | 226 | $response = $chat->sendMessage('Rewrite the same story in 1600s England'); 227 | echo $response->text(); // In the heart of England's lush countryside, amidst emerald fields and thatched-roof cottages, the village of Willowbrook unfolded a tapestry of love, mystery, and the enchantment of ordinary days in the 17th century. 228 | 229 | ``` 230 | 231 | #### Stream Generate Content 232 | By default, the model returns a response after completing the entire generation process. You can achieve faster interactions by not waiting for the entire result, and instead use streaming to handle partial results. 233 | 234 | ```php 235 | $stream = Gemini::generativeModel(model: 'gemini-2.0-flash') 236 | ->streamGenerateContent('Write long a story about a magic backpack.'); 237 | 238 | foreach ($stream as $response) { 239 | echo $response->text(); 240 | } 241 | ``` 242 | 243 | #### Structured Output 244 | Gemini generates unstructured text by default, but some applications require structured text. For these use cases, you can constrain Gemini to respond with JSON, a structured data format suitable for automated processing. You can also constrain the model to respond with one of the options specified in an enum. 245 | 246 | ```php 247 | use Gemini\Data\GenerationConfig; 248 | use Gemini\Data\Schema; 249 | use Gemini\Enums\DataType; 250 | use Gemini\Enums\ResponseMimeType; 251 | use Gemini\Laravel\Facades\Gemini; 252 | 253 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash') 254 | ->withGenerationConfig( 255 | generationConfig: new GenerationConfig( 256 | responseMimeType: ResponseMimeType::APPLICATION_JSON, 257 | responseSchema: new Schema( 258 | type: DataType::ARRAY, 259 | items: new Schema( 260 | type: DataType::OBJECT, 261 | properties: [ 262 | 'recipe_name' => new Schema(type: DataType::STRING), 263 | 'cooking_time_in_minutes' => new Schema(type: DataType::INTEGER) 264 | ], 265 | required: ['recipe_name', 'cooking_time_in_minutes'], 266 | ) 267 | ) 268 | ) 269 | ) 270 | ->generateContent('List 5 popular cookie recipes with cooking time'); 271 | 272 | $result->json(); 273 | 274 | //[ 275 | // { 276 | // +"cooking_time_in_minutes": 10, 277 | // +"recipe_name": "Chocolate Chip Cookies", 278 | // }, 279 | // { 280 | // +"cooking_time_in_minutes": 12, 281 | // +"recipe_name": "Oatmeal Raisin Cookies", 282 | // }, 283 | // { 284 | // +"cooking_time_in_minutes": 10, 285 | // +"recipe_name": "Peanut Butter Cookies", 286 | // }, 287 | // { 288 | // +"cooking_time_in_minutes": 10, 289 | // +"recipe_name": "Snickerdoodles", 290 | // }, 291 | // { 292 | // +"cooking_time_in_minutes": 12, 293 | // +"recipe_name": "Sugar Cookies", 294 | // }, 295 | // ] 296 | 297 | ``` 298 | 299 | #### Function calling 300 | Gemini provides the ability to define and utilize custom functions that the model can call during conversations. This enables the model to perform specific actions or calculations through your defined functions. 301 | 302 | ```php 303 | name === 'addition') { 319 | return new Content( 320 | parts: [ 321 | new Part( 322 | functionResponse: new FunctionResponse( 323 | name: 'addition', 324 | response: ['answer' => $functionCall->args['number1'] + $functionCall->args['number2']], 325 | ) 326 | ) 327 | ], 328 | role: Role::USER 329 | ); 330 | } 331 | 332 | //Handle other function calls 333 | } 334 | 335 | $chat = Gemini::generativeModel(model: 'gemini-2.0-flash') 336 | ->withTool(new Tool( 337 | functionDeclarations: [ 338 | new FunctionDeclaration( 339 | name: 'addition', 340 | description: 'Performs addition', 341 | parameters: new Schema( 342 | type: DataType::OBJECT, 343 | properties: [ 344 | 'number1' => new Schema( 345 | type: DataType::NUMBER, 346 | description: 'First number' 347 | ), 348 | 'number2' => new Schema( 349 | type: DataType::NUMBER, 350 | description: 'Second number' 351 | ), 352 | ], 353 | required: ['number1', 'number2'] 354 | ) 355 | ) 356 | ] 357 | )) 358 | ->startChat(); 359 | 360 | $response = $chat->sendMessage('What is 4 + 3?'); 361 | 362 | if ($response->parts()[0]->functionCall !== null) { 363 | $functionResponse = handleFunctionCall($response->parts()[0]->functionCall); 364 | 365 | $response = $chat->sendMessage($functionResponse); 366 | } 367 | 368 | echo $response->text(); // 4 + 3 = 7 369 | ``` 370 | 371 | #### Count tokens 372 | When using long prompts, it might be useful to count tokens before sending any content to the model. 373 | 374 | ```php 375 | use Gemini\Laravel\Facades\Gemini; 376 | 377 | $response = Gemini::generativeModel(model: 'gemini-2.0-flash') 378 | ->countTokens('Write a story about a magic backpack.'); 379 | 380 | echo $response->totalTokens; // 9 381 | ``` 382 | 383 | #### Configuration 384 | Every prompt you send to the model includes parameter values that control how the model generates a response. The model can generate different results for different parameter values. Learn more about [model parameters](https://ai.google.dev/docs/concepts#model_parameters). 385 | 386 | Also, you can use safety settings to adjust the likelihood of getting responses that may be considered harmful. By default, safety settings block content with medium and/or high probability of being unsafe content across all dimensions. Learn more about [safety settings](https://ai.google.dev/docs/concepts#safety_setting). 387 | 388 | 389 | ```php 390 | use Gemini\Data\GenerationConfig; 391 | use Gemini\Enums\HarmBlockThreshold; 392 | use Gemini\Data\SafetySetting; 393 | use Gemini\Enums\HarmCategory; 394 | use Gemini\Laravel\Facades\Gemini; 395 | 396 | $safetySettingDangerousContent = new SafetySetting( 397 | category: HarmCategory::HARM_CATEGORY_DANGEROUS_CONTENT, 398 | threshold: HarmBlockThreshold::BLOCK_ONLY_HIGH 399 | ); 400 | 401 | $safetySettingHateSpeech = new SafetySetting( 402 | category: HarmCategory::HARM_CATEGORY_HATE_SPEECH, 403 | threshold: HarmBlockThreshold::BLOCK_ONLY_HIGH 404 | ); 405 | 406 | $generationConfig = new GenerationConfig( 407 | stopSequences: [ 408 | 'Title', 409 | ], 410 | maxOutputTokens: 800, 411 | temperature: 1, 412 | topP: 0.8, 413 | topK: 10 414 | ); 415 | 416 | $generativeModel = Gemini::generativeModel(model: 'gemini-2.0-flash') 417 | ->withSafetySetting($safetySettingDangerousContent) 418 | ->withSafetySetting($safetySettingHateSpeech) 419 | ->withGenerationConfig($generationConfig) 420 | ->generateContent("Write a story about a magic backpack."); 421 | ``` 422 | 423 | ### Embedding Resource 424 | Embedding is a technique used to represent information as a list of floating point numbers in an array. With Gemini, you can represent text (words, sentences, and blocks of text) in a vectorized form, making it easier to compare and contrast embeddings. For example, two texts that share a similar subject matter or sentiment should have similar embeddings, which can be identified through mathematical comparison techniques such as cosine similarity. 425 | 426 | Use the `text-embedding-004` model with either `embedContents` or `batchEmbedContents`: 427 | 428 | ```php 429 | use Gemini\Laravel\Facades\Gemini; 430 | 431 | $response = Gemini::embeddingModel('text-embedding-004') 432 | ->embedContent("Write a story about a magic backpack."); 433 | 434 | print_r($response->embedding->values); 435 | //[ 436 | // [0] => 0.008624583 437 | // [1] => -0.030451821 438 | // [2] => -0.042496547 439 | // [3] => -0.029230341 440 | // [4] => 0.05486475 441 | // [5] => 0.006694871 442 | // [6] => 0.004025645 443 | // [7] => -0.007294857 444 | // [8] => 0.0057651913 445 | // ... 446 | //] 447 | ``` 448 | 449 | ### Models 450 | 451 | We recommend checking [Google documentation](https://ai.google.dev/gemini-api/docs/models) for the latest supported models. 452 | 453 | #### List Models 454 | Use list models to see the available Gemini models: 455 | 456 | ```php 457 | use Gemini\Laravel\Facades\Gemini; 458 | 459 | $response = Gemini::models()->list(); 460 | 461 | $response->models; 462 | //[ 463 | // [0] => Gemini\Data\Model Object 464 | // ( 465 | // [name] => models/gemini-2.0-flash 466 | // [version] => 2.0 467 | // [displayName] => Gemini 2.0 Flash 468 | // [description] => Gemini 2.0 Flash 469 | // ... 470 | // ) 471 | // [1] => Gemini\Data\Model Object 472 | // ( 473 | // [name] => models/gemini-2.5-pro-preview-05-06 474 | // [version] => 2.5-preview-05-06 475 | // [displayName] => Gemini 2.5 Pro Preview 05-06 476 | // [description] => Preview release (May 6th, 2025) of Gemini 2.5 Pro 477 | // ... 478 | // ) 479 | // [2] => Gemini\Data\Model Object 480 | // ( 481 | // [name] => models/text-embedding-004 482 | // [version] => 004 483 | // [displayName] => Text Embedding 004 484 | // [description] => Obtain a distributed representation of a text. 485 | // ... 486 | // ) 487 | //] 488 | ``` 489 | 490 | #### Get Model 491 | Get information about a model, such as version, display name, input token limit, etc. 492 | ```php 493 | use Gemini\Laravel\Facades\Gemini; 494 | 495 | $response = Gemini::models()->retrieve('models/gemini-2.5-pro-preview-05-06'); 496 | 497 | $response->model; 498 | //Gemini\Data\Model Object 499 | //( 500 | // [name] => models/gemini-2.5-pro-preview-05-06 501 | // [version] => 2.5-preview-05-06 502 | // [displayName] => Gemini 2.5 Pro Preview 05-06 503 | // [description] => Preview release (May 6th, 2025) of Gemini 2.5 Pro 504 | // ... 505 | //) 506 | ``` 507 | 508 | ## Testing 509 | 510 | The package provides a fake implementation of the `Gemini\Client` class that allows you to fake the API responses. 511 | 512 | To test your code ensure you swap the `Gemini\Client` class with the `Gemini\Testing\ClientFake` class in your test case. 513 | 514 | The fake responses are returned in the order they are provided while creating the fake client. 515 | 516 | All responses are having a `fake()` method that allows you to easily create a response object by only providing the parameters relevant for your test case. 517 | 518 | ```php 519 | use Gemini\Laravel\Facades\Gemini; 520 | use Gemini\Responses\GenerativeModel\GenerateContentResponse; 521 | 522 | Gemini::fake([ 523 | GenerateContentResponse::fake([ 524 | 'candidates' => [ 525 | [ 526 | 'content' => [ 527 | 'parts' => [ 528 | [ 529 | 'text' => 'success', 530 | ], 531 | ], 532 | ], 533 | ], 534 | ], 535 | ]), 536 | ]); 537 | 538 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash')->generateContent('test'); 539 | 540 | expect($result->text())->toBe('success'); 541 | ``` 542 | 543 | In case of a streamed response you can optionally provide a resource holding the fake response data. 544 | 545 | ```php 546 | use Gemini\Laravel\Facades\Gemini; 547 | use Gemini\Responses\GenerativeModel\GenerateContentResponse; 548 | 549 | Gemini::fake([ 550 | GenerateContentResponse::fakeStream(), 551 | ]); 552 | 553 | $result = Gemini::generativeModel(model: 'gemini-2.0-flash')->streamGenerateContent('Hello'); 554 | 555 | expect($response->getIterator()->current()) 556 | ->text()->toBe('In the bustling city of Aethelwood, where the cobblestone streets whispered'); 557 | ``` 558 | 559 | After the requests have been sent there are various methods to ensure that the expected requests were sent: 560 | 561 | ```php 562 | use Gemini\Laravel\Facades\Gemini; 563 | use Gemini\Resources\GenerativeModel; 564 | use Gemini\Resources\Models; 565 | 566 | // assert list models request was sent 567 | Gemini::models()->assertSent(callback: function ($method) { 568 | return $method === 'list'; 569 | }); 570 | // or 571 | Gemini::assertSent(resource: Models::class, callback: function ($method) { 572 | return $method === 'list'; 573 | }); 574 | 575 | Gemini::generativeModel(model: 'gemini-2.0-flash')->assertSent(function (string $method, array $parameters) { 576 | return $method === 'generateContent' && 577 | $parameters[0] === 'Hello'; 578 | }); 579 | // or 580 | Gemini::assertSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash', callback: function (string $method, array $parameters) { 581 | return $method === 'generateContent' && 582 | $parameters[0] === 'Hello'; 583 | }); 584 | 585 | 586 | // assert 2 generative model requests were sent 587 | Gemini::assertSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash', callback: 2); 588 | // or 589 | Gemini::geminiPro()->assertSent(2); 590 | 591 | // assert no generative model requests were sent 592 | Gemini::assertNotSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash'); 593 | // or 594 | Gemini::geminiPro()->assertNotSent(); 595 | 596 | // assert no requests were sent 597 | Gemini::assertNothingSent(); 598 | ``` 599 | 600 | To write tests expecting the API request to fail you can provide a `Throwable` object as the response. 601 | 602 | ```php 603 | use Gemini\Laravel\Facades\Gemini; 604 | use Gemini\Exceptions\ErrorException; 605 | 606 | Gemini::fake([ 607 | new ErrorException([ 608 | 'message' => 'The model `gemini-basic` does not exist', 609 | 'status' => 'INVALID_ARGUMENT', 610 | 'code' => 400, 611 | ]), 612 | ]); 613 | 614 | // the `ErrorException` will be thrown 615 | Gemini::generativeModel(model: 'gemini-2.0-flash')->generateContent('test'); 616 | ``` 617 | -------------------------------------------------------------------------------- /art/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-gemini-php/laravel/561c3921f449ede58ab89ad3e8017f2eef16aca7/art/example.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-gemini-php/laravel", 3 | "description": "Google Gemini PHP for Laravel is a supercharged PHP API client that allows you to interact with the Google Gemini AI API", 4 | "keywords": ["laravel","php", "gemini", "sdk", "api", "client", "natural", "language", "processing"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Fatih AYDIN", 9 | "email": "aydinfatih52@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1.0", 14 | "google-gemini-php/client": "^2.0", 15 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0" 16 | }, 17 | "require-dev": { 18 | "guzzlehttp/guzzle": "^7.8.1", 19 | "laravel/pint": "^1.13.6", 20 | "pestphp/pest": "^2.27.0", 21 | "pestphp/pest-plugin-arch": "^2.4.1", 22 | "phpstan/phpstan": "^1.10.47", 23 | "symfony/var-dumper": "^6.4.0|^7.0.1" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Gemini\\Laravel\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Tests\\": "tests/" 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "config": { 38 | "sort-packages": true, 39 | "preferred-install": "dist", 40 | "allow-plugins": { 41 | "pestphp/pest-plugin": true, 42 | "php-http/discovery": true 43 | } 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Gemini\\Laravel\\ServiceProvider" 49 | ] 50 | } 51 | }, 52 | "scripts": { 53 | "lint": "pint -v", 54 | "test:lint": "pint --test -v", 55 | "test:types": "phpstan analyse --ansi", 56 | "test:unit": "pest --colors=always", 57 | "test": [ 58 | "@test:lint", 59 | "@test:types", 60 | "@test:unit" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/gemini.php: -------------------------------------------------------------------------------- 1 | env('GEMINI_API_KEY'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Gemini Base URL 22 | |-------------------------------------------------------------------------- 23 | | 24 | | If you need a specific base URL for the Gemini API, you can provide it here. 25 | | Otherwise, leave empty to use the default value. 26 | */ 27 | 'base_url' => env('GEMINI_BASE_URL'), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Request Timeout 32 | |-------------------------------------------------------------------------- 33 | | 34 | | The timeout may be used to specify the maximum number of seconds to wait 35 | | for a response. By default, the client will time out after 30 seconds. 36 | */ 37 | 38 | 'request_timeout' => env('GEMINI_REQUEST_TIMEOUT', 30), 39 | ]; 40 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | 6 | reportUnmatchedIgnoredErrors: true -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | ./src 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/components/badge.php: -------------------------------------------------------------------------------- 1 | ['red', 'ERROR'], 7 | default => ['blue', 'INFO'], 8 | }; 9 | 10 | ?> 11 | 12 |
13 | 14 | 15 | 16 | 17 |
-------------------------------------------------------------------------------- /resources/views/components/new-line.php: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /resources/views/components/two-column-detail.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
-------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | 'https://github.com/google-gemini-php/laravel', 15 | 'Gemini PHP Docs' => 'https://github.com/google-gemini-php/client#readme', 16 | ]; 17 | 18 | protected $signature = 'gemini:install'; 19 | 20 | protected $description = 'Prepares the Gemini client for use.'; 21 | 22 | public function handle(): void 23 | { 24 | View::renderUsing($this->output); 25 | 26 | View::render('components.badge', [ 27 | 'type' => 'INFO', 28 | 'content' => 'Installing Gemini for Laravel.', 29 | ]); 30 | 31 | $this->copyConfig(); 32 | 33 | View::render('components.new-line'); 34 | 35 | $this->addEnvKeys('.env'); 36 | $this->addEnvKeys('.env.example'); 37 | 38 | View::render('components.new-line'); 39 | 40 | $wantsToSupport = $this->askToStarRepository(); 41 | 42 | $this->showLinks(); 43 | 44 | View::render('components.badge', [ 45 | 'type' => 'INFO', 46 | 'content' => 'Open your .env and add your Gemini API key.', 47 | ]); 48 | 49 | if ($wantsToSupport) { 50 | $this->openRepositoryInBrowser(); 51 | } 52 | } 53 | 54 | private function copyConfig(): void 55 | { 56 | if (file_exists(config_path('gemini.php'))) { 57 | View::render('components.two-column-detail', [ 58 | 'left' => 'config/gemini.php', 59 | 'right' => 'File already exists.', 60 | ]); 61 | 62 | return; 63 | } 64 | 65 | View::render('components.two-column-detail', [ 66 | 'left' => 'config/gemini.php', 67 | 'right' => 'File created.', 68 | ]); 69 | 70 | $this->callSilent('vendor:publish', [ 71 | '--provider' => ServiceProvider::class, 72 | ]); 73 | } 74 | 75 | private function addEnvKeys(string $envFile): void 76 | { 77 | if (! file_exists(base_path($envFile))) { 78 | return; 79 | } 80 | 81 | $fileContent = file_get_contents(base_path($envFile)); 82 | 83 | if ($fileContent === false) { 84 | return; 85 | } 86 | 87 | if (str_contains($fileContent, 'GEMINI_API_KEY')) { 88 | View::render('components.two-column-detail', [ 89 | 'left' => $envFile, 90 | 'right' => 'Variables already exists.', 91 | ]); 92 | 93 | return; 94 | } 95 | 96 | file_put_contents(base_path($envFile), PHP_EOL.'GEMINI_API_KEY='.PHP_EOL, FILE_APPEND); 97 | 98 | View::render('components.two-column-detail', [ 99 | 'left' => $envFile, 100 | 'right' => 'GEMINI_API_KEY variable added.', 101 | ]); 102 | } 103 | 104 | private function askToStarRepository(): bool 105 | { 106 | if (! $this->input->isInteractive()) { 107 | return false; 108 | } 109 | 110 | return $this->confirm(' Wanna show Gemini for Laravel some love by starring it on GitHub?', false); 111 | } 112 | 113 | private function openRepositoryInBrowser(): void 114 | { 115 | if (PHP_OS_FAMILY == 'Darwin') { 116 | exec('open https://github.com/google-gemini-php/laravel'); 117 | } 118 | if (PHP_OS_FAMILY == 'Windows') { 119 | exec('start https://github.com/google-gemini-php/laravel'); 120 | } 121 | if (PHP_OS_FAMILY == 'Linux') { 122 | exec('xdg-open https://github.com/google-gemini-php/laravel'); 123 | } 124 | } 125 | 126 | private function showLinks(): void 127 | { 128 | $links = [ 129 | ...self::LINKS, 130 | ]; 131 | 132 | foreach ($links as $message => $link) { 133 | View::render('components.two-column-detail', [ 134 | 'left' => $message, 135 | 'right' => $link, 136 | ]); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Exceptions/MissingApiKey.php: -------------------------------------------------------------------------------- 1 | $responses 31 | */ 32 | public static function fake(array $responses = []): GeminiFake 33 | { 34 | $fake = new GeminiFake($responses); 35 | self::swap($fake); 36 | 37 | return $fake; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(ClientContract::class, static function (): Client { 25 | $apiKey = config('gemini.api_key'); 26 | 27 | if (! is_string($apiKey)) { 28 | throw MissingApiKey::create(); 29 | } 30 | 31 | $baseURL = config('gemini.base_url'); 32 | if (isset($baseURL) && ! is_string($baseURL)) { 33 | throw new InvalidArgumentException('Invalid Gemini API base URL.'); 34 | } 35 | 36 | $client = Gemini::factory() 37 | ->withApiKey(apiKey: $apiKey) 38 | ->withHttpClient(client: new GuzzleClient(['timeout' => config('gemini.request_timeout', 30)])); 39 | 40 | if (! empty($baseURL)) { 41 | $client->withBaseUrl(baseUrl: $baseURL); 42 | } 43 | 44 | return $client->make(); 45 | }); 46 | 47 | $this->app->alias(ClientContract::class, 'gemini'); 48 | $this->app->alias(ClientContract::class, Client::class); 49 | } 50 | 51 | /** 52 | * Bootstrap any application services. 53 | */ 54 | public function boot(): void 55 | { 56 | if ($this->app->runningInConsole()) { 57 | $this->publishes([ 58 | __DIR__.'/../config/gemini.php' => config_path('gemini.php'), 59 | ]); 60 | 61 | $this->commands([ 62 | InstallCommand::class, 63 | ]); 64 | } 65 | } 66 | 67 | /** 68 | * Get the services provided by the provider. 69 | * 70 | * @return array 71 | */ 72 | public function provides(): array 73 | { 74 | return [ 75 | Client::class, 76 | ClientContract::class, 77 | 'gemini', 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Support/View.php: -------------------------------------------------------------------------------- 1 | $data 35 | */ 36 | public static function render(string $path, array $data = []): void 37 | { 38 | $contents = self::compile($path, $data); 39 | 40 | $existing = Termwind::getRenderer(); 41 | 42 | renderUsing(self::$output); 43 | 44 | try { 45 | render($contents); 46 | } finally { 47 | renderUsing($existing); 48 | } 49 | } 50 | 51 | /** 52 | * Compiles the given view. 53 | * 54 | * @param array $data 55 | */ 56 | private static function compile(string $path, array $data): string 57 | { 58 | extract($data); 59 | 60 | ob_start(); 61 | 62 | $path = str_replace('.', '/', $path); 63 | 64 | include sprintf('%s/../../resources/views/%s.php', __DIR__, $path); 65 | 66 | $contents = ob_get_contents(); 67 | 68 | ob_clean(); 69 | 70 | return (string) $contents; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Testing/GeminiFake.php: -------------------------------------------------------------------------------- 1 | expect('Gemini\Laravel\Exceptions') 5 | ->toUseNothing(); 6 | 7 | test('facades') 8 | ->expect('Gemini\Laravel\Facades\Gemini') 9 | ->toOnlyUse([ 10 | 'Illuminate\Support\Facades\Facade', 11 | 'Gemini\Contracts\ResponseContract', 12 | 'Gemini\Laravel\Testing\GeminiFake', 13 | 'Gemini\Responses\StreamResponse', 14 | ]); 15 | 16 | test('service providers') 17 | ->expect('Gemini\Laravel\ServiceProvider') 18 | ->toOnlyUse([ 19 | 'GuzzleHttp\Client', 20 | 'Illuminate\Support\ServiceProvider', 21 | 'Gemini\Laravel', 22 | 'Gemini', 23 | 'Illuminate\Contracts\Support\DeferrableProvider', 24 | 25 | // helpers... 26 | 'config', 27 | 'config_path', 28 | ]); 29 | -------------------------------------------------------------------------------- /tests/Facades/Gemini.php: -------------------------------------------------------------------------------- 1 | bind('config', fn () => new Repository([ 15 | 'gemini' => [ 16 | 'api_key' => 'test', 17 | ], 18 | ])); 19 | 20 | (new ServiceProvider($app))->register(); 21 | 22 | Gemini::setFacadeApplication($app); 23 | 24 | $generativeModel = Gemini::geminiPro(); 25 | 26 | expect($generativeModel)->toBeInstanceOf(GenerativeModel::class); 27 | }); 28 | 29 | test('fake returns the given response', function () { 30 | Gemini::fake([ 31 | GenerateContentResponse::fake([ 32 | 'candidates' => [ 33 | [ 34 | 'content' => [ 35 | 'parts' => [ 36 | [ 37 | 'text' => 'success', 38 | ], 39 | ], 40 | ], 41 | ], 42 | ], 43 | ]), 44 | ]); 45 | 46 | $result = Gemini::geminiPro()->generateContent('Php is'); 47 | 48 | expect($result->text())->toBe('success'); 49 | }); 50 | 51 | test('fake throws an exception if there is no more given response', function () { 52 | Gemini::fake([ 53 | GenerateContentResponse::fake(), 54 | ]); 55 | Gemini::geminiPro()->generateContent('Php is'); 56 | 57 | Gemini::geminiPro()->generateContent('Php is'); 58 | })->expectExceptionMessage('No fake responses left'); 59 | 60 | test('append more fake responses', function () { 61 | Gemini::fake([ 62 | GenerateContentResponse::fake([ 63 | 'candidates' => [ 64 | [ 65 | 'content' => [ 66 | 'parts' => [ 67 | [ 68 | 'text' => 'response-1', 69 | ], 70 | ], 71 | ], 72 | ], 73 | ], 74 | ]), 75 | ]); 76 | 77 | Gemini::addResponses([ 78 | GenerateContentResponse::fake([ 79 | 'candidates' => [ 80 | [ 81 | 'content' => [ 82 | 'parts' => [ 83 | [ 84 | 'text' => 'response-2', 85 | ], 86 | ], 87 | ], 88 | ], 89 | ], 90 | ]), 91 | ]); 92 | 93 | $result = Gemini::geminiPro()->generateContent('Php is'); 94 | 95 | expect($result->text())->toBe('response-1'); 96 | 97 | $result = Gemini::geminiPro()->generateContent('Php is'); 98 | 99 | expect($result->text())->toBe('response-2'); 100 | }); 101 | 102 | test('fake can assert a request was sent', function () { 103 | Gemini::fake([ 104 | GenerateContentResponse::fake(), 105 | ]); 106 | 107 | Gemini::geminiPro()->generateContent('test'); 108 | 109 | Gemini::assertSent(resource: GenerativeModel::class, model: ModelType::GEMINI_PRO, callback: function (string $method, array $parameters) { 110 | return $method === 'generateContent' && 111 | $parameters[0] === 'test'; 112 | }); 113 | }); 114 | 115 | test('fake throws an exception if a request was not sent', function () { 116 | Gemini::fake([ 117 | GenerateContentResponse::fake(), 118 | ]); 119 | 120 | Gemini::assertSent(resource: GenerativeModel::class, callback: function (string $method, array $parameters) { 121 | return $method === 'create' && 122 | $parameters['model'] === 'gpt-3.5-turbo-instruct' && 123 | $parameters['prompt'] === 'PHP is '; 124 | }); 125 | })->expectException(ExpectationFailedException::class); 126 | 127 | test('fake can assert a request was sent on the resource', function () { 128 | Gemini::fake([ 129 | GenerateContentResponse::fake(), 130 | ]); 131 | 132 | Gemini::geminiPro()->generateContent('test'); 133 | 134 | Gemini::geminiPro()->assertSent(function (string $method, array $parameters): bool { 135 | return $method === 'generateContent' && 136 | $parameters[0] === 'test'; 137 | }); 138 | }); 139 | 140 | test('fake can assert a request was sent n times', function () { 141 | Gemini::fake([ 142 | GenerateContentResponse::fake(), 143 | GenerateContentResponse::fake(), 144 | ]); 145 | 146 | Gemini::geminiPro()->generateContent('test'); 147 | 148 | Gemini::geminiPro()->generateContent('test'); 149 | 150 | Gemini::assertSent(GenerativeModel::class, ModelType::GEMINI_PRO, 2); 151 | }); 152 | 153 | test('fake throws an exception if a request was not sent n times', function () { 154 | Gemini::fake([ 155 | GenerateContentResponse::fake(), 156 | GenerateContentResponse::fake(), 157 | ]); 158 | 159 | Gemini::geminiPro()->generateContent('test'); 160 | 161 | Gemini::assertSent(GenerativeModel::class, ModelType::GEMINI_PRO, 2); 162 | })->expectException(ExpectationFailedException::class); 163 | 164 | test('fake can assert a request was not sent', function () { 165 | Gemini::fake(); 166 | 167 | Gemini::assertNotSent(GenerativeModel::class); 168 | }); 169 | 170 | test('fake throws an exception if a unexpected request was sent', function () { 171 | Gemini::fake([ 172 | GenerateContentResponse::fake(), 173 | ]); 174 | 175 | Gemini::geminiPro()->generateContent('test'); 176 | 177 | Gemini::assertNotSent(GenerativeModel::class); 178 | })->expectException(ExpectationFailedException::class); 179 | 180 | test('fake can assert a request was not sent on the resource', function () { 181 | Gemini::fake([ 182 | GenerateContentResponse::fake(), 183 | ]); 184 | 185 | Gemini::geminiPro()->assertNotSent(); 186 | }); 187 | 188 | test('fake can assert no request was sent', function () { 189 | Gemini::fake(); 190 | 191 | Gemini::assertNothingSent(); 192 | }); 193 | 194 | test('fake throws an exception if any request was sent when non was expected', function () { 195 | Gemini::fake([ 196 | GenerateContentResponse::fake(), 197 | ]); 198 | 199 | Gemini::geminiPro()->generateContent('test'); 200 | 201 | Gemini::assertNothingSent(); 202 | })->expectException(ExpectationFailedException::class); 203 | -------------------------------------------------------------------------------- /tests/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | bind('config', fn () => new Repository([ 13 | 'gemini' => [ 14 | 'api_key' => 'test', 15 | 'base_url' => 'test', 16 | ], 17 | ])); 18 | 19 | (new ServiceProvider($app))->register(); 20 | 21 | expect($app->get(Client::class))->toBeInstanceOf(Client::class); 22 | }); 23 | 24 | it('binds the client on the container as singleton', function () { 25 | $app = app(); 26 | 27 | $app->bind('config', fn () => new Repository([ 28 | 'gemini' => [ 29 | 'api_key' => 'test', 30 | ], 31 | ])); 32 | 33 | (new ServiceProvider($app))->register(); 34 | 35 | $client = $app->get(Client::class); 36 | 37 | expect($app->get(Client::class))->toBe($client); 38 | }); 39 | 40 | it('requires an api key', function () { 41 | $app = app(); 42 | 43 | $app->bind('config', fn () => new Repository([])); 44 | 45 | (new ServiceProvider($app))->register(); 46 | 47 | $app->get(Client::class); 48 | })->throws( 49 | MissingApiKey::class, 50 | 'The Gemini API Key is missing. Please publish the [gemini.php] configuration file and set the [api_key].', 51 | ); 52 | 53 | it('validates base url', function () { 54 | $app = app(); 55 | 56 | $app->bind('config', fn () => new Repository([ 57 | 'gemini' => [ 58 | 'api_key' => 'test', 59 | 'base_url' => 123, 60 | ], 61 | ])); 62 | 63 | (new ServiceProvider($app))->register(); 64 | 65 | $app->get(Client::class); 66 | })->throws( 67 | InvalidArgumentException::class, 68 | 'Invalid Gemini API base URL.', 69 | ); 70 | 71 | it('provides', function () { 72 | $app = app(); 73 | 74 | $provides = (new ServiceProvider($app))->provides(); 75 | 76 | expect($provides)->toBe([ 77 | Client::class, 78 | ClientContract::class, 79 | 'gemini', 80 | ]); 81 | }); 82 | --------------------------------------------------------------------------------