Skip to content

26: Building a Code Review Assistant

Chapter 26: Building a Code Review Assistant

Overview

Code reviews are critical for maintaining code quality, but they're time-consuming and require significant expertise. In this chapter, you'll build an intelligent code review assistant powered by Claude that automatically analyzes pull requests, detects potential bugs, suggests improvements, and identifies security vulnerabilities.

Your assistant will integrate with GitHub and GitLab webhooks to provide automated reviews, generate detailed feedback, and help teams maintain high code quality standards. You'll implement static analysis, pattern detection, best practice enforcement, and intelligent commenting systems.

What You'll Build

By the end of this chapter, you will have created:

  • A complete code review system with PR analysis and diff parsing
  • Security vulnerability scanner combining pattern matching and AI analysis
  • Bug detection engine that identifies logic errors, performance issues, and code quality problems
  • GitHub/GitLab webhook integration for automated reviews
  • Intelligent comment generator with severity-based prioritization
  • Review summary system with actionable feedback
  • CLI tool for manual PR reviews
  • Comprehensive test suite for validation

Estimated Time: 90-120 minutes

Prerequisites

Before starting, ensure you have:

  • Completed Chapters 11-15 (Tool use and structured outputs)
  • Git knowledge for analyzing diffs and commits
  • GitHub/GitLab API experience for webhook integration
  • Code quality understanding (SOLID, security, performance)

Objectives

By completing this chapter, you will:

  • Understand how to parse and analyze Git diffs programmatically
  • Build a security scanner that combines pattern matching with AI-powered deep analysis
  • Create a bug detection system that identifies logic errors and code quality issues
  • Integrate with GitHub/GitLab APIs for automated code reviews
  • Generate intelligent, actionable review comments with severity levels
  • Implement webhook handlers for seamless CI/CD integration
  • Build a production-ready code review assistant that augments human reviewers
  • Write comprehensive tests to validate review accuracy

Architecture Overview

php
<?php
# filename: src/CodeReview/ReviewSystem.php
declare(strict_types=1);

namespace App\CodeReview;

use Anthropic\Anthropic;
use App\CodeReview\GitHub\GitHubClient;

class ReviewSystem
{
    public function __construct(
        private Anthropic $claude,
        private GitHubClient $github,
        private ReviewConfig $config,
        private SecurityScanner $securityScanner,
        private BugDetector $bugDetector,
        private CommentGenerator $commentGenerator
    ) {}

    /**
     * Review a pull request
     */
    public function reviewPullRequest(int $prNumber): ReviewResult
    {
        // Fetch PR data
        $pr = $this->github->getPullRequest($prNumber);
        $diff = $this->github->getDiff($prNumber);
        $files = $this->github->getChangedFiles($prNumber);

        // Analyze the changes
        $analysis = $this->analyzeChanges($diff, $files);

        // Generate review comments
        $comments = $this->generateComments($analysis, $files);

        // Post review to GitHub
        if ($this->config->autoPost) {
            $this->postReview($prNumber, $comments, $analysis);
        }

        return new ReviewResult(
            analysis: $analysis,
            comments: $comments,
            summary: $analysis['summary']
        );
    }

    private function analyzeChanges(string $diff, array $files): array
    {
        $prompt = $this->buildAnalysisPrompt($diff, $files);

        $response = $this->claude->messages()->create([
            'model' => 'claude-sonnet-4-20250514',
            'max_tokens' => 8192,
            'temperature' => 0.3,
            'system' => $this->getSystemPrompt(),
            'messages' => [[
                'role' => 'user',
                'content' => $prompt
            ]]
        ]);

        return json_decode($response->content[0]->text, true);
    }

    private function buildAnalysisPrompt(string $diff, array $files): string
    {
        $prompt = "Analyze this pull request diff and provide a comprehensive code review.\n\n";
        $prompt .= "Files changed: " . count($files) . "\n\n";
        $prompt .= "Diff:\n```diff\n{$diff}\n```\n\n";
        $prompt .= "Return JSON with analysis including security issues, bugs, code quality concerns, and suggestions.";
        
        return $prompt;
    }

    private function generateComments(array $analysis, array $files): array
    {
        $allIssues = [];
        
        // Security scanning
        if ($this->config->enableSecurityScan) {
            foreach ($files as $file) {
                $filePath = is_array($file) ? $file['filename'] ?? $file['path'] ?? '' : $file;
                if (empty($filePath)) {
                    continue;
                }
                
                try {
                    $fileContent = $this->github->getFileContents($filePath);
                    $securityIssues = $this->securityScanner->scanCode($fileContent, $filePath);
                    $allIssues[$filePath] = array_merge($allIssues[$filePath] ?? [], $securityIssues);
                } catch (\Exception $e) {
                    // Skip files that can't be fetched (deleted files, etc.)
                    continue;
                }
            }
        }
        
        // Bug detection
        if ($this->config->enableBugDetection) {
            $bugIssues = $this->bugDetector->detectBugs($files);
            foreach ($bugIssues as $filePath => $bugs) {
                $allIssues[$filePath] = array_merge($allIssues[$filePath] ?? [], $bugs);
            }
        }
        
        return $this->commentGenerator->generateComments($allIssues, $files);
    }

    private function postReview(int $prNumber, array $comments, array $analysis): void
    {
        $summary = $this->commentGenerator->generateSummary($analysis, $comments);
        
        // Filter comments by minimum severity
        $filteredComments = array_filter($comments, function($comment) {
            $priority = ['CRITICAL' => 0, 'HIGH' => 1, 'MEDIUM' => 2, 'LOW' => 3];
            $minPriority = $priority[$this->config->minSeverity] ?? 3;
            return ($priority[$comment['severity']] ?? 99) <= $minPriority;
        });
        
        // Limit number of comments
        if (count($filteredComments) > $this->config->maxComments) {
            $filteredComments = array_slice($filteredComments, 0, $this->config->maxComments);
        }
        
        // Determine review decision
        $decision = new \App\CodeReview\ReviewDecision();
        $event = $decision->decide($filteredComments, $this->config);
        
        // Post review with decision
        $this->github->postReview($prNumber, $filteredComments, $summary, $event);
        
        // Create status check if enabled
        if ($this->config->enableStatusChecks) {
            $sha = $this->github->getPrHeadSha($prNumber);
            $state = ($event === 'APPROVE') ? 'success' : (($event === 'REQUEST_CHANGES') ? 'failure' : 'pending');
            $description = $decision->getEventMessage($event, $filteredComments);
            
            $this->github->createStatusCheck($sha, $state, $description);
        }
    }

