Laravel and AI features: a practical PHP & Laravel guide
Integrating LLM capabilities into your Laravel application has shifted from experimental territory to a practical requirement for many teams. Whether you're building content summarization, code analysis, or customer support automation, we've found that the right architecture prevents both runaway API costs and frustrated users waiting for responses. This guide walks through the patterns we use in production to safely and efficiently add AI features to Laravel apps.
Why AI in Laravel matters now
Six months ago, adding AI to a PHP application meant wrestling with unfamiliar Python integrations or complex external services. Today, Laravel's Http facade and job queue system make it straightforward to call OpenAI, Anthropic, or other LLM providers directly from your application code. The challenge isn't integration—it's doing it responsibly. Long API calls block requests, repeated calls drain budgets, and unvalidated JSON responses crash features downstream.
We've worked through these issues on trading platforms, SaaS tools, and e-commerce backends. The patterns in this guide are battle-tested on production workloads handling hundreds of AI requests daily.
HTTP client: Call AI APIs with Laravel's Http facade
Laravel's Http facade handles HTTP communication cleanly without external dependencies. For AI API calls, this is your entry point. Most LLM providers expose REST endpoints that accept JSON and return JSON—a perfect fit for the Http facade.
Here's a basic example calling OpenAI's completions API:
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.openai.key'),
])
->timeout(30)
->post('https://api.openai.com/v1/chat/completions', [
'model' => 'gpt-4',
'messages' => [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'Summarize this article: ' . $article],
],
'temperature' => 0.7,
'max_tokens' => 200,
]);
if ($response->successful()) {
$result = $response->json('choices.0.message.content');
} else {
// Handle error
}A few production considerations we've learned:
- Timeouts matter. AI API calls can take 10–30 seconds. Set explicit timeouts and don't rely on defaults. We use 30 seconds for most calls, 60 for long-form content.
- Retry logic. Network flakes happen. Use Laravel's retry helper or configure retry on the Http client itself to handle transient failures gracefully.
- Store credentials safely. Never hardcode API keys. Use environment variables and config files, just as you would database passwords.
- Handle rate limits. Most LLM providers rate-limit requests. The Http facade preserves response headers, so check for rate-limit headers and implement backoff strategies.
For production, wrap this logic in a dedicated service class:
namespace App\Services;
use Illuminate\Support\Facades\Http;
class OpenAiService
{
public function summarizeContent(string $content): string
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.openai.key'),
])
->timeout(30)
->retry(2, 100)
->post('https://api.openai.com/v1/chat/completions', [
'model' => 'gpt-4',
'messages' => [
['role' => 'system', 'content' => 'Summarize the following in 2-3 sentences.'],
['role' => 'user', 'content' => $content],
],
'max_tokens' => 150,
]);
if ($response->failed()) {
throw new \Exception('OpenAI API failed: ' . $response->status());
}
return $response->json('choices.0.message.content');
}
}This keeps your logic testable and reusable across controllers and jobs.
Queue long calls: Process AI work in jobs and notify users
Never call an LLM API directly in a web request. A 20-second OpenAI call will timeout or hang your user. Queue it instead.
Create a job to handle the AI work asynchronously:
namespace App\Jobs;
use App\Models\Article;
use App\Services\OpenAiService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SummarizeArticleJob implements ShouldQueue
{
use Queueable;
public function __construct(public Article $article) {}
public function handle(OpenAiService $service): void
{
try {
$summary = $service->summarizeContent($article->content);
$this->article->update([
'summary' => $summary,
'summary_generated_at' => now(),
]);
// Notify user via event, notification, or webhook
event(new ArticleSummarized($this->article));
} catch (\Exception $e) {
// Log the failure and optionally retry
\Log::error('Article summarization failed', [
'article_id' => $this->article->id,
'error' => $e->getMessage(),
]);
$this->fail($e);
}
}
}Dispatch the job when a user triggers the action—say, clicking a "Generate summary" button:
public function generateSummary(Article $article)
{
SummarizeArticleJob::dispatch($article);
return response()->json(['status' => 'Summarization in progress']);
}On the frontend, poll for completion or use WebSockets to update the user when the summary is ready. This pattern keeps your API responsive and scales naturally as your queue workers handle multiple AI tasks in parallel.
We've run this setup with Laravel Horizon for monitoring, which gives visibility into job success rates and execution times. Essential for tracking down bottlenecks when AI features start consuming significant resources.
Store results: Cache and save outputs to avoid repeated costs
AI API calls cost money. Every repeated call is waste. Cache strategically at two levels: in-memory caching for frequent lookups and database storage for results tied to domain models.
For in-memory caching, use Laravel's cache with a meaningful TTL:
use Illuminate\Support\Facades\Cache;
public function summarizeContent(string $content): string
{
$cacheKey = 'summary:' . hash('sha256', $content);
return Cache::remember($cacheKey, now()->addDays(7), function () use ($content) {
return $this->openAiService->summarizeContent($content);
});
}This caches the result for 7 days. If the same content is summarized again, you hit the cache instead of the API. For trading platforms and SaaS tools with repeated queries, this cuts costs dramatically.
For database-level storage, tie results to your domain model:
Schema::create('article_summaries', function (Blueprint $table) {
$table->id();
$table->foreignId('article_id')->constrained()->cascadeOnDelete();
$table->text('summary');
$table->string('model_used')->default('gpt-4');
$table->integer('tokens_used')->nullable();
$table->timestamp('generated_at')->useCurrent();
$table->timestamp('cached_until')->nullable();
$table->timestamps();
$table->index('generated_at');
});
// In your Article model
public function summary()
{
return $this->hasOne(ArticleSummary::class);
}Store metadata—tokens used, model version, generation time—alongside results. This becomes invaluable for cost analysis and debugging why results changed between model versions.
Combine both strategies: check the cache first, then the database, then call the API. This pattern has reduced our per-user AI costs by 60–70% in production.
Validate output: Parse JSON into DTOs before using it
LLM outputs are unpredictable. A model might return malformed JSON, omit expected fields, or hallucinate data. Never trust raw API responses. Validate and parse them into typed data transfer objects (DTOs).
Define a DTO for your expected structure:
namespace App\Data;
class ArticleSummaryData
{
public function __construct(
public string $summary,
public array $keyPoints,
public string $sentiment,
) {}
public static function fromApiResponse(array $data): self
{
return new self(
summary: $data['summary'] ?? throw new \InvalidArgumentException('Missing summary'),
keyPoints: $data['key_points'] ?? [],
sentiment: $data['sentiment'] ?? 'neutral',
);
}
}Parse the API response and validate it before using it downstream:
public function summarizeContent(string $content): ArticleSummaryData
{
$response = $this->callOpenAi($content);
try {
$json = json_decode($response, associative: true, flags: JSON_THROW_ON_ERROR);
return ArticleSummaryData::fromApiResponse($json);
} catch (\JsonException $e) {
\Log::error('Invalid JSON from AI API', ['response' => $response]);
throw new \Exception('AI response parsing failed');
}
}This approach catches bad data early before it corrupts your database or breaks downstream logic. We've seen models fail silently on edge cases—a robust validation layer catches these before users notice.
Common mistakes
Calling AI APIs synchronously in web requests. Every second adds up. A 25-second API call in a web request will timeout or create a poor user experience. Always queue.
Ignoring cost implications. Without caching and deduplication, AI features become expensive quickly. Token tracking and result reuse are not optional in production.
Trusting API responses without validation. JSON parsing failures, missing fields, and unexpected data types will crash your app if you don't validate. Use DTOs and strict parsing.
Not handling API failures gracefully. Rate limits, network issues, and model degradation happen. Implement retry logic, fallback responses, and proper logging. Users should never see an unhandled 500 error from a failed AI call.
Leaving API keys in code or version control. Use environment variables and never commit credentials. Rotate keys regularly, especially if you find one in your git history.
Key takeaways
- Use Laravel's Http facade to call AI APIs directly—no external dependencies needed for basic integrations.
- Always queue AI API calls via jobs to keep web requests fast and responsive.
- Cache results aggressively at both cache and database levels to reduce API costs.
- Validate and parse API responses into typed DTOs before using them in your application logic.
- Implement proper error handling, logging, and monitoring—AI features are another system that can fail, and you need visibility into those failures.
- Store metadata alongside results (tokens used, model version, generation time) for cost tracking and debugging.






Comments
Loading comments…