Laravel Daily's

AI Image Generation in Laravel: Generate, Store, and Serve Images with the AI SDK

Bright illustrative Laravel pipeline showing AI image generation, PHP, cloud storage, and a marketplace product preview

AI-generated product imagery fits naturally into Laravel applications. You can generate an image from a prompt, store it through any Laravel filesystem driver, and serve it from a Blade view or API response.

The Laravel AI SDK provides one expressive interface for image providers such as OpenAI, Gemini, and xAI. It also connects cleanly with familiar Laravel features, including configuration, queues, filesystem disks, testing, and error handling.

This tutorial builds a marketplace workflow. A product receives an AI-generated listing image, the image is stored on a public disk, and the resulting URL is returned to the frontend.

Laravel is a productive php web framework because these application concerns share the same conventions. You can focus on the feature instead of writing provider-specific HTTP clients and storage adapters.

Set up the Laravel AI SDK

Install the SDK with Composer:

composer require laravel/ai

Publish the package configuration:

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"

Add the credentials for your selected providers to .env:

OPENAI_API_KEY=
GEMINI_API_KEY=
XAI_API_KEY=

The SDK reads provider credentials and default models from config/ai.php. The current documentation includes the complete Laravel AI SDK configuration reference.

For this example, keep the image provider configurable at the application level:

AI_IMAGE_PROVIDER=gemini
AI_IMAGE_MODEL=

Add those values to config/services.php:

'ai_image' => [
    'provider' => env('AI_IMAGE_PROVIDER', 'gemini'),
    'model' => env('AI_IMAGE_MODEL'),
],

This lets you change providers between environments without changing application code.

Bright cartoony illustration showing square, portrait, and landscape image cards connected to Laravel and PHP

Generate a product image from a prompt

The SDK exposes image generation through Laravel\Ai\Image. Start with a prompt, select an aspect ratio and quality, then call generate().

use Laravel\Ai\Image;

$image = Image::of(
    'A premium ceramic coffee mug for an online marketplace listing, '.
    'matte white finish, subtle blue accent, isolated on a clean white background, '.
    'soft studio lighting, realistic product photography, no text, no logos'
)
    ->square()
    ->quality('high')
    ->timeout(120)
    ->generate();

The response contains the generated image and provider metadata. Casting the response to a string returns the raw image contents:

$contents = (string) $image;

For a marketplace listing, square() usually works well. The SDK also provides:

  • square() for 1:1 product cards and thumbnails.
  • portrait() for editorial layouts, mobile screens, and profile-style images.
  • landscape() for banners, hero sections, and video thumbnails.
  • quality('low'), quality('medium'), or quality('high') for controlling output detail and cost.

You can select a provider for a single request:

$image = Image::of('A modern desk lamp in a bright studio setting')
    ->landscape()
    ->quality('medium')
    ->generate('openai');

Or use the provider configured for your application:

$image = Image::of($prompt)
    ->square()
    ->quality('high')
    ->generate(config('services.ai_image.provider'));

If you configure a model, pass it as the second argument:

$image = Image::of($prompt)->generate(
    config('services.ai_image.provider'),
    config('services.ai_image.model')
);

Provider capabilities and supported models can change. Check the AI SDK image documentation before selecting a model for production.

Use a reference image with attachments

Text prompts are useful for new product concepts. Existing product photos are better when the generated image must preserve a shape, color, or composition.

Use Laravel\Ai\Files\Image to attach a stored reference image:

use Laravel\Ai\Files\Image as ImageAttachment;
use Laravel\Ai\Image;

$image = Image::of(
    'Create a polished marketplace product image from the attached photo. '.
    'Preserve the product shape and color. Remove the background. '.
    'Use soft studio lighting and a clean white backdrop.'
)
    ->attachments([
        ImageAttachment::fromStorage('uploads/products/mug-original.jpg'),
    ])
    ->square()
    ->quality('high')
    ->generate('gemini');

You can create image attachments from several sources:

ImageAttachment::fromStorage('products/mug.jpg');
ImageAttachment::fromPath('/var/www/app/product.jpg');
ImageAttachment::fromUrl('https://example.com/product.jpg');
ImageAttachment::fromUpload($request->file('image'));

For queued work, use a local path or stored file. Remote uploads and request objects do not make reliable queue payloads.

Store the generated image

Generated responses integrate with Laravel’s filesystem abstraction. You can store the image on the default disk:

$path = $image->store('products');

Pass a disk name when the application uses a specific driver:

$path = $image->store('products', 'public');

The same API works with S3, local storage, and other configured filesystem drivers. See the Laravel filesystem documentation for disk configuration.

Use storeAs() when the filename must be predictable:

$path = $image->storeAs(
    'products',
    'ceramic-coffee-mug.png',
    'public'
);

For a public marketplace image, use storePublicly():

$path = $image->storePublicly('products', 'public');

You can also control both the path and filename:

$path = $image->storePubliclyAs(
    'products',
    'ceramic-coffee-mug.png',
    'public'
);

The methods return the stored path, not a complete URL. That distinction matters when you switch from local development to object storage.

Bright illustrative diagram showing an AI-generated product image moving through Laravel filesystem storage into a browser storefront

Build a product image endpoint

A controller can combine generation and storage in one workflow:

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Ai\Image;
use Throwable;