    private function getSystemPrompt(): string
    {
        return <<<SYSTEM
You are an expert code reviewer specializing in PHP, with deep knowledge of:
- Security best practices (OWASP, input validation, SQL injection, XSS)
- Performance optimization
- Code quality (SOLID, DRY, design patterns)
- Laravel and modern PHP frameworks
- Testing and test coverage
- Documentation standards

Your reviews are:
- Constructive and educational
- Specific with actionable suggestions
- Balanced between nitpicks and critical issues
- Focused on maintainability and scalability

Severity levels:
- CRITICAL: Security vulnerabilities, data loss risks
- HIGH: Bugs, performance issues, broken functionality
- MEDIUM: Code quality, maintainability concerns
- LOW: Style, minor optimizations, suggestions

Always provide code examples for suggested improvements.
SYSTEM;
    }
}

PR Analysis Engine

Diff Parser and Analyzer

php
<?php
# filename: src/CodeReview/DiffAnalyzer.php
declare(strict_types=1);

namespace App\CodeReview;

class DiffAnalyzer
{
    /**
     * Parse unified diff format
     */
    public function parseDiff(string $diff): array
    {
        $files = [];
        $currentFile = null;
        $currentHunk = null;

        $lines = explode("\n", $diff);

        foreach ($lines as $line) {
            // New file
            if (str_starts_with($line, 'diff --git')) {
                if ($currentFile) {
                    $files[] = $currentFile;
                }
                preg_match('/b\/(.+)$/', $line, $matches);
                $currentFile = [
                    'path' => $matches[1] ?? '',
                    'hunks' => [],
                    'additions' => 0,
                    'deletions' => 0
                ];
                continue;
            }

            // File metadata
            if (str_starts_with($line, '+++') || str_starts_with($line, '---')) {
                continue;
            }

            // Hunk header
            if (str_starts_with($line, '@@')) {
                if ($currentHunk) {
                    $currentFile['hunks'][] = $currentHunk;
                }
                preg_match('/@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@(.*)/', $line, $matches);
                $currentHunk = [
                    'old_start' => (int)$matches[1],
                    'old_count' => (int)($matches[2] ?? 1),
                    'new_start' => (int)$matches[3],
                    'new_count' => (int)($matches[4] ?? 1),
                    'context' => trim($matches[5] ?? ''),
                    'lines' => []
                ];
                continue;
            }

            // Hunk content
            if ($currentHunk !== null) {
                $type = match($line[0] ?? '') {
                    '+' => 'addition',
                    '-' => 'deletion',
                    default => 'context'
                };

                $currentHunk['lines'][] = [
                    'type' => $type,
                    'content' => substr($line, 1)
                ];

                if ($type === 'addition') {
                    $currentFile['additions']++;
                } elseif ($type === 'deletion') {
                    $currentFile['deletions']++;
                }
            }
        }

        if ($currentHunk) {
            $currentFile['hunks'][] = $currentHunk;
        }
        if ($currentFile) {
            $files[] = $currentFile;
        }

        return $files;
    }

    /**
     * Extract added/modified code only
     */
    public function getChangedCode(array $parsedDiff): array
    {
        $changes = [];

        foreach ($parsedDiff as $file) {
            $fileChanges = [
                'path' => $file['path'],
                'additions' => [],
                'modifications' => []
            ];

            foreach ($file['hunks'] as $hunk) {
                $context = [];
                foreach ($hunk['lines'] as $line) {
                    if ($line['type'] === 'addition') {
                        $fileChanges['additions'][] = [
                            'line' => $hunk['new_start'],
                            'code' => $line['content'],
                            'context' => $context
                        ];
                    } elseif ($line['type'] === 'context') {
                        $context[] = $line['content'];
                        if (count($context) > 3) {
                            array_shift($context);
                        }
                    }
                }
            }

            if (!empty($fileChanges['additions'])) {
                $changes[] = $fileChanges;
            }
        }

        return $changes;
    }
}

Security Vulnerability Detection

php
<?php
# filename: src/CodeReview/SecurityScanner.php
declare(strict_types=1);

namespace App\CodeReview;

use Anthropic\Anthropic;

class SecurityScanner
{
    private array $vulnerabilityPatterns = [
        'sql_injection' => [
            'pattern' => '/\$.*query.*\$.*[^prepare]/i',
            'description' => 'Possible SQL injection vulnerability'
        ],
        'xss' => [
            'pattern' => '/echo\s+\$_(GET|POST|REQUEST)/i',
            'description' => 'Possible XSS vulnerability'
        ],
        'command_injection' => [
            'pattern' => '/(exec|system|passthru|shell_exec)\s*\(\s*\$/',
            'description' => 'Possible command injection'
        ],
        'file_inclusion' => [
            'pattern' => '/(include|require).*\$_(GET|POST|REQUEST)/i',
            'description' => 'Possible file inclusion vulnerability'
        ]
    ];

    public function __construct(
        private Anthropic $claude
    ) {}

    /**
     * Scan code for security vulnerabilities
     */
    public function scanCode(string $code, string $filename): array
    {
        $issues = [];

        // Pattern-based detection
        $issues = array_merge($issues, $this->patternScan($code));

        // AI-powered deep analysis
        $aiIssues = $this->deepSecurityAnalysis($code, $filename);
        $issues = array_merge($issues, $aiIssues);

        return $issues;
    }

    private function patternScan(string $code): array
    {
        $issues = [];
        $lines = explode("\n", $code);

        foreach ($lines as $lineNum => $line) {
            foreach ($this->vulnerabilityPatterns as $type => $pattern) {
                if (preg_match($pattern['pattern'], $line)) {
                    $issues[] = [
                        'type' => 'security',
                        'severity' => 'CRITICAL',
                        'category' => $type,
                        'line' => $lineNum + 1,
                        'message' => $pattern['description'],
                        'code' => trim($line)
                    ];
                }
            }
        }

        return $issues;
    }

