Introduction to LLMs in Modern PHP & Laravel Architecture
Artificial Intelligence is no longer restricted to Python data science pipelines. Modern web applications require enterprise backend systems—built on languages like PHP and frameworks like Laravel—to seamlessly integrate Large Language Models (LLMs) for automation, data extraction, and intelligent decision-making.
However, raw text generation from LLMs is notoriously unpredictable. Developers frequently struggle with unparsed markdown blocks, hallucinations, and changing JSON keys. To build robust production applications, we must leverage Structured Outputs and Function Calling (Tool Use).
In this comprehensive architectural guide, we explore how to build a production-grade LLM service layer inside a Laravel Custom Web Application, guaranteeing strict type safety and reliable backend executions.
Understanding Structured Outputs vs. Traditional Prompting
Traditional prompting asks the model nicely to return JSON. In practice, models often prepend conversational filler or omit required fields. Structured outputs enforce schema adherence at the inference layer.
| Approach | Reliability | Type Safety | Error Handling Overhead |
|---|---|---|---|
| Standard Prompting | Low (~70-80%) | None (Parsed via regex/json_decode) | High (frequent retries needed) |
| JSON Mode | Medium (~95%) | Partial (Guarantees JSON, not schema) | Medium |
| Strict JSON Schema Outputs | 100% Guaranteed | Full (Strict adherence) | Low (Type-safe DTO mapping) |
Setting Up the Laravel LLM Service Layer
To keep our controller logic clean and adhere to solid software engineering principles, we will encapsulate our API interaction inside a dedicated service class: App\Services\LLMService.
First, configure your API credentials inside your .env file:
OPENAI_API_KEY=sk-proj-your-production-api-key-here
OPENAI_MODEL=gpt-4o-mini
Next, create the service class using Laravel's HTTP client to enforce structured schemas and parse incoming payloads securely.
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
class LLMService
{
protected string $apiKey;
protected string $model;
public function __construct()
{
$this->apiKey = config('services.openai.key');
$this->model = config('services.openai.model', 'gpt-4o-mini');
}
public function analyzeCustomerFeedback(string $feedback): array
{
$resp>apiKey)
->timeout(30)
->post('https://api.openai.com/v1/chat/completions', [
'model' => $this->model,
'messages' => [
[
'role' => 'system',
'content' => 'You are an AI assistant that analyzes customer support tickets and extracts sentiment and action items.'
],
[
'role' => 'user',
'content' => $feedback
]
],
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'name' => 'feedback_analysis',
'strict' => true,
'schema' => [
'type' => 'object',
'properties' => [
'sentiment' => [
'type' => 'string',
'enum' => ['positive', 'neutral', 'negative']
],
'urgency_score' => [
'type' => 'integer',
'description' => 'Scale from 1 to 5'
],
'summary' => [
'type' => 'string'
],
'tags' => [
'type' => 'array',
'items' => ['type' => 'string']
]
],
'required' => ['sentiment', 'urgency_score', 'summary', 'tags'],
'additionalProperties' => false
]
]
]
]);
if ($response->failed()) {
throw new RequestException($response);
}
$c
return json_decode($content, true);
}
}
Architectural Pro-Tip: Always set
'additionalProperties' => falseinside your JSON schemas. This prevents LLMs from hallucinating extra fields and guarantees your DTOs or arrays will not receive unhandled data keys.
Implementing Function Calling in PHP
Function calling allows the LLM to understand when it needs external data or actions. Instead of executing arbitrary code, the model outputs a structured JSON instruction telling your PHP backend which function to invoke and with what arguments.
Let's define a tool for checking real-time database inventories or user subscription statuses.
namespace App\Services;
use Illuminate\Support\Facades\Http;
class LLMToolService
{
public function processWithTools(string $userPrompt): array
{
$apiKey = config('services.openai.key');
$tools = [
[
'type' => 'function',
'function' => [
'name' => 'get_user_billing_status',
'description' => 'Retrieve the current billing and subscription tier of a registered user by email.',
'parameters' => [
'type' => 'object',
'properties' => [
'email' => [
'type' => 'string',
'description' => 'The user email address'
]
],
'required' => ['email'],
'additionalProperties' => false
]
]
]
];
$resp>post('https://api.openai.com/v1/chat/completions', [
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'user', 'content' => $userPrompt]
],
'tools' => $tools,
'tool_choice' => 'auto'
]);
$message = $response->json('choices.0.message');
// Check if model decided to call a function
if (isset($message['tool_calls'])) {
return $this->handleToolCalls($message['tool_calls']);
}
return ['response' => $message['content']];
}
protected function handleToolCalls(array $toolCalls): array
{
$results = [];
foreach ($toolCalls as $toolCall) {
$functi
$arguments = json_decode($toolCall['function']['arguments'], true);
if ($functi 'get_user_billing_status') {
// Execute local service logic
$results[] = [
'tool_call_id' => $toolCall['id'],
'output' => json_encode([
'email' => $arguments['email'],
'status' => 'active',
'tier' => 'Enterprise'
])
];
}
}
return $results;
}
}
Handling Asynchronous Processing with Laravel Queues
LLM requests can sometimes take between 2 to 8 seconds depending on token length and provider traffic. Performing these inside synchronous web requests can severely degrade Website Speed & Core Web Vitals.
To maintain high throughput, dispatch your AI tasks to background workers. For advanced queue tuning references, consult our guide on Mastering Laravel Queue Workers.
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\LLMService;
use App\Models\SupportTicket;
class ProcessTicketAIJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public SupportTicket $ticket)
{
}
public function handle(LLMService $llmService): void
{
$analysis = $llmService->analyzeCustomerFeedback($this->ticket->body);
$this->ticket->update([
'sentiment' => $analysis['sentiment'],
'urgency_score' => $analysis['urgency_score'],
'ai_summary' => $analysis['summary'],
'ai_tags' => $analysis['tags'],
'processed_at' => now(),
]);
}
}
Conclusion and Next Steps
Integrating Large Language Models into PHP and Laravel using strict structured outputs and function calling transforms AI from an unpredictable toy into a precise, dependable backend component. By pairing strict JSON schemas with Laravel's powerful service container and queue architecture, developers can build scalable, AI-driven applications with confidence.
Ready to supercharge your web architecture with custom AI tools and high-performance engineering? Contact Tech Aditya today or explore our Web Engineering Services to discuss your project requirements.