Now that we have our service set up, let's implement the core method: generateImage(). This method will take a user input string, format it into a detailed prompt using our PromptManager, send the request to the Gemini image generation API, and store the resulting image using our ImageManager.
Here's the implementation:
public function generateImage(string $userInput): string
{
$prompt = $this->promptManager->formatPrompt($userInput);
$url = sprintf("%sv1beta/models/gemini-3.1-flash-image:generateContent?key=%s", $this->baseUrl, $this->apiKey);
try {
$response = $this->httpClient->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'contents' => [
[
'parts' => [
['text' => $prompt],
],
]
],
'generationConfig' => [
'responseModalities' => ['TEXT', 'IMAGE'],
'imageConfig' => [
'aspectRatio' => '1:1',
],
],
],
]);
$statusCode = $response->getStatusCode();
if ($statusCode !== 200) {
throw new \RuntimeException("API returned status code $statusCode");
}
$content = $response->toArray();
$imageBase64 = null;
foreach ($content['candidates'][0]['content']['parts'] ?? [] as $part) {
if (isset($part['inlineData']['data'])) {
$imageBase64 = $part['inlineData']['data'];
break;
}
}
if ($imageBase64 === null) {
throw new \RuntimeException("Unexpected API response format");
}
return $this->imageManager->addImage($prompt, $imageBase64);
} catch (\Exception $e) {
throw new \RuntimeException("Error generating image: " . $e->getMessage());
}
}
Let's break down what's happening in this method:
-
We start by calling formatPrompt() to convert the user's input into a detailed prompt using our predefined template.
-
We then construct the URL for the Gemini image generation API, including our API key as a query parameter. Gemini supports passing the API key as a ?key= query parameter, which is what we use here for the demo. Be aware that URLs are commonly logged by servers and proxies; where an endpoint supports it, a header-based credential is preferable.
-
We make the API call using Symfony's HTTP client, which takes several parameters:
- Headers: We specify that we're sending JSON data.
- JSON payload: We wrap the prompt in a
contents array and include a generationConfig requesting both text and image response modalities, along with image configuration such as the aspect ratio.
-
We check the status code of the response to ensure the request was successful.
-
We parse the response and walk the returned parts, extracting the first part that contains inline image data (inlineData.data), which holds the base64-encoded image.
-
Finally, we pass the prompt and image data to our ImageManager's addImage() method, which stores it and returns the base64 string.
To understand why the extraction loop is written the way it is, here is a minimal example of the kind of response generateContent returns:
{
"candidates": [
{
"content": {
"parts": [
{ "text": "Here is your banner." },
{ "inlineData": { "mimeType": "image/png", "data": "<base64>" } }
]
}
}
]
}
Notice that parts can contain a text part and an image part. That's why we loop through the parts and pick the first one that has inlineData.data, rather than assuming the image is always at a fixed position.
The method returns the base64-encoded image data, which can be used directly in web applications (for example, to display the image in an HTML <img> tag).