    private function deepSecurityAnalysis(string $code, string $filename): array
    {
        $prompt = <<<PROMPT
Analyze this PHP code for security vulnerabilities. Return a JSON array of issues found.

File: {$filename}

Code:
```php
{$code}

Look for:

  1. SQL injection vulnerabilities
  2. XSS (Cross-Site Scripting)
  3. CSRF (Cross-Site Request Forgery)
  4. Authentication/authorization issues
  5. Insecure cryptography
  6. Information disclosure
  7. Insecure deserialization
  8. File upload vulnerabilities
  9. Path traversal
  10. Weak password policies

For each issue found, return: { "type": "security", "severity": "CRITICAL|HIGH|MEDIUM|LOW", "category": "vulnerability type", "line": line_number, "message": "description of the issue", "suggestion": "how to fix it", "code_example": "fixed code example" }

Return ONLY valid JSON array, no explanation. PROMPT;

    $response = $this->claude->messages()->create([
        'model' => 'claude-sonnet-4-20250514',
        'max_tokens' => 4096,
        'temperature' => 0.2,
        'messages' => [[
            'role' => 'user',
            'content' => $prompt
        ]]
    ]);

    $jsonText = $response->content[0]->text;

    // Extract JSON from response
    if (preg_match('/```json\s*(\[.*?\])\s*```/s', $jsonText, $matches)) {
        $jsonText = $matches[1];
    } elseif (preg_match('/(\[.*\])/s', $jsonText, $matches)) {
        $jsonText = $matches[1];
    }

    return json_decode($jsonText, true) ?? [];
}

}


## Bug Detection and Code Quality Analysis

```php
<?php
# filename: src/CodeReview/BugDetector.php
declare(strict_types=1);

namespace App\CodeReview;

use Anthropic\Anthropic;

class BugDetector
{
    public function __construct(
        private Anthropic $claude
    ) {}

    /**
     * Detect potential bugs in code
     */
    public function detectBugs(array $files): array
    {
        $bugs = [];

        foreach ($files as $file) {
            $filePath = is_array($file) ? $file['filename'] ?? $file['path'] ?? '' : $file;
            if (empty($filePath)) {
                continue;
            }

            // Skip non-PHP files
            if (!str_ends_with($filePath, '.php')) {
                continue;
            }

            try {
                $fileContent = file_get_contents($filePath);
                if ($fileContent === false) {
                    continue;
                }

                $analysis = $this->analyzeFile($fileContent, $filePath);

                if (!empty($analysis)) {
                    $bugs[$filePath] = $analysis;
                }
            } catch (\Exception $e) {
                // Skip files that can't be read
                continue;
            }
        }

        return $bugs;
    }

    private function analyzeFile(string $code, string $filename): array
    {
        $prompt = <<<PROMPT
Analyze this PHP code for potential bugs and code quality issues.

File: {$filename}

```php
{$code}

Check for:

  1. Logic errors and edge cases
  2. Null pointer exceptions
  3. Type errors
  4. Resource leaks (unclosed files, connections)
  5. Race conditions
  6. Off-by-one errors
  7. Incorrect error handling
  8. Missing validation
  9. Dead code
  10. Code duplication
  11. Performance issues (N+1 queries, inefficient loops)
  12. Missing return statements
  13. Incorrect comparisons (== vs ===)
  14. Uninitialized variables

Return JSON array with format: [ { "severity": "CRITICAL|HIGH|MEDIUM|LOW", "line": line_number, "type": "bug|performance|quality", "category": "specific issue type", "message": "clear description", "suggestion": "how to fix", "code_before": "problematic code", "code_after": "suggested fix" } ]

Return ONLY valid JSON, no explanation. PROMPT;

    $response = $this->claude->messages()->create([
        'model' => 'claude-sonnet-4-20250514',
        'max_tokens' => 6144,
        'temperature' => 0.2,
        'messages' => [[
            'role' => 'user',
            'content' => $prompt
        ]]
    ]);

    $jsonText = $response->content[0]->text;

    // Extract and parse JSON
    if (preg_match('/```json\s*(\[.*?\])\s*```/s', $jsonText, $matches)) {
        $jsonText = $matches[1];
    } elseif (preg_match('/(\[.*\])/s', $jsonText, $matches)) {
        $jsonText = $matches[1];
    }

    return json_decode($jsonText, true) ?? [];
}

}


## GitHub/GitLab Integration

### GitHub Webhook Handler

```php
<?php
# filename: src/CodeReview/GitHub/WebhookHandler.php
declare(strict_types=1);

namespace App\CodeReview\GitHub;

use App\CodeReview\ReviewSystem;

class WebhookHandler
{
    public function __construct(
        private GitHubClient $github,
        private ReviewSystem $reviewSystem,
        private string $webhookSecret
    ) {}

    /**
     * Handle incoming webhook
     */
    public function handle(string $payload, string $signature): ?array
    {
        // Verify signature
        if (!$this->verifySignature($payload, $signature)) {
            throw new \RuntimeException('Invalid webhook signature');
        }

        $data = json_decode($payload, true);

        // Handle different events
        return match($data['action'] ?? '') {
            'opened', 'synchronize' => $this->handlePullRequest($data),
            'created' => $this->handleComment($data),
            default => null
        };
    }

    private function handlePullRequest(array $data): array
    {
        $prNumber = $data['pull_request']['number'];
        $repository = $data['repository']['full_name'];

        // Trigger review
        $result = $this->reviewSystem->reviewPullRequest($prNumber);

        return [
            'pr' => $prNumber,
            'repository' => $repository,
            'review_posted' => true,
            'issues_found' => count($result->comments)
        ];
    }

    private function handleComment(array $data): ?array
    {
        // Handle review comments and questions
        if (!isset($data['comment']['body'])) {
            return null;
        }

        $comment = $data['comment']['body'];

        // Check if comment mentions the bot
        if (str_contains($comment, '@code-reviewer')) {
            // Respond to specific questions
            return $this->respondToComment($data);
        }

        return null;
    }

    private function verifySignature(string $payload, string $signature): bool
    {
        $expected = 'sha256=' . hash_hmac('sha256', $payload, $this->webhookSecret);
        return hash_equals($expected, $signature);
    }

    private function respondToComment(array $data): array
    {
        // Extract question from comment
        $comment = $data['comment']['body'];
        $prNumber = $data['issue']['number'];

        // Use Claude to generate response
        // Implementation details...

        return [
            'responded' => true,
            'pr' => $prNumber
        ];
    }
}

GitLab Client

php
<?php
# filename: src/CodeReview/GitLab/GitLabClient.php
declare(strict_types=1);

namespace App\CodeReview\GitLab;

class GitLabClient
{
    public function __construct(
        private string $token,
        private string $projectId,
        private string $baseUrl = 'https://gitlab.com/api/v4'
    ) {}

    /**
     * Get merge request data
     */
    public function getMergeRequest(int $mrIid): array
    {
        return $this->request("GET", "/projects/{$this->projectId}/merge_requests/{$mrIid}");
    }

    /**
     * Get MR diff
     */
    public function getDiff(int $mrIid): string
    {
        $changes = $this->request("GET", "/projects/{$this->projectId}/merge_requests/{$mrIid}/changes");
        return $changes['changes'] ?? '';
    }

    /**
     * Get changed files
     */
    public function getChangedFiles(int $mrIid): array
    {
        $changes = $this->request("GET", "/projects/{$this->projectId}/merge_requests/{$mrIid}/changes");
        return array_map(fn($change) => ['filename' => $change['new_path']], $changes['changes'] ?? []);
    }

    /**
     * Post review comment
     */
    public function postReview(int $mrIid, array $comments, string $body, string $state = 'commented'): array
    {
        // GitLab uses discussions instead of reviews
        return $this->request("POST", "/projects/{$this->projectId}/merge_requests/{$mrIid}/discussions", [
            'body' => $body . "\n\n" . implode("\n", array_map(fn($c) => $c['body'], $comments))
        ]);
    }

    /**
     * Create status check
     */
    public function createStatusCheck(int $mrIid, string $state, string $description, ?string $targetUrl = null): array
    {
        $sha = $this->getMergeRequest($mrIid)['sha'];
        return $this->request("POST", "/projects/{$this->projectId}/statuses/{$sha}", [
            'state' => $state, // success, failed, pending
            'description' => $description,
            'target_url' => $targetUrl
        ]);
    }

    /**
     * Get file contents
     */
    public function getFileContents(string $path, string $ref = 'main'): string
    {
        $data = $this->request("GET", "/projects/{$this->projectId}/repository/files/" . urlencode($path) . "/raw?ref={$ref}");
        return $data;
    }

    private function request(string $method, string $endpoint, ?array $body = null): mixed
    {
        $ch = curl_init("{$this->baseUrl}{$endpoint}");

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => [
                "PRIVATE-TOKEN: {$this->token}",
                'Content-Type: application/json'
            ]
        ]);

        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 400) {
            throw new \RuntimeException("GitLab API error: {$httpCode}");
        }

        return json_decode($response, true);
    }
}

GitHub Client

php
<?php
# filename: src/CodeReview/GitHub/GitHubClient.php
declare(strict_types=1);

namespace App\CodeReview\GitHub;

class GitHubClient
{
    public function __construct(
        private string $token,
        private string $repository
    ) {}

