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.
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.
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.
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.
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.
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.
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.
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
'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.
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.
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]);
}
}Implement Mocked Tests
Use Pest or PHPUnit to test your application without making real API calls. This saves costs and ensures tests run offline.
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.