Guides

Building Laravel with open-source tools

This guide outlines the architectural pattern for integrating AI services into a Laravel application using the Service Container. This approach ensures your LLM integrations are testable, swappable, and maintainable across large-scale SaaS projects.

45 minutes6 steps
1

Define the AI Service Contract

Create an interface to decouple your application logic from specific AI provider SDKs. This allows you to swap providers (e.g., from OpenAI to Claude) without modifying your business logic.

app/Contracts/AiServiceInterface.php
namespace App\Contracts;

interface AiServiceInterface
{
    public function prompt(string $system, string $user): string;
    public function stream(string $system, string $user): iterable;
}

⚠ Common Pitfalls

  • Defining provider-specific methods in the interface which defeats the purpose of abstraction.
  • Forgetting to handle return types for streaming responses.
2

Implement the Concrete Service

Implement the interface using a specific SDK. In this example, we use the OpenAI PHP client. Ensure you handle common API failures like timeouts or rate limits here.

app/Services/OpenAiService.php
namespace App\Services;

use App\Contracts\AiServiceInterface;
use OpenAI\Client;

class OpenAiService implements AiServiceInterface
{
    public function __construct(protected Client $client, protected string $model) {}

    public function prompt(string $system, string $user): string
    {
        $response = $this->client->chat()->create([
            'model' => $this->model,
            'messages' => [
                ['role' => 'system', 'content' => $system],
                ['role' => 'user', 'content' => $user],
            ],
        ]);

        return $response->choices[0]->message->content;
    }

    public function stream(string $system, string $user): iterable
    {
        return $this->client->chat()->createStreamed([
            'model' => $this->model,
            'messages' => [
                ['role' => 'system', 'content' => $system],
                ['role' => 'user', 'content' => $user],
            ],
        ]);
    }
}

⚠ Common Pitfalls

  • Hardcoding the model name instead of passing it via the constructor.
  • Neglecting to wrap API calls in try-catch blocks for production stability.
3

Register the Service in the Container

Bind your interface to the concrete implementation in a Service Provider. This centralizes configuration and allows Laravel to inject the service wherever it is needed.

app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(AiServiceInterface::class, function ($app) {
        $client = \OpenAI::client(config('services.openai.key'));
        return new OpenAiService($client, config('services.openai.model'));
    });
}

⚠ Common Pitfalls

  • Using 'bind' instead of 'singleton' for services that don't need to be re-instantiated multiple times in a single request.
  • Missing configuration validation which leads to null-pointer exceptions if the API key is missing.
4

Configure Environment Variables

Store sensitive credentials in your .env file and map them through config/services.php. Never commit raw API keys to version control.

config/services.php
// config/services.php
'openai' => [
    'key' => env('OPENAI_API_KEY'),
    'model' => env('OPENAI_MODEL', 'gpt-4-turbo'),
],

⚠ Common Pitfalls

  • Accessing env() directly in service classes instead of using config(), which breaks config caching.
5

Inject and Use the Service

Inject the interface into your Controllers or Livewire components. Laravel's auto-wiring will resolve the concrete implementation registered in Step 3.

app/Http/Controllers/AiController.php
namespace App\Http\Controllers;

use App\Contracts\AiServiceInterface;
use Illuminate\Http\Request;

class AiController extends Controller
{
    public function __invoke(Request $request, AiServiceInterface $ai)
    {
        $result = $ai->prompt(
            'You are a helpful assistant.',
            $request->input('message')
        );

        return response()->json(['data' => $result]);
    }
}
6

Implement Mocked Tests

Use Pest or PHPUnit to test your application without making real API calls. This saves costs and ensures tests run offline.

tests/Feature/AiTest.php
it('processes a prompt correctly', function () {
    $mock = Mockery::mock(AiServiceInterface::class);
    $mock->shouldReceive('prompt')->andReturn('Mocked response');
    
    $this->app->instance(AiServiceInterface::class, $mock);

    $response = $this->post('/ai-endpoint', ['message' => 'Hello']);

    $response->assertJson(['data' => 'Mocked response']);
});

⚠ Common Pitfalls

  • Testing the third-party SDK instead of your application's interaction with the service.
  • Leaving real API keys in the phpunit.xml file.

What you built

By following this architecture, you create a robust AI integration layer. You can now easily implement features like multi-tenancy (swapping keys per user) or A/B testing different LLM models simply by adjusting the Service Provider logic.