    /**
     * Get pull request data
     */
    public function getPullRequest(int $prNumber): array
    {
        return $this->request("GET", "/repos/{$this->repository}/pulls/{$prNumber}");
    }

    /**
     * Get PR diff
     */
    public function getDiff(int $prNumber): string
    {
        return $this->request(
            "GET",
            "/repos/{$this->repository}/pulls/{$prNumber}",
            accept: 'application/vnd.github.v3.diff'
        );
    }

    /**
     * Get changed files
     */
    public function getChangedFiles(int $prNumber): array
    {
        return $this->request("GET", "/repos/{$this->repository}/pulls/{$prNumber}/files");
    }

    /**
     * Post review comment
     */
    public function postReview(int $prNumber, array $comments, string $body, string $event = 'COMMENT'): array
    {
        return $this->request("POST", "/repos/{$this->repository}/pulls/{$prNumber}/reviews", [
            'body' => $body,
            'event' => $event, // APPROVE, REQUEST_CHANGES, COMMENT
            'comments' => $comments
        ]);
    }

    /**
     * Post single comment
     */
    public function postComment(int $prNumber, string $body, string $path, int $line): array
    {
        return $this->request("POST", "/repos/{$this->repository}/pulls/{$prNumber}/comments", [
            'body' => $body,
            'path' => $path,
            'line' => $line
        ]);
    }

    /**
     * Get file contents
     */
    public function getFileContents(string $path, string $ref = 'main'): string
    {
        $data = $this->request("GET", "/repos/{$this->repository}/contents/{$path}?ref={$ref}");
        return base64_decode($data['content']);
    }

    /**
     * Create status check
     */
    public function createStatusCheck(string $sha, string $state, string $description, ?string $targetUrl = null): array
    {
        return $this->request("POST", "/repos/{$this->repository}/statuses/{$sha}", [
            'state' => $state, // success, failure, pending, error
            'description' => $description,
            'context' => 'claude-code-review',
            'target_url' => $targetUrl
        ]);
    }

    /**
     * Get commit SHA from PR
     */
    public function getPrHeadSha(int $prNumber): string
    {
        $pr = $this->getPullRequest($prNumber);
        return $pr['head']['sha'];
    }

    private function request(string $method, string $endpoint, ?array $body = null, string $accept = 'application/vnd.github.v3+json'): mixed
    {
        $ch = curl_init("https://api.github.com{$endpoint}");

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => [
                "Authorization: Bearer {$this->token}",
                "Accept: {$accept}",
                'User-Agent: Code-Review-Assistant'
            ]
        ]);

        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 400) {
            throw new \RuntimeException("GitHub API error: {$httpCode}");
        }

        // For diff format, return as string
        if ($accept === 'application/vnd.github.v3.diff') {
            return $response;
        }

        return json_decode($response, true);
    }
}

Review Decision Logic

php
<?php
# filename: src/CodeReview/ReviewDecision.php
declare(strict_types=1);

namespace App\CodeReview;

class ReviewDecision
{
    /**
     * Determine review action based on issues found
     */
    public function decide(array $comments, ReviewConfig $config): string
    {
        $criticalCount = count(array_filter($comments, fn($c) => $c['severity'] === 'CRITICAL'));
        $highCount = count(array_filter($comments, fn($c) => $c['severity'] === 'HIGH'));
        $mediumCount = count(array_filter($comments, fn($c) => $c['severity'] === 'MEDIUM'));

        // Block merge if critical issues found
        if ($criticalCount > 0) {
            return 'REQUEST_CHANGES';
        }

        // Request changes if high severity issues exceed threshold
        if ($highCount > 5) {
            return 'REQUEST_CHANGES';
        }

        // Approve if only low/medium issues and below threshold
        if ($criticalCount === 0 && $highCount === 0 && $mediumCount <= 10) {
            return 'APPROVE';
        }

        // Default to comment-only for review
        return 'COMMENT';
    }

    /**
     * Generate review event message
     */
    public function getEventMessage(string $event, array $comments): string
    {
        return match($event) {
            'APPROVE' => '✅ Code review passed. No critical issues found.',
            'REQUEST_CHANGES' => '❌ Please address the critical/high severity issues before merging.',
            default => '💬 Review comments posted. Please review feedback.'
        };
    }
}

Review Comment Generator

php
<?php
# filename: src/CodeReview/CommentGenerator.php
declare(strict_types=1);

namespace App\CodeReview;

use Anthropic\Anthropic;

class CommentGenerator
{
    public function __construct(
        private Anthropic $claude,
        private ?string $template = null
    ) {}

    /**
     * Generate helpful review comments
     */
    public function generateComments(array $issues, array $files): array
    {
        $comments = [];

        foreach ($issues as $filePath => $fileIssues) {
            foreach ($fileIssues as $issue) {
                $comment = $this->formatComment($issue);

                $comments[] = [
                    'path' => $filePath,
                    'line' => $issue['line'],
                    'body' => $comment,
                    'severity' => $issue['severity']
                ];
            }
        }

        return $this->prioritizeComments($comments);
    }

    private function formatComment(array $issue): string
    {
        // Use custom template if provided
        if ($this->template) {
            return $this->applyTemplate($issue);
        }

        $emoji = match($issue['severity']) {
            'CRITICAL' => '🚨',
            'HIGH' => '⚠️',
            'MEDIUM' => '💡',
            'LOW' => 'ℹ️',
            default => '📝'
        };

        $comment = "{$emoji} **{$issue['severity']}**: {$issue['message']}\n\n";

        if (isset($issue['code_before'])) {
            $comment .= "**Current code:**\n```php\n{$issue['code_before']}\n```\n\n";
        }

        if (isset($issue['suggestion'])) {
            $comment .= "**Suggestion:**\n{$issue['suggestion']}\n\n";
        }

        if (isset($issue['code_after'])) {
            $comment .= "**Improved version:**\n```php\n{$issue['code_after']}\n```\n\n";
        }

        if (isset($issue['references'])) {
            $comment .= "**References:**\n";
            foreach ($issue['references'] as $ref) {
                $comment .= "- {$ref}\n";
            }
        }

        return $comment;
    }