class ProductImageController extends Controller
{
    public function store(Product $product): JsonResponse
    {
        $prompt = sprintf(
            'A realistic marketplace product photo of %s. '.
            'Clean white background, soft studio lighting, centered composition, no text.',
            $product->name
        );

        try {
            $image = Image::of($prompt)
                ->square()
                ->quality('high')
                ->timeout(120)
                ->generate(config('services.ai_image.provider'));

            $filename = Str::slug($product->name).'-'.$product->id.'.png';

            $path = $image->storePubliclyAs(
                'products',
                $filename,
                'public'
            );

            $product->update([
                'image_path' => $path,
            ]);

            return response()->json([
                'path' => $path,
                'url' => Storage::disk('public')->url($path),
            ]);
        } catch (Throwable $exception) {
            report($exception);

            return response()->json([
                'message' => 'The product image could not be generated.',
            ], 502);
        }
    }
}

The route can be defined as follows:

use App\Http\Controllers\ProductImageController;
use Illuminate\Support\Facades\Route;

Route::post(
    '/products/{product}/image',
    [ProductImageController::class, 'store']
);

The try block handles provider failures, timeouts, invalid configuration, and filesystem errors. In production, log the provider, product ID, and request correlation ID. Avoid logging sensitive prompts or uploaded image contents.

Serve the stored image

For a Blade view, generate the URL from the same disk:

@if ($product->image_path)
    <img
        src="{{ Storage::disk('public')->url($product->image_path) }}"
        alt="{{ $product->name }}"
    >
@endif

If you are building an API, return the URL with the product resource:

return [
    'id' => $product->id,
    'name' => $product->name,
    'image_url' => $product->image_path
        ? Storage::disk('public')->url($product->image_path)
        : null,
];

This is also a practical pattern when you need to build REST API with PHP. The API stores a stable path while the filesystem disk determines how the client accesses the asset.

For private disks, do not expose a permanent public URL. Generate a temporary URL instead:

$url = Storage::disk('s3')->temporaryUrl(
    $product->image_path,
    now()->addMinutes(10)
);

Use public visibility only when the image is intended for anonymous access.

Move expensive generation to a queue

High-quality image generation can take longer than a normal HTTP request. It can also consume more provider credits. Laravel’s queue system keeps the request responsive while a worker performs the generation in the background.

You can use the SDK’s built-in queue method for a small workflow:

use Laravel\Ai\Image;
use Laravel\Ai\Responses\ImageResponse;

Image::of('A bright product photo of a handmade leather wallet')
    ->portrait()
    ->quality('high')
    ->queue()
    ->then(function (ImageResponse $image) {
        $path = $image->storePublicly('products', 'public');

        // Persist $path or dispatch a follow-up job.
    })
    ->catch(function (Throwable $exception) {
        report($exception);
    });

For marketplace listings, a dedicated job offers better control over retries and database updates.

namespace App\Jobs;

use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Ai\Image;
use Throwable;

class GenerateProductImage implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public int $timeout = 180;

    public function __construct(
        public Product $product,
    ) {}

    public function handle(): void
    {
        $prompt = sprintf(
            'A realistic marketplace product photo of %s. '.
            'Centered on a clean white background, soft studio lighting, '.
            'accurate proportions, no text, no watermark.',
            $this->product->name
        );

        $image = Image::of($prompt)
            ->square()
            ->quality('high')
            ->timeout(150)
            ->generate(config('services.ai_image.provider'));

        $filename = Str::slug($this->product->name)
            .'-'.$this->product->id
            .'.png';

        $path = $image->storePubliclyAs(
            'products',
            $filename,
            'public'
        );

        $this->product->update([
            'image_path' => $path,
        ]);
    }

    public function failed(?Throwable $exception): void
    {
        report($exception);
    }
}

Dispatch the job after creating or updating the product:

GenerateProductImage::dispatch($product)
    ->onQueue('ai-images');

Start a worker for that queue:

php artisan queue:work --queue=ai-images --timeout=180

Keep the worker timeout below the queue connection’s retry_after value. Otherwise, a slow generation could be processed twice.

Bright queue illustration showing Laravel sending image generation work to workers, provider icons, retries, and a finished marketplace listing

Switch providers and add failover

Provider switching can happen through .env configuration:

AI_IMAGE_PROVIDER=openai

Your application code remains unchanged:

$image = Image::of($prompt)
    ->landscape()
    ->quality('medium')
    ->generate(config('services.ai_image.provider'));

For resilience, the SDK can fail over between supported image providers:

use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Image;

$image = Image::of($prompt)
    ->square()
    ->quality('medium')
    ->generate(provider: [
        Lab::Gemini,
        Lab::xAI,
    ]);

Failover is intended for provider availability problems, rate limits, and similar transient failures. It does not replace prompt validation or application-level error handling.

Test without calling an image provider

The AI SDK includes image fakes for tests:

use Laravel\Ai\Image;

Image::fake();

$response = Image::of('A blue ceramic mug')
    ->square()
    ->generate();

Image::assertGenerated(function ($prompt) {
    return $prompt->contains('blue ceramic mug')
        && $prompt->isSquare();
});

You can also assert queued image prompts:

Image::fake();

Image::of('A marketplace product image')
    ->portrait()
    ->queue();

Image::assertQueued(
    fn ($prompt) => $prompt->contains('marketplace')
        && $prompt->isPortrait()
);

This keeps provider costs out of your test suite. It also lets you verify that aspect ratios, prompts, and queue behavior remain stable as the application changes.

Among PHP developer tools, this kind of integration is valuable because the image workflow uses the same patterns as the rest of Laravel: configuration, filesystem disks, queues, testing, and dependency-free application code.

The result is a complete image pipeline. Generate with Image::of(), tune the output with aspect ratio and quality helpers, store it through Laravel’s filesystem, and queue slow work when the request should stay fast. Your provider can change later without forcing a rewrite of the marketplace feature.

Previous
Prefetching in Inertia 3.x: Make Your Laravel + Vue SPA Feel Instant
Next
State Management in Laravel + Vue + Inertia: Shared Props, usePage, and Pinia