Laravel OpenAI integration is not simpler than Claude integration. The APIs differ, the model families differ — but the architectural requirements are nearly identical once you care about production stability.
Laravel works well here because it already solves the problems AI integrations create: dependency boundaries, environment isolation, background work, failure handling, and observability. You’re not fighting the framework. You’re leaning on it.
On model names: This guide uses
gpt-4oandgpt-4o-minias examples. These are current as of early 2026. OpenAI’s model catalogue evolves quickly — always verify against OpenAI’s official models reference before deploying.gpt-3.5-turbois deprecated;gpt-4o-miniis its cost-effective replacement.
Understanding the OpenAI API Surface
OpenAI’s platform has evolved faster than its documentation. Many tutorials conflate multiple API generations or mix incompatible approaches.
For Laravel applications, think in terms of three distinct capabilities: the Chat Completions API for text generation and reasoning, the Embeddings API for semantic search and retrieval, and the Audio/Vision APIs for transcription and multimodal inputs.
OpenAI is not “one API”. Your Laravel architecture should reflect that separation early. Controllers that call the Completions and Embeddings endpoints directly will become unmaintainable fast.
Foundations
Authentication and Environment Configuration
OpenAI uses API keys issued at the account or project level. Treat them as production secrets — same as database credentials or payment tokens.
OPENAI_API_KEY=sk-... OPENAI_ORG_ID=optional
Never read these values directly from env() outside of configuration files. This is one of the most common structural mistakes we see in Laravel AI codebases. Expose them through a dedicated config file:
// config/openai.php
return [
'api_key' => env('OPENAI_API_KEY'),
'organization' => env('OPENAI_ORG_ID'),
'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
];
This centralizes OpenAI configuration and allows overrides for testing, proxying, or self-hosted gateways. Skipping this step is how vendor logic leaks across the codebase and makes rotation painful.
Security: Never commit API keys to git. Use Laravel’s encrypted environment handling in production, and rotate keys periodically. Treat a leaked OpenAI key with the same urgency as a leaked database password — it has a direct billing impact.
Why You Should Not Use the OpenAI SDK Directly
OpenAI provides official SDKs, including PHP community implementations. Injecting these directly into controllers or jobs is a mistake.
SDKs change. Model names change. Response formats evolve. Your application should not absorb that churn.
Access OpenAI through a service boundary you control:
class OpenAiClient
{
public function __construct(
protected HttpClient $http
) {}
}
And then:
class OpenAiService
{
public function __construct(
protected OpenAiClient $client
) {}
}
This prevents your controllers from becoming vendor adapters. It also makes the Laravel Service Container work for you — you can bind a mock in tests without touching a single controller.
HTTP Client Configuration and Timeouts
AI requests can be long-running, bursty, expensive, and sensitive to retries. A default Http::post() is insufficient. Configure a dedicated client with explicit behaviour:
Http::withHeaders([
'Authorization' => 'Bearer ' . config('openai.api_key'),
'Content-Type' => 'application/json',
])
->timeout(60)
->retry(0, 0);
The timeout(60) matters because OpenAI requests can legitimately take tens of seconds with large prompts or slow models. The retry(0, 0) matters even more — retrying AI requests blindly can double your cost and produce inconsistent outputs. If you need retries, implement them at the application level with full context awareness, not at the transport layer.
Error Handling as a First-Class Concern
OpenAI’s Chat Completions API fails in ways standard REST APIs rarely do: partial responses, rate limit throttling, model overload, malformed outputs despite 200 responses.
Your service layer must treat OpenAI responses as untrusted input, even when HTTP status codes indicate success.
$response = $this->client->post(...);
if (! $response->successful()) {
throw new OpenAiRequestFailed(
$response->status(),
$response->body()
);
}
But that’s only half the problem. You must also validate semantic correctness. Did the model return text when you expected JSON? Did it ignore system instructions? Did it hallucinate fields? That validation logic does not belong in a controller.
Service Architecture: Designing for Change
Your OpenAI service should not expose low-level API primitives unless absolutely necessary.
Instead of:
$openai->chatCompletion([...]);
Prefer intent-based methods:
$openai->generateText(...) $openai->summarize(...) $openai->extractStructuredData(...)
Internally, these methods may all use the same API endpoint. Externally, they express business intent. This gives you three architectural wins: prompts become versionable assets, you can A/B test models without touching callers, and you can migrate providers with minimal surface-area changes.
If you later introduce Claude alongside OpenAI — a decision many teams make as they want provider redundancy — this design pays for itself immediately. The service boundary is what makes multi-model routing tractable.
Dependency Injection and Testability
Every OpenAI-facing class should be injectable and mockable. No static helpers, no facades hiding HTTP calls, no env() reads at runtime.
In tests, you should be able to swap the OpenAI client with a fake implementation that returns deterministic responses. If you cannot do that without modifying production code, you are not integrating OpenAI — you are coupling your application to it.
Core Integration Patterns
Once your foundation is in place, OpenAI becomes a capability your application depends on. These patterns are not OpenAI-specific — they’re standard Laravel patterns applied to probabilistic systems.
Pattern 1: Simple Request–Response (One-Shot Tasks)
The simplest OpenAI interaction is still the most common: you send a prompt, you receive a response, the request lifecycle ends. Classification, extraction, summarization, content drafting — all one-shot.
Most tutorials implement this directly in controllers. Don’t.
Designing the Service Method
class OpenAiService
{
public function generateText(string $prompt): string
{
// implementation
}
}
Implementing the API Call
$response = $this->client->post('/chat/completions', [
'model' => 'gpt-4o',
'messages' => [
[
'role' => 'user',
'content' => $prompt,
],
],
]);
Model selection is explicit. Never rely on defaults — model choice is an architectural decision with cost and quality implications.
Extracting the Result Safely
$data = $response->json();
$content = data_get($data, 'choices.0.message.content');
if (! $content) {
throw new UnexpectedOpenAiResponse($data);
}
return $content;
Use data_get() here. Future models may return multiple content blocks, safety filters may suppress output, and partial responses can occur under load. Your service method should either return valid output or fail loudly — silent nulls are debugging nightmares.
Pattern 2: Streaming Responses (Long-Running Output)
Streaming is not about novelty. It’s about user experience under latency. Large outputs can take seconds. Streaming turns that wait into visible progress.
When Streaming Makes Sense
Use streaming when output length is unbounded and perceived latency matters. Avoid it when you need structured JSON output, must validate the full response before use, or results are consumed only by background jobs.
Server-Side Streaming with OpenAI
OpenAI supports streaming via Server-Sent Events (SSE). On the Laravel side:
return response()->stream(function () use ($prompt) {
$this->openAi->streamText($prompt, function ($chunk) {
echo "data: " . json_encode($chunk) . "\n\n";
ob_flush();
flush();
});
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
Implementing the Stream Method
⚠️ The
sinkOption Defeats Streaming EntirelyA common implementation mistake is using
->withOptions(['stream' => true, 'sink' => fopen('php://temp', 'w+')])in Laravel’s HTTP client. Thesinkoption tells Guzzle to buffer the entire response to that destination before returning. Yourwhile (!feof($handle))loop runs after the connection is already closed. You are not streaming — you are reading a completed buffer with extra steps. The UX benefits you expect don’t exist.For true streaming, use Guzzle directly with
stream => trueand read the PSR-7 body incrementally:
public function streamText(string $prompt, callable $onChunk): void
{
$client = new \GuzzleHttp\Client();
$response = $client->post('https://api.openai.com/v1/chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . config('openai.api_key'),
'Content-Type' => 'application/json',
],
'json' => [
'model' => 'gpt-4o',
'messages' => [['role' => 'user', 'content' => $prompt]],
'stream' => true,
],
'stream' => true,
]);
$body = $response->getBody();
$resource = $body->detach();
while (! feof($resource)) {
$line = rtrim(stream_get_line($resource, 4096, "\n"));
if (empty($line) || $line === 'data: [DONE]') {
continue;
}
if (str_starts_with($line, 'data: ')) {
$data = json_decode(substr($line, 6), true);
if (isset($data['choices'][0]['delta']['content'])) {
$onChunk($data['choices'][0]['delta']['content']);
}
}
}
}
Senior Dev Tip: If you need to decide between Livewire polling, SSE, and WebSockets for your AI UI, that architectural choice has more impact than the streaming implementation itself. We compared all three approaches in Livewire vs SSE vs WebSockets for AI UIs — worth reading before you commit.
Pattern 3: Conversation Memory (Multi-Turn Interactions)
Chatbots are one example. Multi-step reasoning, guided forms, iterative content refinement — all of these are multi-turn workflows. The key challenge is state management, not prompt engineering.
Representing Conversation State
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->json('context')->nullable();
$table->timestamps();
});
Schema::create('conversation_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id');
$table->string('role'); // 'user' or 'assistant'
$table->text('content');
$table->timestamps();
});
Rehydrating Context
Before sending a request, load the full message history via Eloquent and build the message array:
$messages = $conversation->messages()
->orderBy('created_at')
->get()
->map(fn ($msg) => [
'role' => $msg->role,
'content' => $msg->content,
])
->toArray();
Then append the new user input and send the full list to OpenAI. Large language models do not remember anything between API calls. Memory is entirely your responsibility. Persisting messages also lets you truncate context intelligently and summarize older turns when token counts grow large.
Pattern 4: Background Processing (Queued AI Work)
Not every AI task belongs in the request–response cycle. Bulk processing, document analysis, scheduled generation — these belong in queued jobs.
class GenerateSummaryJob implements ShouldQueue
{
public function handle(OpenAiService $openAi): void
{
$summary = $openAi->summarize($this->document);
$this->document->update(['summary' => $summary]);
}
}
Laravel’s queue system provides retry semantics, failure hooks, and concurrency control — all essential when working with a rate-limited, cost-based API. A background job that silently retries three times on a large document can triple your bill. Jobs must cap token usage, log cost metadata, and fail deterministically.
Connecting the Patterns
These four patterns combine in real applications: a Livewire UI streams output, backed by a conversation record, which occasionally offloads heavy steps to queues, all of which reuse the same OpenAI service layer. That consistency is the real win — one abstraction, composable in every direction.
Building a Document Q&A Feature
Chatbots are not the most interesting application of LLMs in production. The real leverage comes from augmenting your existing data — documents, records, internal knowledge — with natural language access.
We’ll implement a Document Q&A feature: a user uploads a document, the system extracts and stores its content, and the user can ask questions grounded strictly in that document’s content. This exercises nearly every core pattern: one-shot generation, multi-turn context, background processing, and defensive prompt design.
We deliberately skip embeddings and vector databases here. That’s a scoped decision, not a shortcut. The simpler architecture works today and degrades predictably when it eventually outgrows itself.
Problem Definition and Constraints
What we want: natural language questions over uploaded documents, answers limited to document content, reasonable performance for mid-sized files, and clear failure modes.
What we’re not solving yet: semantic search across large corpora, cross-document reasoning, or high-recall retrieval at scale. Many “RAG tutorials” prematurely introduce vector databases when a simpler architecture would suffice.
High-Level Architecture
Four stages: ingestion (document uploaded and stored), extraction (text extracted and normalized), context preparation (content transformed into prompt context), and question answering (user questions answered via OpenAI). Each maps cleanly to a Laravel responsibility.
Stage 1: Document Upload and Persistence
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('original_name');
$table->string('path');
$table->longText('content')->nullable();
$table->timestamps();
});
At upload time, do not call OpenAI:
public function store(Request $request): JsonResponse
{
$file = $request->file('document');
$path = $file->store('documents');
$document = Document::create([
'original_name' => $file->getClientOriginalName(),
'path' => $path,
]);
ExtractDocumentContent::dispatch($document);
return response()->json(['id' => $document->id]);
}
Dispatch a job. Extraction may be slow. Upload requests should return quickly. Extraction failures should be isolated — not cascade into upload failures.
Stage 2: Text Extraction as a Background Job
class ExtractDocumentContent implements ShouldQueue
{
public function handle(): void
{
$text = $this->extractText(
Storage::get($this->document->path)
);
$this->document->update([
'content' => $this->normalize($text),
]);
}
}
No AI calls, no tokens consumed. Failures are deterministic. You want extraction bugs to be boring.
Stage 3: Chunking Document Content
Here’s where many implementations fail. The naive approach — stuffing the entire document into a prompt — works briefly, then collapses under token limits and cost pressure.
Split the document into chunks based on size:
function chunkText(string $text, int $size = 800): array
{
$chunks = [];
$length = mb_strlen($text);
$offset = 0;
while ($offset < $length) {
$chunks[] = mb_substr($text, $offset, $size);
$offset += $size;
}
return $chunks;
}
⚠️ Gotcha —
str_split()Will Corrupt Multibyte TextThe PHP standard library’s
str_split($text, $size)splits by bytes, not characters. Any document with UTF-8 content — French accents, German umlauts, Arabic, CJK — will produce chunks with broken characters at boundaries. Always usemb_substr()andmb_strlen()for text chunking. This is a silent production bug that only surfaces with real-world content.
Stage 4: Answering Questions
public function answerQuestion(Document $document, string $question): string
{
$chunks = chunkText($document->content);
$prompt = $this->buildPrompt($chunks, $question);
return $this->openAi->generateText($prompt);
}
Prompt Construction
protected function buildPrompt(array $chunks, string $question): string
{
$context = implode("\n\n", $chunks);
return <<<PROMPT
You are answering questions based ONLY on the following document content.
If the answer cannot be found in the document, respond with:
"I don't know based on the provided document."
Document:
{$context}
Question:
{$question}
PROMPT;
}
This prompt constrains hallucination, defines a fallback response, and makes incorrect answers detectable. The goal is not perfection — it’s bounded failure. You’ll eventually outgrow this approach, but it fails gracefully, which makes it an excellent foundation.
Handling Multi-Turn Follow-Ups
Users rarely ask one question. Instead of re-sending the entire document every time, summarize prior context, append follow-ups as messages, and re-inject only relevant chunks. The conversation schema from Pattern 3 handles this directly.
Observability
Every OpenAI call in this feature should log document ID, token count, model, and latency. Not for analytics — for debugging. When a user reports “the AI gave a wrong answer,” you need the exact inputs that produced it. Without that log, you’re guessing.
Production Concerns
By the time an OpenAI integration reaches production, the question shifts from “Can we generate an answer?” to “How often does this fail? How expensive is it over time? What happens under load?”
AI integrations are probabilistic, rate-limited, and cost-based. Treating them like traditional REST APIs produces brittle systems.
Rate Limiting as a System Behaviour
OpenAI enforces rate limits at the account and model level. Most developers respond to sporadic 429s by adding retries. That’s the wrong mental model.
Rate limits are not an error condition. They’re a signal that your system’s demand exceeds its contract.
Centralizing Rate Awareness
Your OpenAI service layer should be the only place where rate-limit responses are interpreted:
if ($response->status() === 429) {
throw new OpenAiRateLimited(
retryAfter: (int) $response->header('Retry-After', 30)
);
}
This exception should not be caught silently. Its consumers — controllers, jobs — must decide what to do next.
Different Strategies for Different Contexts
User-facing requests should fail fast with a clear message or degraded experience. Background jobs can be delayed and rescheduled:
public function handle(): void
{
try {
// OpenAI call
} catch (OpenAiRateLimited $e) {
$this->release($e->retryAfter ?? 30);
}
}
This aligns retries with system capacity instead of guessing at backoff timing.
Cost as an Operational Metric
Token usage is not an abstract billing detail. It’s a runtime characteristic of your application. Two requests that look identical in code can differ by orders of magnitude in cost depending on prompt length, document size, conversation history, and model choice.
Recording Token Usage
Every OpenAI response includes token metadata. Capture it:
$usage = data_get($response->json(), 'usage');
OpenAiUsage::create([
'model' => $model,
'prompt_tokens' => $usage['prompt_tokens'] ?? 0,
'completion_tokens' => $usage['completion_tokens'] ?? 0,
'total_tokens' => $usage['total_tokens'] ?? 0,
]);
This data is not for dashboards at first. It’s for answering questions like: Why did costs spike yesterday? Which feature is expensive? Which users generate the most load? You cannot optimize what you do not measure.
Cost-Aware Design Decisions
Once usage is visible, architectural decisions become concrete: summarizing older conversation turns to stay within token budgets, truncating document context for cheaper models, and routing simple tasks to gpt-4o-mini instead of gpt-4o. These are the difference between sustainable and runaway costs.
Caching AI Outputs (Carefully)
Caching AI responses is tempting. It’s also dangerous if done naively.
What Is Safe to Cache
Caching works well for deterministic prompts, non-user-specific inputs, and expensive but repeatable operations — document summaries, extracted metadata, classification labels:
Cache::remember(
"doc:{$document->id}:summary",
now()->addDay(),
fn () => $openAi->summarize($document->content)
);
What Should Not Be Cached
Avoid caching conversational replies, user-personalized outputs, or anything derived from mutable state. If you cannot confidently answer “When should this be invalidated?”, don’t cache it.
Senior Dev Tip: Use Redis for AI output caching, not the file driver. Redis gives you atomic expiry, explicit invalidation by key pattern, and the ability to inspect what’s cached without reading from disk. File-based caches become unmanageable quickly when you’re caching per-document, per-user outputs.
Testing OpenAI Integrations Without Lying
This is where many teams get stuck. Mocking the OpenAI client to return a fixed string proves nothing useful. You need two distinct kinds of tests.
1. Contract Tests (Fast, Deterministic)
These verify that your service layer calls OpenAI correctly, that failures are handled, and that parsing logic works. Mocking is appropriate here:
Http::fake([
'api.openai.com/*' => Http::response([
'choices' => [
['message' => ['content' => 'Test output']],
],
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 5,
'total_tokens' => 15,
],
]),
]);
You’re testing plumbing, not intelligence.
2. Behavioural Tests (Slow, Real)
These hit the real OpenAI API, use real prompts, and validate constraints — not specific wording:
$response = $service->answerQuestion($doc, 'What is the refund policy?');
$this->assertStringContainsString('refund', strtolower($response));
These tests should run infrequently, be clearly labelled, use dedicated API keys, and have cost limits. They catch failures no mock ever will.
Dealing With Non-Determinism
AI output is not stable across runs. Tests must reflect that. Assert structure, presence or absence, and refusal behaviour. If a model is instructed to say “I don’t know” under certain conditions, test for that branch explicitly. You’re testing behavioural boundaries, not string values.
Deployment Considerations
Increase PHP and proxy timeouts for streaming routes. Ensure queue workers can handle long-running jobs. If you haven’t locked down your production environment yet, deploy Laravel to production covers the infrastructure baseline these integrations depend on. Separate API keys across local, staging, and production — never reuse them. Wrap major prompt changes behind feature flags; a prompt change can alter behaviour as dramatically as a code change.
At minimum, you should be able to answer: Which OpenAI calls failed today? Which features are most expensive? Which prompts changed recently? This doesn’t require a full observability stack. It requires discipline: structured logs, request IDs, and consistent metadata.
Advanced Patterns
Your Laravel application now has a clean OpenAI service layer with request/response, streaming, memory, queues, cost tracking, rate limiting, testability, and observability. Let’s extend it.
Livewire Integration: Real-Time AI Without a Frontend Framework
Not every Laravel team wants a JavaScript SPA. Livewire allows server-driven interactivity that pairs naturally with AI workflows.
Livewire is request-driven, not socket-driven. Each interaction is an HTTP round trip, and long-running calls block the request lifecycle unless handled intentionally. For a complete working implementation, see Laravel Livewire + Claude Integration — the architectural patterns are identical for OpenAI.
Basic Livewire AI Component
use Livewire\Component;
class AiAssistant extends Component
{
public string $prompt = '';
public string $response = '';
public bool $loading = false;
public function generate(OpenAiService $openAi): void
{
$this->loading = true;
$this->response = $openAi->generateText($this->prompt);
$this->loading = false;
}
public function render()
{
return view('livewire.ai-assistant');
}
}
This works immediately but it’s blocking. If generation takes 10 seconds, the request takes 10 seconds. For short responses, acceptable. For longer outputs, you have three realistic strategies:
- Polling: Trigger generation in a queued job, persist partial output, and poll from Livewire until complete. Simple and resilient.
- Hybrid Route for Streaming: Livewire initiates the request, a separate streaming endpoint handles chunked output, and a thin JavaScript layer listens and updates the DOM.
- Accept Blocking: For short, predictable outputs, it may be entirely acceptable.
The decision depends on latency tolerance, not preference.
Inertia + Vue: SPA-Style AI Interfaces
If your application already uses Inertia, you can leverage SSE natively on the client side.
Laravel Route:
Route::get('/ai/stream', function (Request $request, OpenAiService $openAi) {
return response()->stream(function () use ($request, $openAi) {
$openAi->streamText(
$request->query('prompt'),
function ($chunk) {
echo "data: " . json_encode($chunk) . "\n\n";
ob_flush();
flush();
}
);
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
});
Vue Client:
const eventSource = new EventSource(`/ai/stream?prompt=${encodeURIComponent(prompt)}`);
eventSource.onmessage = (event) => {
response.value += JSON.parse(event.data);
};
Streaming is an architectural decision, not a cosmetic enhancement. It changes how users perceive your application’s responsiveness.
Retrieval-Augmented Generation (RAG) Fundamentals
Eventually, chunking entire documents into prompts stops scaling. You need selective context retrieval.
RAG solves this by converting text into embeddings, storing them in a vector store, and injecting only semantically relevant chunks into the prompt at query time — rather than the full document.
Step 1: Generating Embeddings
$response = $this->client->post('/embeddings', [
'model' => 'text-embedding-3-small',
'input' => $textChunk,
]);
$vector = data_get($response->json(), 'data.0.embedding');
Step 2: Storing Vectors
At small scale, PostgreSQL with the pgvector extension is sufficient. If you need a deeper introduction to how embeddings actually work before choosing your storage strategy, Embeddings and Vector Databases: The Foundation of Semantic AI Systems covers the concepts and storage trade-offs in detail.
Step 3: Retrieving Relevant Chunks
At query time, convert the question to an embedding, perform a cosine similarity search, and retrieve the top N chunks. Only those chunks enter the OpenAI prompt. This reduces token usage, latency, and hallucination. RAG is not magic — it’s controlled context injection.
Multi-Model Strategies
Assuming one model should do everything is one of the most expensive architectural mistakes in LLM-backed applications. Large reasoning models are slow and costly. Smaller models are fast and cheap. Embedding models serve an entirely different purpose.
Intent-Based Model Routing
$model = match ($taskType) {
'summarization' => 'gpt-4o-mini',
'complex_reasoning' => 'gpt-4o',
'embedding' => 'text-embedding-3-small',
default => 'gpt-4o-mini',
};
This prevents overpaying for simple tasks. A document classification job does not need the same model as a multi-step reasoning pipeline.
Fallback Strategies
You can also implement primary → fallback model routing, high-accuracy → fast mode switching, and user-tier-based model selection. These are business decisions with direct cost and SLA implications, not technical hacks.
Senior Dev Tip: If you want to go further and treat your AI layer as a genuinely provider-agnostic abstraction — with tool calling, multi-model orchestration, and structured outputs built in — take a look at Building Agentic Laravel Apps with Prism PHP. Prism handles the abstraction layer so you’re not maintaining it yourself.
Where This Leaves You
Your Laravel + OpenAI system now supports real-time interfaces, retrieval-based document reasoning, multi-model routing, and a structured architecture ready to scale.
The service boundary protects you from API changes, model shifts, and provider swaps. OpenAI becomes infrastructure — replaceable, measurable, and contained. That’s the outcome worth building toward.
Next Steps
Ready to build with real conversation memory and streaming? Building a Claude API Chatbot in Laravel demonstrates both patterns in a complete, working implementation.
Hit rate limits in production? The queue-based throttling and backpressure strategies in Laravel Claude API Integration Guide apply directly to OpenAI workloads.
Want a server-driven real-time UI without JavaScript frameworks? Laravel Livewire + Claude Integration shows how to build AI features that stay in PHP.
Questions? Ask in our Developer Q&A or reach out directly.
Official References:
- OpenAI Chat Completions API
- OpenAI Models Reference
- Laravel HTTP Client Documentation
- Laravel Queues Documentation
This guide was featured on Laravel News in February 2026.
Senior Laravel Developer and AI Architect with 10+ years in the trenches. Dewald writes about building resilient, cost-aware AI integrations and modernizing the Laravel developer workflow for the 2026 ecosystem.