    private function prioritizeComments(array $comments): array
    {
        // Sort by severity
        usort($comments, function($a, $b) {
            $priority = ['CRITICAL' => 0, 'HIGH' => 1, 'MEDIUM' => 2, 'LOW' => 3];
            return ($priority[$a['severity']] ?? 99) <=> ($priority[$b['severity']] ?? 99);
        });

        return $comments;
    }

    /**
     * Generate review summary
     */
    public function generateSummary(array $analysis, array $comments): string
    {
        $criticalCount = count(array_filter($comments, fn($c) => $c['severity'] === 'CRITICAL'));
        $highCount = count(array_filter($comments, fn($c) => $c['severity'] === 'HIGH'));
        $mediumCount = count(array_filter($comments, fn($c) => $c['severity'] === 'MEDIUM'));
        $lowCount = count(array_filter($comments, fn($c) => $c['severity'] === 'LOW'));

        $summary = "## 🤖 Automated Code Review\n\n";
        $summary .= "### Summary\n\n";
        $summary .= "Found **" . count($comments) . " issues**:\n\n";

        if ($criticalCount > 0) {
            $summary .= "- 🚨 **{$criticalCount} Critical** - Requires immediate attention\n";
        }
        if ($highCount > 0) {
            $summary .= "- ⚠️ **{$highCount} High** - Should be addressed\n";
        }
        if ($mediumCount > 0) {
            $summary .= "- 💡 **{$mediumCount} Medium** - Improvements suggested\n";
        }
        if ($lowCount > 0) {
            $summary .= "- ℹ️ **{$lowCount} Low** - Minor suggestions\n";
        }

        $summary .= "\n### Files Analyzed\n\n";
        $summary .= "- **" . count($analysis['files']) . " files** changed\n";
        $summary .= "- **" . ($analysis['additions'] ?? 0) . " additions**, ";
        $summary .= "**" . ($analysis['deletions'] ?? 0) . " deletions**\n\n";

        if ($criticalCount > 0) {
            $summary .= "⚠️ **This PR has critical issues that must be addressed before merging.**\n\n";
        } elseif ($highCount === 0 && $mediumCount === 0 && $lowCount === 0) {
            $summary .= "✅ **Great work! No issues found.**\n\n";
        }

        $summary .= "---\n\n";
        $summary .= "*Reviewed by Claude Code Review Assistant*";

        return $summary;
    }

    private function applyTemplate(array $issue): string
    {
        // Simple template replacement
        $template = $this->template ?? '{{severity}}: {{message}}';
        return str_replace(
            ['{{severity}}', '{{message}}', '{{suggestion}}', '{{code_before}}', '{{code_after}}'],
            [
                $issue['severity'],
                $issue['message'],
                $issue['suggestion'] ?? '',
                $issue['code_before'] ?? '',
                $issue['code_after'] ?? ''
            ],
            $template
        );
    }
}

Caching for Code Reviews

php
<?php
# filename: src/CodeReview/ReviewCache.php
declare(strict_types=1);

namespace App\CodeReview;

use Psr\SimpleCache\CacheInterface;

class ReviewCache
{
    public function __construct(
        private CacheInterface $cache,
        private int $ttl = 3600 // 1 hour
    ) {}

    /**
     * Generate cache key from file content hash
     */
    private function getCacheKey(string $filePath, string $content): string
    {
        $hash = hash('sha256', $filePath . $content);
        return "code_review:{$hash}";
    }

    /**
     * Get cached review if available
     */
    public function getCachedReview(string $filePath, string $content): ?array
    {
        $key = $this->getCacheKey($filePath, $content);
        return $this->cache->get($key);
    }

    /**
     * Cache review results
     */
    public function cacheReview(string $filePath, string $content, array $issues): void
    {
        $key = $this->getCacheKey($filePath, $content);
        $this->cache->set($key, $issues, $this->ttl);
    }

    /**
     * Check if file content has changed
     */
    public function hasChanged(string $filePath, string $newContent): bool
    {
        $cached = $this->getCachedReview($filePath, $newContent);
        return $cached === null;
    }
}

Rate Limiting for Reviews

php
<?php
# filename: src/CodeReview/ReviewRateLimiter.php
declare(strict_types=1);

namespace App\CodeReview;

use Psr\SimpleCache\CacheInterface;

class ReviewRateLimiter
{
    public function __construct(
        private CacheInterface $cache,
        private int $maxRequestsPerMinute = 10,
        private int $maxRequestsPerHour = 100
    ) {}

    /**
     * Check if request is allowed
     */
    public function allow(string $identifier): bool
    {
        $minuteKey = "rate_limit:minute:{$identifier}:" . date('Y-m-d-H-i');
        $hourKey = "rate_limit:hour:{$identifier}:" . date('Y-m-d-H');

        $minuteCount = $this->cache->get($minuteKey, 0);
        $hourCount = $this->cache->get($hourKey, 0);

        if ($minuteCount >= $this->maxRequestsPerMinute) {
            return false;
        }

        if ($hourCount >= $this->maxRequestsPerHour) {
            return false;
        }

        // Increment counters
        $this->cache->set($minuteKey, $minuteCount + 1, 60);
        $this->cache->set($hourKey, $hourCount + 1, 3600);

        return true;
    }

    /**
     * Get remaining requests
     */
    public function getRemaining(string $identifier): array
    {
        $minuteKey = "rate_limit:minute:{$identifier}:" . date('Y-m-d-H-i');
        $hourKey = "rate_limit:hour:{$identifier}:" . date('Y-m-d-H');

        return [
            'minute' => max(0, $this->maxRequestsPerMinute - ($this->cache->get($minuteKey, 0))),
            'hour' => max(0, $this->maxRequestsPerHour - ($this->cache->get($hourKey, 0)))
        ];
    }
}

Cost Tracking

php
<?php
# filename: src/CodeReview/CostTracker.php
declare(strict_types=1);

namespace App\CodeReview;

class CostTracker
{
    private array $costs = [];

