Building an AI-Powered Feature in Laravel Using the Claude API

calendar_today July 17, 2026
person info@softcrony.com

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