Adding AI capabilities to a Laravel application is simpler than most developers expect. The Claude API is well-documented, the PHP client is straightforward, and the patterns for integrating AI into Laravel’s architecture are clean.
This guide builds a real AI-powered feature from scratch — an intelligent code review assistant that analyzes code submitted via API and returns structured feedback.
What We’re Building
An AI code review endpoint that:
- Accepts code via API request
- Sends it to Claude for analysis
- Returns structured feedback (issues, severity, suggestions)
- Streams the response in real-time
- Caches results for identical inputs
- Tracks usage per user
Prerequisites
- Laravel 11 or 12
- Anthropic API key (get one at console.anthropic.com)
- PHP 8.2+
- Redis (for caching and rate limiting)
Step 1 — Install the Anthropic PHP SDK
composer require anthropics/anthropic-sdk-php
Add your API key to .env:
ANTHROPIC_API_KEY=sk-ant-your-key-here
ANTHROPIC_MODEL=claude-sonnet-4-5
ANTHROPIC_MAX_TOKENS=2000
Add to config/services.php:
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL', 'claude-sonnet-4-5'),
'max_tokens' => env('ANTHROPIC_MAX_TOKENS', 2000),
],
Step 2 — Create the Claude Service
php artisan make:class Services/ClaudeService
<?php
namespace App\Services;
use Anthropic\Client;
use Anthropic\Anthropic;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ClaudeService
{
private Client $client;
private string $model;
private int $maxTokens;
public function __construct()
{
$this->client = Anthropic::client(
config('services.anthropic.api_key')
);
$this->model = config('services.anthropic.model');
$this->maxTokens = config('services.anthropic.max_tokens');
}
/**
* Send a message and get a complete response.
*/
public function message(
string $prompt,
string $systemPrompt = '',
?string $cacheKey = null,
int $cacheTtl = 3600
): string {
// Return cached response if available
if ($cacheKey && Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
try {
$messages = [
['role' => 'user', 'content' => $prompt]
];
$params = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'messages' => $messages,
];
if ($systemPrompt) {
$params['system'] = $systemPrompt;
}
$response = $this->client->messages()->create($params);
$content = $response->content[0]->text;
// Cache the response
if ($cacheKey) {
Cache::put($cacheKey, $content, $cacheTtl);
}
return $content;
} catch (\Exception $e) {
Log::error('Claude API error', [
'error' => $e->getMessage(),
'model' => $this->model,
'prompt' => substr($prompt, 0, 200),
]);
throw $e;
}
}
/**
* Stream a response — yields chunks as they arrive.
*/
public function stream(
string $prompt,
string $systemPrompt = '',
callable $onChunk = null
): string {
$fullResponse = '';
try {
$params = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'messages' => [
['role' => 'user', 'content' => $prompt]
],
];
if ($systemPrompt) {
$params['system'] = $systemPrompt;
}
$stream = $this->client->messages()->createStreaming($params);
foreach ($stream as $event) {
if ($event->type === 'content_block_delta') {
$chunk = $event->delta->text ?? '';
$fullResponse .= $chunk;
if ($onChunk) {
$onChunk($chunk);
}
}
}
} catch (\Exception $e) {
Log::error('Claude stream error', ['error' => $e->getMessage()]);
throw $e;
}
return $fullResponse;
}
/**
* Send a structured prompt and parse JSON response.
*/
public function structured(string $prompt, string $systemPrompt = ''): array
{
$jsonSystemPrompt = $systemPrompt . "\n\nIMPORTANT: Respond ONLY with valid JSON. No markdown, no explanation, no code blocks. Raw JSON only.";
$response = $this->message($prompt, $jsonSystemPrompt);
try {
return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
Log::error('Failed to parse Claude JSON response', [
'response' => $response,
'error' => $e->getMessage(),
]);
throw new \RuntimeException('AI returned invalid JSON: ' . $e->getMessage());
}
}
}
Step 3 — Create the Code Review Service
php artisan make:class Services/CodeReviewService
<?php
namespace App\Services;
class CodeReviewService
{
private string $systemPrompt = <<<'PROMPT'
You are an expert code reviewer specializing in PHP, Laravel, and web security.
Analyze the submitted code and identify issues across these categories:
- Security vulnerabilities (SQL injection, XSS, CSRF, mass assignment, etc.)
- Performance problems (N+1 queries, missing indexes, inefficient loops)
- Laravel best practices violations
- Code quality issues (complexity, duplication, naming)
- Missing error handling
- Type safety issues
For each issue found, provide:
- The specific problem
- The line or code block affected
- The severity (critical/high/medium/low)
- A concrete fix or recommendation
Be specific and actionable. Reference the actual code, not generic advice.
PROMPT;
public function __construct(
private readonly ClaudeService $claude
) {}
public function review(string $code, string $language = 'php'): array
{
$cacheKey = 'code_review_' . md5($code . $language);
$prompt = <<claude->structured($prompt, $this->systemPrompt);
}
}
Step 4 — Create the Controller
php artisan make:controller Api/CodeReviewController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\CodeReviewService;
use App\Models\CodeReview;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class CodeReviewController extends Controller
{
public function __construct(
private readonly CodeReviewService $reviewService
) {}
public function review(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string|max:10000',
'language' => 'in:php,javascript,python,typescript|nullable',
]);
$language = $validated['language'] ?? 'php';
try {
$result = $this->reviewService->review(
$validated['code'],
$language
);
// Save review to database
$review = CodeReview::create([
'user_id' => $request->user()->id,
'code' => $validated['code'],
'language' => $language,
'result' => $result,
'score' => $result['score'] ?? null,
]);
return response()->json([
'success' => true,
'review_id' => $review->id,
'result' => $result,
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Code review failed. Please try again.',
], 500);
}
}
/**
* Stream the review for real-time display.
*/
public function stream(Request $request)
{
$validated = $request->validate([
'code' => 'required|string|max:10000',
]);
return response()->stream(function () use ($validated) {
$this->reviewService->streamReview(
$validated['code'],
function (string $chunk) {
echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
ob_flush();
flush();
}
);
echo "data: [DONE]\n\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
}
}
Step 5 — Routes and Rate Limiting
// routes/api.php
use App\Http\Controllers\Api\CodeReviewController;
Route::middleware(['auth:sanctum', 'throttle:code_review'])->group(function () {
Route::post('/code-review', [CodeReviewController::class, 'review']);
Route::post('/code-review/stream', [CodeReviewController::class, 'stream']);
});
// app/Providers/RouteServiceProvider.php — add the rate limiter
RateLimiter::for('code_review', function (Request $request) {
return [
Limit::perMinute(5)->by($request->user()->id),
Limit::perDay(50)->by($request->user()->id),
];
});
Step 6 — Test It
POST /api/code-review
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"code": "
Expected response:
{
"success": true,
"review_id": 1,
"result": {
"summary": "This code has critical security vulnerabilities and uses deprecated PHP functions that no longer exist in modern PHP.",
"score": 12,
"issues": [
{
"title": "SQL Injection Vulnerability",
"description": "The $id parameter is concatenated directly into the SQL query without sanitization.",
"severity": "critical",
"code_snippet": "\"SELECT * FROM users WHERE id = \" . $id",
"fix": "Use PDO with prepared statements: $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]);"
},
{
"title": "Deprecated mysql_ Functions",
"description": "mysql_query() and mysql_fetch_assoc() were removed in PHP 7.0.",
"severity": "critical",
"code_snippet": "mysql_query(...), mysql_fetch_assoc(...)",
"fix": "Use PDO or MySQLi instead."
}
],
"positives": ["Simple, readable function structure"],
"recommendations": [
"Migrate to PDO with prepared statements immediately",
"Add input validation and type hints",
"Use a framework ORM like Eloquent instead of raw SQL"
]
}
}
Production Considerations
Cost management: The Claude API charges per token. A typical code review of 500 lines costs approximately $0.01–0.03. At 50 reviews/day per user, that's manageable — but monitor usage and set hard limits per user per day.
Caching: We cache code review results by MD5 hash of the code. Identical code submitted twice returns the cached result instantly at zero cost. Cache TTL of 1 hour is usually sufficient.
Queue for heavy loads: For non-streaming requests, consider dispatching reviews to a queue job rather than blocking the HTTP request:
// Dispatch to queue instead of blocking
DispatchCodeReview::dispatch($validated['code'], $request->user()->id);
return response()->json(['message' => 'Review queued', 'job_id' => $jobId]);
Error handling: The Anthropic API occasionally returns errors (overload, timeout). Always wrap API calls in try-catch and implement exponential backoff for retries in queue jobs.
Prompt versioning: Store your system prompts in the database or config files with version numbers. When you improve a prompt, you can compare results and roll back if needed.
If you want to add AI-powered features to your Laravel application — code review, content generation, document analysis, or custom chatbots — our team at Softcrony can build it for you.
Leave a comment