    /**
     * Track API costs for a review
     */
    public function trackReview(int $prNumber, int $inputTokens, int $outputTokens, string $model = 'claude-sonnet-4-20250514'): float
    {
        // Pricing per 1M tokens (as of 2025)
        $pricing = [
            'claude-sonnet-4-20250514' => ['input' => 3.00, 'output' => 15.00],
            'claude-haiku-4-20250514' => ['input' => 0.25, 'output' => 1.25],
            'claude-opus-4-20250514' => ['input' => 15.00, 'output' => 75.00],
        ];

        $modelPricing = $pricing[$model] ?? $pricing['claude-sonnet-4-20250514'];
        
        $inputCost = ($inputTokens / 1_000_000) * $modelPricing['input'];
        $outputCost = ($outputTokens / 1_000_000) * $modelPricing['output'];
        $totalCost = $inputCost + $outputCost;

        $this->costs[$prNumber] = [
            'pr' => $prNumber,
            'model' => $model,
            'input_tokens' => $inputTokens,
            'output_tokens' => $outputTokens,
            'input_cost' => $inputCost,
            'output_cost' => $outputCost,
            'total_cost' => $totalCost,
            'timestamp' => time()
        ];

        return $totalCost;
    }

    /**
     * Get cost for a PR review
     */
    public function getReviewCost(int $prNumber): ?float
    {
        return $this->costs[$prNumber]['total_cost'] ?? null;
    }

    /**
     * Get total costs
     */
    public function getTotalCosts(): array
    {
        return $this->costs;
    }

    /**
     * Get daily cost summary
     */
    public function getDailySummary(): array
    {
        $today = date('Y-m-d');
        $todayCosts = array_filter($this->costs, fn($cost) => date('Y-m-d', $cost['timestamp']) === $today);
        
        return [
            'date' => $today,
            'reviews' => count($todayCosts),
            'total_cost' => array_sum(array_column($todayCosts, 'total_cost')),
            'total_tokens' => array_sum(array_column($todayCosts, 'input_tokens')) + array_sum(array_column($todayCosts, 'output_tokens'))
        ];
    }
}

Incremental Review Strategy

php
<?php
# filename: src/CodeReview/IncrementalReviewer.php
declare(strict_types=1);

namespace App\CodeReview;

class IncrementalReviewer
{
    /**
     * Review only changed lines, not entire files
     */
    public function reviewChanges(array $diff, array $files): array
    {
        $issues = [];
        
        foreach ($files as $file) {
            $changedLines = $this->extractChangedLines($diff, $file['path']);
            
            if (empty($changedLines)) {
                continue;
            }

            // Only review changed lines with context
            $context = $this->getContextLines($file['path'], $changedLines);
            $reviewCode = $this->buildReviewableCode($context, $changedLines);
            
            // Review only the changed code
            $fileIssues = $this->reviewCode($reviewCode, $file['path']);
            $issues[$file['path']] = $fileIssues;
        }

        return $issues;
    }

    private function extractChangedLines(string $diff, string $filePath): array
    {
        $lines = [];
        $inFile = false;
        
        foreach (explode("\n", $diff) as $line) {
            if (str_contains($line, $filePath)) {
                $inFile = true;
                continue;
            }
            
            if ($inFile && str_starts_with($line, '@@')) {
                preg_match('/\+(\d+)/', $line, $matches);
                $startLine = (int)($matches[1] ?? 0);
                continue;
            }
            
            if ($inFile && str_starts_with($line, '+') && !str_starts_with($line, '+++')) {
                $lines[] = ['line' => $startLine++, 'content' => substr($line, 1)];
            }
        }
        
        return $lines;
    }

    private function getContextLines(string $filePath, array $changedLines): array
    {
        // Get 3 lines before and after each changed line
        $context = [];
        $lineNumbers = array_column($changedLines, 'line');
        
        if (empty($lineNumbers)) {
            return [];
        }

        $minLine = max(1, min($lineNumbers) - 3);
        $maxLine = max($lineNumbers) + 3;
        
        $fileContent = file_get_contents($filePath);
        $allLines = explode("\n", $fileContent);
        
        return array_slice($allLines, $minLine - 1, $maxLine - $minLine + 1);
    }

    private function buildReviewableCode(array $context, array $changedLines): string
    {
        // Build code snippet with changed lines highlighted
        $code = implode("\n", $context);
        return "// Changed lines:\n" . $code;
    }

    private function reviewCode(string $code, string $filePath): array
    {
        // Use SecurityScanner and BugDetector on code snippet
        // Implementation similar to main review flow
        return [];
    }
}

Retry Logic for Failed Reviews

php
<?php
# filename: src/CodeReview/ReviewRetry.php
declare(strict_types=1);

namespace App\CodeReview;

class ReviewRetry
{
    public function __construct(
        private int $maxRetries = 3,
        private int $baseDelay = 1000 // milliseconds
    ) {}

    /**
     * Execute review with retry logic
     */
    public function executeWithRetry(callable $reviewFunction): mixed
    {
        $attempt = 0;
        $lastException = null;

        while ($attempt < $this->maxRetries) {
            try {
                return $reviewFunction();
            } catch (\Exception $e) {
                $lastException = $e;
                $attempt++;

                if ($attempt >= $this->maxRetries) {
                    break;
                }

                // Exponential backoff
                $delay = $this->baseDelay * (2 ** ($attempt - 1));
                usleep($delay * 1000); // Convert to microseconds
            }
        }

        throw $lastException ?? new \RuntimeException('Review failed after retries');
    }

    /**
     * Check if exception is retryable
     */
    public function isRetryable(\Exception $e): bool
    {
        // Retry on rate limits, timeouts, and server errors
        $message = strtolower($e->getMessage());
        
        return str_contains($message, 'rate limit') ||
               str_contains($message, 'timeout') ||
               str_contains($message, '503') ||
               str_contains($message, '429') ||
               str_contains($message, '500');
    }
}

Review History and Analytics

php
<?php
# filename: src/CodeReview/ReviewAnalytics.php
declare(strict_types=1);

namespace App\CodeReview;

class ReviewAnalytics
{
    private array $history = [];

    /**
     * Record review result
     */
    public function recordReview(int $prNumber, array $result): void
    {
        $this->history[] = [
            'pr' => $prNumber,
            'timestamp' => time(),
            'issues_found' => count($result['comments'] ?? []),
            'critical_count' => count(array_filter($result['comments'] ?? [], fn($c) => $c['severity'] === 'CRITICAL')),
            'high_count' => count(array_filter($result['comments'] ?? [], fn($c) => $c['severity'] === 'HIGH')),
            'files_reviewed' => count($result['files'] ?? []),
            'cost' => $result['cost'] ?? 0
        ];
    }

    /**
     * Get common issues
     */
    public function getCommonIssues(int $limit = 10): array
    {
        $issueCounts = [];
        
        foreach ($this->history as $review) {
            // Aggregate issue types
            // Implementation depends on storing issue details
        }
        
        arsort($issueCounts);
        return array_slice($issueCounts, 0, $limit, true);
    }

