├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── art
└── example.png
├── composer.json
├── phpstan.neon
├── phpunit.xml
├── src
├── DependencyInjection
│ ├── Configuration.php
│ └── GeminiExtension.php
├── GeminiBundle.php
└── Resources
│ └── config
│ └── services.php
└── tests
└── DependencyInjection
└── GeminiExtensionTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /.phpunit.cache
2 | /.php-cs-fixer.cache
3 | /.php-cs-fixer.php
4 | /composer.lock
5 | /vendor/
6 | .idea
7 |
--------------------------------------------------------------------------------
/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 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 google-gemini-php/symfony
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ------
10 |
11 | **Gemini PHP** for Symfony 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 [Symfony 5,6,7](https://symfony.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/symfony
57 | ```
58 |
59 | Next, register the bundle in your config/bundles.php:
60 |
61 | ```bash
62 | return [
63 | // ...
64 | Gemini\Symfony\GeminiBundle::class => ['all' => true],
65 | ]
66 | ```
67 |
68 | This will create a `.env` configuration file in your project, which you can modify to your needs using environment variables:
69 |
70 | ```
71 | GEMINI_API_KEY=
72 | ```
73 |
74 | You can also define the following environment variables.
75 | ```
76 | GEMINI_BASE_URL=
77 | ```
78 |
79 |
80 | ### Setup your API key
81 | 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.
82 |
83 | [Get an API key](https://aistudio.google.com/app/apikey)
84 |
85 | ### Upgrade to 2.0
86 |
87 | 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)).
88 |
89 | To update, run this command:
90 |
91 | ```bash
92 | composer require google-gemini-php/symfony:^2.0
93 | ```
94 |
95 | This release introduces support for new features:
96 | * Structured output
97 | * System instructions
98 | * File uploads
99 | * Function calling
100 | * Code execution
101 | * Grounding with Google Search
102 | * Cached content
103 | * Thinking model configuration
104 | * Speech model configuration
105 |
106 | `\Gemini\Enums\ModelType` enum has been deprecated and will be removed in next major version. Together with this `->geminiPro()` and `->geminiFlash()` methods have been deprecated as well.
107 | We suggest using `->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.
108 |
109 | There may be other breaking changes not listed here. If you encounter any issues, please submit an issue or a pull request.
110 |
111 | ## Usage
112 |
113 | Interact with Gemini's API:
114 |
115 | ```php
116 | $result = $container->get('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. If the input contains only text, use the `gemini-pro` model.
127 |
128 | ```php
129 | $result = $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->generateContent('Hello');
130 |
131 | $result->text(); // Hello! How can I assist you today?
132 | ```
133 |
134 | #### Text-and-image Input
135 | If the input contains both text and image, use the `gemini-pro-vision` model.
136 |
137 | ```php
138 | use Gemini\Data\Blob;
139 | use Gemini\Enums\MimeType;
140 |
141 | $result = $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')
142 | ->generateContent([
143 | 'What is this picture?',
144 | new Blob(
145 | mimeType: MimeType::IMAGE_JPEG,
146 | data: base64_encode(
147 | file_get_contents('https://storage.googleapis.com/generativeai-downloads/images/scones.jpg')
148 | )
149 | )
150 | ]);
151 |
152 | $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.
153 | ```
154 |
155 | #### File Upload
156 | To reference larger files and videos with various prompts, upload them to Gemini storage.
157 |
158 | ```php
159 | use Gemini\Enums\FileState;
160 | use Gemini\Enums\MimeType;
161 |
162 | $files = $container->get('gemini')->files();
163 | echo "Uploading\n";
164 | $meta = $files->upload(
165 | filename: 'video.mp4',
166 | mimeType: MimeType::VIDEO_MP4,
167 | displayName: 'Video'
168 | );
169 | echo "Processing";
170 | do {
171 | echo ".";
172 | sleep(2);
173 | $meta = $files->metadataGet($meta->uri);
174 | } while (!$meta->state->complete());
175 | echo "\n";
176 |
177 | if ($meta->state == FileState::Failed) {
178 | die("Upload failed:\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT));
179 | }
180 |
181 | echo "Processing complete\n" . json_encode($meta->toArray(), JSON_PRETTY_PRINT);
182 | echo "\n{$meta->uri}";
183 | ```
184 |
185 | #### Text-and-video Input
186 | Process video content and get AI-generated descriptions using the Gemini API with an uploaded video file.
187 |
188 | ```php
189 | use Gemini\Data\UploadedFile;
190 | use Gemini\Enums\MimeType;
191 |
192 | $result = $container->get('gemini')
193 | ->generativeModel(model: 'gemini-2.0-flash')
194 | ->generateContent([
195 | 'What is this video?',
196 | new UploadedFile(
197 | fileUri: '123-456', // accepts just the name or the full URI
198 | mimeType: MimeType::VIDEO_MP4
199 | )
200 | ]);
201 |
202 | $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.
203 | ```
204 |
205 | #### Multi-turn Conversations (Chat)
206 | Using Gemini, you can build freeform conversations across multiple turns.
207 |
208 | ```php
209 | use Gemini\Data\Content;
210 | use Gemini\Enums\Role;
211 |
212 | $chat = $container->get('gemini')
213 | ->chat(model: 'gemini-2.0-flash')
214 | ->startChat(history: [
215 | Content::parse(part: 'The stories you write about what I have to say should be one line. Is that clear?'),
216 | Content::parse(part: 'Yes, I understand. The stories I write about your input should be one line long.', role: Role::MODEL)
217 | ]);
218 |
219 | $response = $chat->sendMessage('Create a story set in a quiet village in 1600s France');
220 | 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.
221 |
222 | $response = $chat->sendMessage('Rewrite the same story in 1600s England');
223 | 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.
224 |
225 | ```
226 |
227 | #### Stream Generate Content
228 | 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.
229 |
230 | ```php
231 | $stream = $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')
232 | ->streamGenerateContent('Write long a story about a magic backpack.');
233 |
234 | foreach ($stream as $response) {
235 | echo $response->text();
236 | }
237 | ```
238 |
239 | #### Structured Output
240 | 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.
241 |
242 | ```php
243 | use Gemini\Data\GenerationConfig;
244 | use Gemini\Data\Schema;
245 | use Gemini\Enums\DataType;
246 | use Gemini\Enums\ResponseMimeType;
247 |
248 | $result = $container->get('gemini')
249 | ->generativeModel(model: 'gemini-2.0-flash')
250 | ->withGenerationConfig(
251 | generationConfig: new GenerationConfig(
252 | responseMimeType: ResponseMimeType::APPLICATION_JSON,
253 | responseSchema: new Schema(
254 | type: DataType::ARRAY,
255 | items: new Schema(
256 | type: DataType::OBJECT,
257 | properties: [
258 | 'recipe_name' => new Schema(type: DataType::STRING),
259 | 'cooking_time_in_minutes' => new Schema(type: DataType::INTEGER)
260 | ],
261 | required: ['recipe_name', 'cooking_time_in_minutes'],
262 | )
263 | )
264 | )
265 | )
266 | ->generateContent('List 5 popular cookie recipes with cooking time');
267 |
268 | $result->json();
269 |
270 | //[
271 | // {
272 | // +"cooking_time_in_minutes": 10,
273 | // +"recipe_name": "Chocolate Chip Cookies",
274 | // },
275 | // {
276 | // +"cooking_time_in_minutes": 12,
277 | // +"recipe_name": "Oatmeal Raisin Cookies",
278 | // },
279 | // {
280 | // +"cooking_time_in_minutes": 10,
281 | // +"recipe_name": "Peanut Butter Cookies",
282 | // },
283 | // {
284 | // +"cooking_time_in_minutes": 10,
285 | // +"recipe_name": "Snickerdoodles",
286 | // },
287 | // {
288 | // +"cooking_time_in_minutes": 12,
289 | // +"recipe_name": "Sugar Cookies",
290 | // },
291 | // ]
292 |
293 | ```
294 |
295 | #### Function calling
296 | 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.
297 |
298 | ```php
299 | name === 'addition') {
314 | return new Content(
315 | parts: [
316 | new Part(
317 | functionResponse: new FunctionResponse(
318 | name: 'addition',
319 | response: ['answer' => $functionCall->args['number1'] + $functionCall->args['number2']],
320 | )
321 | )
322 | ],
323 | role: Role::USER
324 | );
325 | }
326 |
327 | //Handle other function calls
328 | }
329 |
330 | $chat = $container->get('gemini')
331 | ->generativeModel(model: 'gemini-2.0-flash')
332 | ->withTool(new Tool(
333 | functionDeclarations: [
334 | new FunctionDeclaration(
335 | name: 'addition',
336 | description: 'Performs addition',
337 | parameters: new Schema(
338 | type: DataType::OBJECT,
339 | properties: [
340 | 'number1' => new Schema(
341 | type: DataType::NUMBER,
342 | description: 'First number'
343 | ),
344 | 'number2' => new Schema(
345 | type: DataType::NUMBER,
346 | description: 'Second number'
347 | ),
348 | ],
349 | required: ['number1', 'number2']
350 | )
351 | )
352 | ]
353 | ))
354 | ->startChat();
355 |
356 | $response = $chat->sendMessage('What is 4 + 3?');
357 |
358 | if ($response->parts()[0]->functionCall !== null) {
359 | $functionResponse = handleFunctionCall($response->parts()[0]->functionCall);
360 |
361 | $response = $chat->sendMessage($functionResponse);
362 | }
363 |
364 | echo $response->text(); // 4 + 3 = 7
365 | ```
366 |
367 | #### Count tokens
368 | When using long prompts, it might be useful to count tokens before sending any content to the model.
369 |
370 | ```php
371 | $response = $container->get('gemini')
372 | ->generativeModel(model: 'gemini-2.0-flash')
373 | ->countTokens('Write a story about a magic backpack.');
374 |
375 | echo $response->totalTokens; // 9
376 | ```
377 |
378 | #### Configuration
379 | 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).
380 |
381 | 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).
382 |
383 |
384 | ```php
385 | use Gemini\Data\GenerationConfig;
386 | use Gemini\Enums\HarmBlockThreshold;
387 | use Gemini\Data\SafetySetting;
388 | use Gemini\Enums\HarmCategory;
389 |
390 | $safetySettingDangerousContent = new SafetySetting(
391 | category: HarmCategory::HARM_CATEGORY_DANGEROUS_CONTENT,
392 | threshold: HarmBlockThreshold::BLOCK_ONLY_HIGH
393 | );
394 |
395 | $safetySettingHateSpeech = new SafetySetting(
396 | category: HarmCategory::HARM_CATEGORY_HATE_SPEECH,
397 | threshold: HarmBlockThreshold::BLOCK_ONLY_HIGH
398 | );
399 |
400 | $generationConfig = new GenerationConfig(
401 | stopSequences: [
402 | 'Title',
403 | ],
404 | maxOutputTokens: 800,
405 | temperature: 1,
406 | topP: 0.8,
407 | topK: 10
408 | );
409 |
410 | $generativeModel = $container->get('gemini')
411 | ->generativeModel(model: 'gemini-2.0-flash')
412 | ->withSafetySetting($safetySettingDangerousContent)
413 | ->withSafetySetting($safetySettingHateSpeech)
414 | ->withGenerationConfig($generationConfig)
415 | ->generateContent("Write a story about a magic backpack.");
416 | ```
417 |
418 | ### Embedding Resource
419 | 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.
420 |
421 | Use the `text-embedding-004` model with either `embedContents` or `batchEmbedContents`:
422 |
423 | ```php
424 | $response = $container->get('gemini')
425 | ->embeddingModel('text-embedding-004')
426 | ->embedContent("Write a story about a magic backpack.");
427 |
428 | print_r($response->embedding->values);
429 | //[
430 | // [0] => 0.008624583
431 | // [1] => -0.030451821
432 | // [2] => -0.042496547
433 | // [3] => -0.029230341
434 | // [4] => 0.05486475
435 | // [5] => 0.006694871
436 | // [6] => 0.004025645
437 | // [7] => -0.007294857
438 | // [8] => 0.0057651913
439 | // ...
440 | //]
441 | ```
442 |
443 | ### Models
444 |
445 | We recommend checking [Google documentation](https://ai.google.dev/gemini-api/docs/models) for the latest supported models.
446 |
447 | #### List Models
448 | Use list models to see the available Gemini models:
449 |
450 | ```php
451 | $response = $container->get('gemini')->models()->list();
452 |
453 | $response->models;
454 | //[
455 | // [0] => Gemini\Data\Model Object
456 | // (
457 | // [name] => models/gemini-pro
458 | // [version] => 001
459 | // [displayName] => Gemini Pro
460 | // [description] => The best model for scaling across a wide range of tasks
461 | // ...
462 | // )
463 | // [1] => Gemini\Data\Model Object
464 | // (
465 | // [name] => models/gemini-pro-vision
466 | // [version] => 001
467 | // [displayName] => Gemini Pro Vision
468 | // [description] => The best image understanding model to handle a broad range of applications
469 | // ...
470 | // )
471 | // [2] => Gemini\Data\Model Object
472 | // (
473 | // [name] => models/embedding-001
474 | // [version] => 001
475 | // [displayName] => Embedding 001
476 | // [description] => Obtain a distributed representation of a text.
477 | // ...
478 | // )
479 | //]
480 | ```
481 |
482 | #### Get Model
483 | Get information about a model, such as version, display name, input token limit, etc.
484 | ```php
485 |
486 | $response = $container->get('gemini')->models()->retrieve('models/gemini-2.5-pro-preview-05-06');
487 |
488 | $response->model;
489 | //Gemini\Data\Model Object
490 | //(
491 | // [name] => models/gemini-2.5-pro-preview-05-06
492 | // [version] => 2.5-preview-05-06
493 | // [displayName] => Gemini 2.5 Pro Preview 05-06
494 | // [description] => Preview release (May 6th, 2025) of Gemini 2.5 Pro
495 | // ...
496 | //)
497 | ```
498 |
499 | ## Testing
500 |
501 | The package provides a fake implementation of the `Gemini\Client` class that allows you to fake the API responses.
502 |
503 | To test your code ensure you swap the `Gemini\Client` class with the `Gemini\Testing\ClientFake` class in your test case.
504 |
505 | The fake responses are returned in the order they are provided while creating the fake client.
506 |
507 | 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.
508 |
509 | ```php
510 | use Gemini\Testing\ClientFake;
511 | use Gemini\Responses\GenerativeModel\GenerateContentResponse;
512 |
513 | $container->get('gemini')->fake([
514 | GenerateContentResponse::fake([
515 | 'candidates' => [
516 | [
517 | 'content' => [
518 | 'parts' => [
519 | [
520 | 'text' => 'success',
521 | ],
522 | ],
523 | ],
524 | ],
525 | ],
526 | ]),
527 | ]);
528 |
529 | $result = $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->generateContent('test');
530 |
531 | expect($result->text())->toBe('success');
532 | ```
533 |
534 | In case of a streamed response you can optionally provide a resource holding the fake response data.
535 |
536 | ```php
537 | use Gemini\Testing\ClientFake;
538 | use Gemini\Responses\GenerativeModel\GenerateContentResponse;
539 |
540 | $container->get('gemini')->fake([
541 | GenerateContentResponse::fakeStream(),
542 | ]);
543 |
544 | $result = $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->streamGenerateContent('Hello');
545 |
546 | expect($response->getIterator()->current())
547 | ->text()->toBe('In the bustling city of Aethelwood, where the cobblestone streets whispered');
548 | ```
549 |
550 | After the requests have been sent there are various methods to ensure that the expected requests were sent:
551 |
552 | ```php
553 | use Gemini\Resources\GenerativeModel;
554 | use Gemini\Resources\Models;
555 |
556 | // assert list models request was sent
557 | $container->get('gemini')->models()->assertSent(callback: function ($method) {
558 | return $method === 'list';
559 | });
560 | // or
561 | $container->get('gemini')->assertSent(resource: Models::class, callback: function ($method) {
562 | return $method === 'list';
563 | });
564 |
565 | $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->assertSent(function (string $method, array $parameters) {
566 | return $method === 'generateContent' &&
567 | $parameters[0] === 'Hello';
568 | });
569 | // or
570 | $container->get('gemini')->assertSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash', callback: function (string $method, array $parameters) {
571 | return $method === 'generateContent' &&
572 | $parameters[0] === 'Hello';
573 | });
574 |
575 |
576 | // assert 2 generative model requests were sent
577 | $container->get('gemini')->assertSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash', callback: 2);
578 | // or
579 | $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->assertSent(2);
580 |
581 | // assert no generative model requests were sent
582 | $container->get('gemini')->assertNotSent(resource: GenerativeModel::class, model: 'gemini-2.0-flash');
583 | // or
584 | $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->assertNotSent();
585 |
586 | // assert no requests were sent
587 | $container->get('gemini')->assertNothingSent();
588 | ```
589 |
590 | To write tests expecting the API request to fail you can provide a `Throwable` object as the response.
591 |
592 | ```php
593 | use Gemini\Exceptions\ErrorException;
594 |
595 | $container->get('gemini')->fake([
596 | new ErrorException([
597 | 'message' => 'The model `gemini-basic` does not exist',
598 | 'status' => 'INVALID_ARGUMENT',
599 | 'code' => 400,
600 | ]),
601 | ]);
602 |
603 | // the `ErrorException` will be thrown
604 | $container->get('gemini')->generativeModel(model: 'gemini-2.0-flash')->generateContent('test');
605 | ```
606 |
--------------------------------------------------------------------------------
/art/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google-gemini-php/symfony/ac7c14999b941fab8d251e288711b01cab2b6af2/art/example.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-gemini-php/symfony",
3 | "description": "Symfony Bundle for Gemini",
4 | "keywords": ["symfony","php", "gemini", "sdk", "api", "client", "natural", "language", "processing"],
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Fatih AYDIN",
10 | "email": "aydinfatih52@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.1.0",
15 | "google-gemini-php/client": "^2.0",
16 | "nyholm/psr7": "^1.8.1",
17 | "psr/http-client": "^1.0.3",
18 | "psr/http-factory": "^1.0.2",
19 | "symfony/config": "^5.4|^6.3|^7.0",
20 | "symfony/dependency-injection": "^5.4|^6.3|^7.0.1",
21 | "symfony/http-client": "^5.4|^6.3|^7.0",
22 | "symfony/http-kernel": "^5.4|^6.3|^7.0.1"
23 | },
24 | "require-dev": {
25 | "laravel/pint": "^1.13.6",
26 | "phpstan/phpstan": "^1.10.47",
27 | "symfony/phpunit-bridge": "^5.4|^6.3|^7.0.1"
28 | },
29 | "autoload": {
30 | "psr-4": {
31 | "Gemini\\Symfony\\": "src/"
32 | }
33 | },
34 | "autoload-dev": {
35 | "psr-4": {
36 | "Gemini\\Symfony\\Tests\\": "tests/"
37 | }
38 | },
39 | "minimum-stability": "dev",
40 | "prefer-stable": true,
41 | "config": {
42 | "sort-packages": true,
43 | "preferred-install": "dist",
44 | "allow-plugins": {
45 | "php-http/discovery": false
46 | }
47 | },
48 | "scripts": {
49 | "lint": "pint -v",
50 | "test:lint": "pint --test -v",
51 | "test:types": "phpstan analyse --ansi",
52 | "test:unit": "simple-phpunit --colors=always",
53 | "test": [
54 | "@test:lint",
55 | "@test:types",
56 | "@test:unit"
57 | ]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: max
3 | paths:
4 | - src
5 |
6 | reportUnmatchedIgnoredErrors: true
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 | ./tests
19 |
20 |
21 |
22 |
23 | ./src
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
24 |
25 | assert($rootNode instanceof ArrayNodeDefinition);
26 |
27 | $children = $rootNode->children();
28 |
29 | assert($children instanceof NodeBuilder);
30 |
31 | $children->scalarNode('api_key')->defaultValue('%env(GEMINI_API_KEY)%')->end();
32 |
33 | return $treeBuilder;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/DependencyInjection/GeminiExtension.php:
--------------------------------------------------------------------------------
1 | > $configs
22 | */
23 | public function load(array $configs, ContainerBuilder $container): void
24 | {
25 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
26 | $loader->load('services.php');
27 |
28 | $configuration = $this->getConfiguration($configs, $container);
29 |
30 | assert($configuration instanceof ConfigurationInterface);
31 |
32 | $config = $this->processConfiguration($configuration, $configs);
33 |
34 | $definition = $container->getDefinition(Factory::class);
35 | $definition->addMethodCall('withApiKey', [$config['api_key']]);
36 |
37 | if (isset($config['base_url']) && ! is_string($config['base_url'])) {
38 | throw new InvalidArgumentException('Invalid Gemini API base URL.');
39 | }
40 |
41 | if (! empty($config['base_url'])) {
42 | $definition->addMethodCall('withBaseUrl', [$config['base_url']]);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/GeminiBundle.php:
--------------------------------------------------------------------------------
1 | services()
14 | ->set('gemini.http_client', Psr18Client::class)
15 | ->arg(0, service('http_client'))
16 |
17 | ->set(Factory::class)
18 | ->factory([Gemini::class, 'factory'])
19 | ->call('withHttpClient', [service('gemini.http_client')])
20 |
21 | ->set(Client::class)
22 | ->factory([service(Factory::class), 'make'])
23 | ->alias('gemini', Client::class);
24 | };
25 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/GeminiExtensionTest.php:
--------------------------------------------------------------------------------
1 | 200,
26 | 'response_headers' => [
27 | 'content-type' => 'application/json',
28 | ],
29 | ]);
30 | });
31 |
32 | $container = new ContainerBuilder;
33 | $container->set('http_client', $httpClient);
34 |
35 | $extension = new GeminiExtension;
36 | $extension->load([
37 | 'gemini' => [
38 | 'api_key' => '123456789',
39 | ],
40 | ], $container);
41 |
42 | $gemini = $container->get('gemini');
43 | self::assertInstanceOf(Client::class, $gemini);
44 |
45 | $response = $gemini->models()->list();
46 | self::assertCount(1, $response->models);
47 | self::assertSame([
48 | 'models' => [
49 | [
50 | 'name' => 'models/chat-bison-001',
51 | 'version' => '001',
52 | 'displayName' => 'Chat Bison',
53 | 'description' => 'Chat-optimized generative language model.',
54 | 'inputTokenLimit' => 4096,
55 | 'outputTokenLimit' => 1024,
56 | 'supportedGenerationMethods' => [
57 | 'generateMessage',
58 | 'countMessageTokens',
59 | ],
60 | 'baseModelId' => null,
61 | 'temperature' => 0.25,
62 | 'maxTemperature' => null,
63 | 'topP' => 0.95,
64 | 'topK' => 40,
65 | ],
66 | ],
67 | 'nextPageToken' => null,
68 | ], $response->toArray());
69 | }
70 | }
71 |
--------------------------------------------------------------------------------