    /**
     * Get review statistics
     */
    public function getStatistics(): array
    {
        $totalReviews = count($this->history);
        $totalIssues = array_sum(array_column($this->history, 'issues_found'));
        $totalCost = array_sum(array_column($this->history, 'cost'));
        
        return [
            'total_reviews' => $totalReviews,
            'average_issues_per_review' => $totalReviews > 0 ? $totalIssues / $totalReviews : 0,
            'total_cost' => $totalCost,
            'average_cost_per_review' => $totalReviews > 0 ? $totalCost / $totalReviews : 0,
            'critical_issues_found' => array_sum(array_column($this->history, 'critical_count'))
        ];
    }
}

Custom Rules Configuration

php
<?php
# filename: src/CodeReview/CustomRules.php
declare(strict_types=1);

namespace App\CodeReview;

class CustomRules
{
    public function __construct(
        private array $rules = []
    ) {}

    /**
     * Add custom rule
     */
    public function addRule(string $name, string $pattern, string $severity, string $message): void
    {
        $this->rules[$name] = [
            'pattern' => $pattern,
            'severity' => $severity,
            'message' => $message
        ];
    }

    /**
     * Check code against custom rules
     */
    public function checkCode(string $code, string $filename): array
    {
        $issues = [];
        $lines = explode("\n", $code);

        foreach ($lines as $lineNum => $line) {
            foreach ($this->rules as $ruleName => $rule) {
                if (preg_match($rule['pattern'], $line)) {
                    $issues[] = [
                        'type' => 'custom_rule',
                        'rule' => $ruleName,
                        'severity' => $rule['severity'],
                        'line' => $lineNum + 1,
                        'message' => $rule['message'],
                        'code' => trim($line)
                    ];
                }
            }
        }

        return $issues;
    }

    /**
     * Load rules from config
     */
    public static function fromConfig(array $config): self
    {
        $rules = new self();
        
        foreach ($config as $name => $rule) {
            $rules->addRule(
                $name,
                $rule['pattern'],
                $rule['severity'],
                $rule['message']
            );
        }
        
        return $rules;
    }
}

Queue-Based Processing

php
<?php
# filename: app/Jobs/ProcessCodeReview.php
declare(strict_types=1);

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\CodeReview\ReviewSystem;

class ProcessCodeReview implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private int $prNumber,
        private string $provider = 'github' // 'github' or 'gitlab'
    ) {}

    public function handle(ReviewSystem $reviewSystem): void
    {
        try {
            $result = $reviewSystem->reviewPullRequest($this->prNumber);
            
            // Store result in database, send notification, etc.
            \Log::info("Review completed for PR #{$this->prNumber}", [
                'issues_found' => count($result->comments),
                'cost' => $result->cost ?? 0
            ]);
        } catch (\Exception $e) {
            \Log::error("Review failed for PR #{$this->prNumber}: " . $e->getMessage());
            throw $e; // Will trigger retry
        }
    }

    public function retryUntil(): \DateTime
    {
        return now()->addHours(1);
    }

    public function backoff(): array
    {
        return [10, 30, 60]; // Retry after 10s, 30s, 60s
    }
}

Complete Integration Example

php
<?php
# filename: webhook.php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Anthropic\Anthropic;
use App\CodeReview\ReviewSystem;
use App\CodeReview\GitHub\GitHubClient;
use App\CodeReview\GitLab\GitLabClient;
use App\CodeReview\GitHub\WebhookHandler;
use App\CodeReview\ReviewConfig;
use App\CodeReview\SecurityScanner;
use App\CodeReview\BugDetector;
use App\CodeReview\CommentGenerator;
use App\CodeReview\ReviewCache;
use App\CodeReview\ReviewRateLimiter;
use App\CodeReview\CostTracker;
use App\CodeReview\ReviewRetry;
use App\CodeReview\ReviewAnalytics;
use App\CodeReview\CustomRules;
use Psr\SimpleCache\CacheInterface;

// Initialize cache (Redis, Memcached, etc.)
$cache = new \Symfony\Component\Cache\Simple\RedisCache(
    new \Redis(),
    'code_review_'
);

// Initialize Claude
$claude = Anthropic::factory()
    ->withApiKey(getenv('ANTHROPIC_API_KEY'))
    ->make();

// Initialize GitHub or GitLab client
$provider = getenv('GIT_PROVIDER') ?: 'github';
if ($provider === 'gitlab') {
    $gitClient = new GitLabClient(
        token: getenv('GITLAB_TOKEN'),
        projectId: getenv('GITLAB_PROJECT_ID')
    );
} else {
    $gitClient = new GitHubClient(
        token: getenv('GITHUB_TOKEN'),
        repository: getenv('GITHUB_REPOSITORY')
    );
}

// Configure review system with all features
$config = new ReviewConfig(
    autoPost: true,
    minSeverity: 'MEDIUM',
    enableSecurityScan: true,
    enableBugDetection: true,
    maxComments: 50,
    enableCaching: true,
    enableRateLimiting: true,
    enableCostTracking: true,
    enableStatusChecks: true,
    incrementalReview: true,
    reviewTemplate: getenv('REVIEW_TEMPLATE'),
    customRules: json_decode(getenv('CUSTOM_RULES') ?: '[]', true)
);

// Initialize components
$securityScanner = new SecurityScanner($claude);
$bugDetector = new BugDetector($claude);
$commentGenerator = new CommentGenerator($claude, $config->reviewTemplate);
$reviewCache = new ReviewCache($cache);
$rateLimiter = new ReviewRateLimiter($cache);
$costTracker = new CostTracker();
$retryLogic = new ReviewRetry();
$analytics = new ReviewAnalytics();
$customRules = CustomRules::fromConfig($config->customRules);

// Initialize review system
$reviewSystem = new ReviewSystem(
    claude: $claude,
    github: $gitClient,
    config: $config,
    securityScanner: $securityScanner,
    bugDetector: $bugDetector,
    commentGenerator: $commentGenerator
);

// Initialize webhook handler
$webhookHandler = new WebhookHandler(
    github: $gitClient,
    reviewSystem: $reviewSystem,
    webhookSecret: getenv('GITHUB_WEBHOOK_SECRET')
);

// Handle incoming webhook
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';

try {
    // Check rate limiting
    $identifier = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    if (!$rateLimiter->allow($identifier)) {
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        exit;
    }

    // Process webhook with retry logic
    $result = $retryLogic->executeWithRetry(function() use ($webhookHandler, $payload, $signature) {
        return $webhookHandler->handle($payload, $signature);
    });

    // Track costs and analytics
    if (isset($result['pr'])) {
        $analytics->recordReview($result['pr'], $result);
    }

    http_response_code(200);
    header('Content-Type: application/json');
    echo json_encode($result ?? ['status' => 'ok']);

} catch (\Exception $e) {
    error_log("Webhook error: " . $e->getMessage());
    http_response_code(500);
    echo json_encode(['error' => 'Internal server error']);
}

CLI Tool for Manual Reviews

php
<?php
# filename: bin/review-pr.php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use App\CodeReview\ReviewSystem;

if ($argc < 2) {
    echo "Usage: php review-pr.php <pr-number>\n";
    exit(1);
}

$prNumber = (int)$argv[1];

// Initialize system (same as webhook.php)
// ...

echo "🔍 Reviewing PR #{$prNumber}...\n\n";

try {
    $result = $reviewSystem->reviewPullRequest($prNumber);

    echo "✅ Review complete!\n\n";
    echo "Summary:\n";
    echo $result->summary . "\n\n";

    echo "Issues found: " . count($result->comments) . "\n";

    foreach ($result->comments as $comment) {
        echo "\n---\n";
        echo "File: {$comment['path']}:{$comment['line']}\n";
        echo "Severity: {$comment['severity']}\n";
        echo $comment['body'] . "\n";
    }

} catch (\Exception $e) {
    echo "❌ Error: " . $e->getMessage() . "\n";
    exit(1);
}

Supporting Classes

ReviewConfig

php
<?php
# filename: src/CodeReview/ReviewConfig.php
declare(strict_types=1);

namespace App\CodeReview;

class ReviewConfig
{
    public function __construct(
        public bool $autoPost = true,
        public string $minSeverity = 'MEDIUM',
        public bool $enableSecurityScan = true,
        public bool $enableBugDetection = true,
        public int $maxComments = 50,
        public bool $enableCaching = true,
        public bool $enableRateLimiting = true,
        public bool $enableCostTracking = true,
        public bool $enableStatusChecks = true,
        public bool $incrementalReview = true,
        public ?string $reviewTemplate = null,
        public array $customRules = []
    ) {}
}

ReviewResult

php
<?php
# filename: src/CodeReview/ReviewResult.php
declare(strict_types=1);

namespace App\CodeReview;

class ReviewResult
{
    public function __construct(
        public array $analysis,
        public array $comments,
        public string $summary
    ) {}
}

Testing the Review System

php
<?php
# filename: tests/CodeReview/SecurityScannerTest.php
declare(strict_types=1);

namespace Tests\CodeReview;

use PHPUnit\Framework\TestCase;
use App\CodeReview\SecurityScanner;
use Anthropic\Anthropic;
use Mockery;

class SecurityScannerTest extends TestCase
{
    protected function tearDown(): void
    {
        Mockery::close();
    }

    private function getMockClaude(): Anthropic
    {
        $mock = Mockery::mock(Anthropic::class);
        $mockResponse = (object)[
            'content' => [(object)['text' => '[]']]
        ];
        $mock->shouldReceive('messages->create')->andReturn($mockResponse);
        return $mock;
    }

    public function testDetectsSqlInjection(): void
    {
        $scanner = new SecurityScanner($this->getMockClaude());

        $code = <<<'PHP'
$userId = $_GET['id'];
$query = "SELECT * FROM users WHERE id = " . $userId;
$result = $db->query($query);
PHP;

        $issues = $scanner->scanCode($code, 'test.php');

        $this->assertNotEmpty($issues);
        $this->assertStringContainsString('injection', strtolower($issues[0]['message']));
    }

    public function testDetectsXss(): void
    {
        $scanner = new SecurityScanner($this->getMockClaude());

        $code = <<<'PHP'
echo $_GET['name'];
PHP;

        $issues = $scanner->scanCode($code, 'test.php');

        $this->assertNotEmpty($issues);
        $this->assertEquals('CRITICAL', $issues[0]['severity']);
    }
}

Wrap-up

Congratulations! You've built a complete, production-ready code review assistant powered by Claude. Here's what you've accomplished:

  • Diff Parser — Parse and analyze Git unified diff format to extract changed code
  • Security Scanner — Combine pattern matching with AI-powered deep analysis to detect vulnerabilities
  • Bug Detector — Identify logic errors, performance issues, and code quality problems
  • GitHub Integration — Full API client for fetching PRs, diffs, and posting reviews
  • Webhook Handler — Automated review triggering on PR events with signature verification
  • Comment Generator — Intelligent, prioritized review comments with severity levels
  • Review Summary — Comprehensive summaries with issue counts and actionable feedback
  • CLI Tool — Manual review capability for testing and on-demand reviews
  • Test Suite — Validation tests to ensure accurate issue detection
  • GitLab Integration — Full GitLab API client for merge request reviews
  • Review Decision Logic — Automatic approval/rejection based on issue severity
  • Caching System — Cache reviews of unchanged code to reduce API costs
  • Rate Limiting — Prevent API throttling with per-minute and per-hour limits
  • Cost Tracking — Monitor API costs per review with daily summaries
  • Incremental Reviews — Review only changed lines, not entire files
  • Retry Logic — Automatic retry with exponential backoff for failed reviews
  • Review Analytics — Track review patterns, common issues, and statistics
  • Custom Rules — Team-specific rules and patterns beyond default checks
  • CI/CD Status Checks — Block merges on critical issues via status checks
  • Queue Processing — Async processing for large PRs using Laravel queues
  • Review Templates — Customizable comment formats for team preferences

Your code review assistant now automates the tedious parts of code review while maintaining the quality and context awareness that only AI can provide. It integrates seamlessly into your development workflow through webhooks, provides actionable feedback that helps teams maintain high code quality standards, and includes production-ready features like caching, rate limiting, cost tracking, and analytics.

In the next chapter, you'll build a documentation generator that uses Claude to automatically create and maintain project documentation.

Key Takeaways

  • ✓ Code review automation saves time and improves code quality consistently
  • ✓ Claude excels at understanding code context and suggesting improvements
  • ✓ Security scanning should combine pattern matching with AI analysis
  • ✓ GitHub/GitLab webhooks enable seamless integration into development workflow
  • ✓ Structured output ensures consistent, actionable review comments
  • ✓ Severity levels help prioritize which issues to address first
  • ✓ Automated reviews augment, not replace, human code reviews
  • ✓ Comments should be educational and provide code examples
  • ✓ Rate limiting and error handling are critical for production use
  • ✓ Testing your review system prevents false positives
  • ✓ Caching unchanged code reviews can reduce API costs by 80-90%
  • ✓ Incremental reviews (changed lines only) significantly reduce token usage
  • ✓ Cost tracking helps budget and optimize API usage
  • ✓ Review analytics provide insights into code quality trends
  • ✓ Custom rules allow teams to enforce project-specific standards
  • ✓ CI/CD status checks prevent merging code with critical issues
  • ✓ Queue-based processing handles large PRs without blocking webhooks
  • ✓ Retry logic ensures reviews complete even with transient API failures
  • ✓ GitLab support enables teams using either platform

Further Reading


Continue to Chapter 27: Documentation Generator to automate documentation creation.

💻 Code Samples

All code examples from this chapter are available in the GitHub repository:

View Chapter 26 Code Samples

Clone and run locally:

bash
git clone https://github.com/dalehurley/codewithphp.git
cd codewithphp/code/claude-php/chapter-26
composer install
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
export GITHUB_TOKEN="ghp_your-token"
php bin/review-pr.php 123