Automated AI Code Review: Integrating with CI/CD in Laravel

· 14 min read

Introduction

Code review is one of the most important practices for ensuring code quality. However, manual code review is time-consuming and inconsistent. AI code review can complement human review by:

  • Detecting bugs and security issues
  • Checking coding standards
  • Suggesting improvements
  • Reviewing 24/7 without fatigue

AI Code Review vs Human Review

Factor AI Review Human Review
Speed Seconds Hours/Days
Availability 24/7 Working hours
Consistency 100% Varies
Context understanding Limited Excellent
Business logic Basic Deep
Creativity Low High

Best practice: Combine both - AI review first, human review after.

Architecture

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   GitHub    │────▶│  GitHub      │────▶│   Laravel   │
│   PR/Push   │     │  Actions     │     │   API       │
└─────────────┘     └──────────────┘     └─────────────┘
                                               │
                                               ▼
                                         ┌─────────────┐
                                         │   OpenAI/   │
                                         │   Claude    │
                                         └─────────────┘
                                               │
                                               ▼
                                         ┌─────────────┐
                                         │   GitHub    │
                                         │   Comment   │
                                         └─────────────┘

Setting Up Laravel API

Routes and Controller

// routes/api.php
use App\Http\Controllers\CodeReviewController;

Route::prefix('code-review')->group(function () {
    Route::post('/', [CodeReviewController::class, 'review']);
    Route::post('/webhook', [CodeReviewController::class, 'handleWebhook']);
});
// app/Http/Controllers/CodeReviewController.php
namespace App\Http\Controllers;

use App\Services\CodeReview\CodeReviewService;
use App\Services\CodeReview\GitHubService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class CodeReviewController extends Controller
{
    public function __construct(
        private CodeReviewService $reviewService,
        private GitHubService $githubService,
    ) {}
    
    public function review(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'code' => 'required|string',
            'language' => 'nullable|string',
            'context' => 'nullable|string',
        ]);
        
        $review = $this->reviewService->review(
            $validated['code'],
            $validated['language'] ?? 'php',
            $validated['context'] ?? null
        );
        
        return response()->json($review);
    }
    
    public function handleWebhook(Request $request): JsonResponse
    {
        // Verify GitHub signature
        if (!$this->verifyGitHubSignature($request)) {
            return response()->json(['error' => 'Invalid signature'], 401);
        }
        
        $event = $request->header('X-GitHub-Event');
        $payload = $request->all();
        
        match ($event) {
            'pull_request' => $this->handlePullRequest($payload),
            'push' => $this->handlePush($payload),
            default => null,
        };
        
        return response()->json(['status' => 'ok']);
    }
    
    protected function handlePullRequest(array $payload): void
    {
        if (!in_array($payload['action'], ['opened', 'synchronize'])) {
            return;
        }
        
        $pr = $payload['pull_request'];
        $repo = $payload['repository']['full_name'];
        $prNumber = $pr['number'];
        
        // Get changed files
        $files = $this->githubService->getPRFiles($repo, $prNumber);
        
        // Review each file
        $reviews = [];
        foreach ($files as $file) {
            if ($this->shouldReview($file)) {
                $content = $this->githubService->getFileContent($repo, $file['sha']);
                $review = $this->reviewService->review($content, $this->getLanguage($file['filename']));
                $reviews[$file['filename']] = $review;
            }
        }
        
        // Post review comment
        if (!empty($reviews)) {
            $this->githubService->createReview($repo, $prNumber, $reviews);
        }
    }
    
    protected function shouldReview(array $file): bool
    {
        $extensions = ['php', 'js', 'ts', 'vue', 'blade.php'];
        $filename = $file['filename'];
        
        foreach ($extensions as $ext) {
            if (str_ends_with($filename, ".{$ext}")) {
                return true;
            }
        }
        
        return false;
    }
    
    protected function verifyGitHubSignature(Request $request): bool
    {
        $signature = $request->header('X-Hub-Signature-256');
        if (!$signature) return false;
        
        $secret = config('services.github.webhook_secret');
        $payload = $request->getContent();
        
        $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
        
        return hash_equals($expected, $signature);
    }
}

Code Review Service

// app/Services/CodeReview/CodeReviewService.php
namespace App\Services\CodeReview;

use OpenAI\Laravel\Facades\OpenAI;

class CodeReviewService
{
    private array $reviewCriteria = [
        'bugs' => 'Identify potential bugs, null pointer exceptions, infinite loops',
        'security' => 'Find security vulnerabilities: SQL injection, XSS, CSRF, insecure auth',
        'performance' => 'Detect performance issues: N+1 queries, memory leaks, slow algorithms',
        'style' => 'Check coding standards: PSR-12, naming conventions, formatting',
        'best_practices' => 'Suggest Laravel best practices and design patterns',
        'documentation' => 'Check for missing PHPDoc, unclear code',
    ];
    
    public function review(string $code, string $language = 'php', ?string $context = null): array
    {
        $prompt = $this->buildPrompt($code, $language, $context);
        
        $response = OpenAI::chat()->create([
            'model' => config('ai.code_review_model', 'gpt-4o'),
            'messages' => [
                [
                    'role' => 'system',
                    'content' => $this->getSystemPrompt()
                ],
                [
                    'role' => 'user',
                    'content' => $prompt
                ]
            ],
            'response_format' => ['type' => 'json_object'],
            'temperature' => 0.3,
        ]);
        
        $result = json_decode($response->choices[0]->message->content, true);
        
        return $this->formatReview($result);
    }
    
    public function reviewDiff(string $diff, string $context = ''): array
    {
        $response = OpenAI::chat()->create([
            'model' => config('ai.code_review_model', 'gpt-4o'),
            'messages' => [
                [
                    'role' => 'system',
                    'content' => $this->getDiffReviewPrompt()
                ],
                [
                    'role' => 'user',
                    'content' => "Context: {$context}\n\nDiff:\n```diff\n{$diff}\n```"
                ]
            ],
            'response_format' => ['type' => 'json_object'],
            'temperature' => 0.3,
        ]);
        
        return json_decode($response->choices[0]->message->content, true);
    }
    
    protected function getSystemPrompt(): string
    {
        return <<<PROMPT
You are an expert code reviewer specializing in PHP and Laravel applications.

Review the code thoroughly and provide feedback in JSON format:
{
    "summary": "Brief overview of the code quality",
    "score": 1-10,
    "issues": [
        {
            "type": "bug|security|performance|style|best_practice",
            "severity": "critical|high|medium|low",
            "line": 123,
            "message": "Description of the issue",
            "suggestion": "How to fix it",
            "code_example": "Optional corrected code"
        }
    ],
    "positives": ["List of good practices found"],
    "improvements": ["General suggestions for improvement"]
}

Be constructive and specific. Provide actionable feedback with code examples when helpful.
PROMPT;
    }
    
    protected function getDiffReviewPrompt(): string
    {
        return <<<PROMPT
You are reviewing a code diff (pull request changes).
Focus on:
1. New bugs introduced
2. Security issues in new code
3. Breaking changes
4. Missing tests for new functionality
5. Code style consistency

Respond in JSON:
{
    "approved": true|false,
    "summary": "Brief assessment",
    "line_comments": [
        {
            "path": "file/path.php",
            "line": 123,
            "side": "RIGHT",
            "body": "Comment text"
        }
    ],
    "general_comments": ["List of general feedback"]
}
PROMPT;
    }
    
    protected function buildPrompt(string $code, string $language, ?string $context): string
    {
        $prompt = "Review this {$language} code:\n\n```{$language}\n{$code}\n```";
        
        if ($context) {
            $prompt = "Context: {$context}\n\n" . $prompt;
        }
        
        $prompt .= "\n\nReview for:\n";
        foreach ($this->reviewCriteria as $category => $description) {
            $prompt .= "- {$category}: {$description}\n";
        }
        
        return $prompt;
    }
    
    protected function formatReview(array $result): array
    {
        // Sort issues by severity
        $severityOrder = ['critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3];
        
        usort($result['issues'], function ($a, $b) use ($severityOrder) {
            return ($severityOrder[$a['severity']] ?? 4) <=> ($severityOrder[$b['severity']] ?? 4);
        });
        
        return $result;
    }
}

GitHub Integration Service

// app/Services/CodeReview/GitHubService.php
namespace App\Services\CodeReview;

use Illuminate\Support\Facades\Http;

class GitHubService
{
    private string $baseUrl = 'https://api.github.com';
    private string $token;
    
    public function __construct()
    {
        $this->token = config('services.github.token');
    }
    
    public function getPRFiles(string $repo, int $prNumber): array
    {
        $response = Http::withToken($this->token)
            ->get("{$this->baseUrl}/repos/{$repo}/pulls/{$prNumber}/files");
        
        return $response->json();
    }
    
    public function getPRDiff(string $repo, int $prNumber): string
    {
        $response = Http::withToken($this->token)
            ->withHeaders(['Accept' => 'application/vnd.github.v3.diff'])
            ->get("{$this->baseUrl}/repos/{$repo}/pulls/{$prNumber}");
        
        return $response->body();
    }
    
    public function getFileContent(string $repo, string $sha): string
    {
        $response = Http::withToken($this->token)
            ->get("{$this->baseUrl}/repos/{$repo}/git/blobs/{$sha}");
        
        $data = $response->json();
        
        return base64_decode($data['content']);
    }
    
    public function createReview(string $repo, int $prNumber, array $reviews): void
    {
        $comments = [];
        $body = "## 🤖 AI Code Review\n\n";
        
        foreach ($reviews as $filename => $review) {
            $body .= "### 📄 {$filename}\n\n";
            $body .= "**Score:** {$review['score']}/10\n\n";
            $body .= "**Summary:** {$review['summary']}\n\n";
            
            if (!empty($review['issues'])) {
                $body .= "#### Issues Found:\n\n";
                foreach ($review['issues'] as $issue) {
                    $emoji = match($issue['severity']) {
                        'critical' => '🔴',
                        'high' => '🟠',
                        'medium' => '🟡',
                        'low' => '🟢',
                        default => '⚪'
                    };
                    
                    $body .= "{$emoji} **{$issue['type']}** (Line {$issue['line']}): {$issue['message']}\n";
                    
                    if (isset($issue['suggestion'])) {
                        $body .= "   💡 {$issue['suggestion']}\n";
                    }
                    
                    if (isset($issue['code_example'])) {
                        $body .= "   ```php\n   {$issue['code_example']}\n   ```\n";
                    }
                    
                    $body .= "\n";
                    
                    // Add line comment
                    if ($issue['line']) {
                        $comments[] = [
                            'path' => $filename,
                            'line' => $issue['line'],
                            'body' => "{$emoji} **{$issue['type']}**: {$issue['message']}\n\n💡 {$issue['suggestion']}"
                        ];
                    }
                }
            }
            
            if (!empty($review['positives'])) {
                $body .= "#### ✅ Good Practices:\n";
                foreach ($review['positives'] as $positive) {
                    $body .= "- {$positive}\n";
                }
                $body .= "\n";
            }
        }
        
        // Create PR review with comments
        Http::withToken($this->token)->post(
            "{$this->baseUrl}/repos/{$repo}/pulls/{$prNumber}/reviews",
            [
                'body' => $body,
                'event' => 'COMMENT',
                'comments' => $comments
            ]
        );
    }
    
    public function addComment(string $repo, int $issueNumber, string $body): void
    {
        Http::withToken($this->token)->post(
            "{$this->baseUrl}/repos/{$repo}/issues/{$issueNumber}/comments",
            ['body' => $body]
        );
    }
}

GitHub Actions Integration

Workflow File

# .github/workflows/ai-code-review.yml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v44
        with:
          files: |
            **/*.php
            **/*.js
            **/*.ts
            **/*.vue

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: AI Review
        if: steps.changed-files.outputs.any_changed == 'true'
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
        run: |
          # Install dependencies
          composer global require openai-php/client
          
          # Create review script
          cat > review.php << 'EOF'
          <?php
          require_once getenv('HOME') . '/.composer/vendor/autoload.php';
          
          $client = OpenAI::client(getenv('OPENAI_API_KEY'));
          $token = getenv('GITHUB_TOKEN');
          $repo = getenv('REPO');
          $prNumber = getenv('PR_NUMBER');
          
          // Get changed files from args
          $files = array_slice($argv, 1);
          $reviews = [];
          
          foreach ($files as $file) {
              if (!file_exists($file)) continue;
              
              $content = file_get_contents($file);
              $extension = pathinfo($file, PATHINFO_EXTENSION);
              
              $response = $client->chat()->create([
                  'model' => 'gpt-4o',
                  'messages' => [
                      [
                          'role' => 'system',
                          'content' => 'Review code briefly. Return JSON: {"issues": [{"line": N, "severity": "high|medium|low", "message": "..."}], "summary": "..."}'
                      ],
                      [
                          'role' => 'user',
                          'content' => "Review this {$extension} code:\n\n```{$extension}\n{$content}\n```"
                      ]
                  ],
                  'response_format' => ['type' => 'json_object'],
              ]);
              
              $review = json_decode($response->choices[0]->message->content, true);
              if (!empty($review['issues'])) {
                  $reviews[$file] = $review;
              }
          }
          
          if (empty($reviews)) {
              echo "No issues found!\n";
              exit(0);
          }
          
          // Build comment
          $comment = "## 🤖 AI Code Review\n\n";
          foreach ($reviews as $file => $review) {
              $comment .= "### 📄 {$file}\n{$review['summary']}\n\n";
              foreach ($review['issues'] as $issue) {
                  $emoji = match($issue['severity']) {
                      'high' => '🔴',
                      'medium' => '🟡',
                      default => '🟢'
                  };
                  $comment .= "{$emoji} Line {$issue['line']}: {$issue['message']}\n";
              }
              $comment .= "\n";
          }
          
          // Post comment
          $ch = curl_init("https://api.github.com/repos/{$repo}/issues/{$prNumber}/comments");
          curl_setopt_array($ch, [
              CURLOPT_POST => true,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_HTTPHEADER => [
                  "Authorization: Bearer {$token}",
                  "Accept: application/vnd.github.v3+json",
                  "User-Agent: AI-Code-Review",
                  "Content-Type: application/json"
              ],
              CURLOPT_POSTFIELDS => json_encode(['body' => $comment])
          ]);
          curl_exec($ch);
          curl_close($ch);
          EOF
          
          php review.php ${{ steps.changed-files.outputs.all_changed_files }}

Custom GitHub Action

# .github/actions/ai-review/action.yml
name: 'AI Code Review'
description: 'Review code changes with AI'
inputs:
  openai-api-key:
    description: 'OpenAI API Key'
    required: true
  github-token:
    description: 'GitHub Token'
    required: true
  review-endpoint:
    description: 'Custom review API endpoint'
    required: false
  model:
    description: 'AI model to use'
    required: false
    default: 'gpt-4o'
  
runs:
  using: 'composite'
  steps:
    - name: Get PR diff
      shell: bash
      run: |
        curl -H "Authorization: Bearer ${{ inputs.github-token }}" \
             -H "Accept: application/vnd.github.v3.diff" \
             "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \
             > pr.diff
             
    - name: Review with AI
      shell: bash
      env:
        OPENAI_API_KEY: ${{ inputs.openai-api-key }}
      run: |
        # Call review API or use OpenAI directly
        if [ -n "${{ inputs.review-endpoint }}" ]; then
          curl -X POST "${{ inputs.review-endpoint }}" \
               -H "Content-Type: application/json" \
               -d @pr.diff > review.json
        else
          # Direct OpenAI call
          DIFF=$(cat pr.diff | jq -Rs .)
          
          curl https://api.openai.com/v1/chat/completions \
            -H "Authorization: Bearer $OPENAI_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{
              \"model\": \"${{ inputs.model }}\",
              \"messages\": [
                {\"role\": \"system\", \"content\": \"Review this diff. Return JSON with issues array.\"},
                {\"role\": \"user\", \"content\": $DIFF}
              ],
              \"response_format\": {\"type\": \"json_object\"}
            }" > review.json
        fi
        
    - name: Post review comment
      shell: bash
      run: |
        REVIEW=$(cat review.json | jq -r '.choices[0].message.content // .')
        
        curl -X POST \
          "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
          -H "Authorization: Bearer ${{ inputs.github-token }}" \
          -H "Content-Type: application/json" \
          -d "{\"body\": \"## 🤖 AI Review\\n\\n\${REVIEW}\"}"

Advanced Features

Security-Focused Review

// app/Services/CodeReview/SecurityReviewer.php
namespace App\Services\CodeReview;

class SecurityReviewer
{
    private array $securityChecks = [
        'sql_injection' => [
            'patterns' => [
                '/DB::raw\s*\(\s*["\'].*\$/',
                '/->whereRaw\s*\(\s*["\'].*\$/',
                '/query\s*\(\s*["\'].*\$/',
            ],
            'severity' => 'critical',
            'message' => 'Potential SQL injection vulnerability',
        ],
        'xss' => [
            'patterns' => [
                '/\{\!\!\s*\$/',
                '/echo\s+\$/',
                '/print\s+\$/',
            ],
            'severity' => 'high',
            'message' => 'Potential XSS vulnerability - unescaped output',
        ],
        'mass_assignment' => [
            'patterns' => [
                '/->fill\s*\(\s*\$request->all\(\)\s*\)/',
                '/::create\s*\(\s*\$request->all\(\)\s*\)/',
            ],
            'severity' => 'high',
            'message' => 'Mass assignment vulnerability',
        ],
        'hardcoded_secrets' => [
            'patterns' => [
                '/password\s*=\s*["\'][^"\']+["\']/',
                '/api_key\s*=\s*["\'][^"\']+["\']/',
                '/secret\s*=\s*["\'][^"\']+["\']/',
            ],
            'severity' => 'critical',
            'message' => 'Hardcoded secret detected',
        ],
    ];
    
    public function scan(string $code): array
    {
        $issues = [];
        $lines = explode("\n", $code);
        
        foreach ($this->securityChecks as $type => $check) {
            foreach ($check['patterns'] as $pattern) {
                foreach ($lines as $lineNumber => $line) {
                    if (preg_match($pattern, $line)) {
                        $issues[] = [
                            'type' => 'security',
                            'subtype' => $type,
                            'severity' => $check['severity'],
                            'line' => $lineNumber + 1,
                            'message' => $check['message'],
                            'code' => trim($line),
                        ];
                    }
                }
            }
        }
        
        return $issues;
    }
    
    public function reviewWithAI(string $code): array
    {
        // Combine pattern-based + AI review
        $patternIssues = $this->scan($code);
        
        $aiReview = OpenAI::chat()->create([
            'model' => 'gpt-4o',
            'messages' => [
                [
                    'role' => 'system',
                    'content' => $this->getSecurityPrompt()
                ],
                [
                    'role' => 'user',
                    'content' => $code
                ]
            ],
            'response_format' => ['type' => 'json_object'],
        ]);
        
        $aiIssues = json_decode($aiReview->choices[0]->message->content, true)['issues'] ?? [];
        
        return [
            'pattern_based' => $patternIssues,
            'ai_detected' => $aiIssues,
            'combined' => $this->mergeIssues($patternIssues, $aiIssues),
        ];
    }
    
    private function getSecurityPrompt(): string
    {
        return <<<PROMPT
You are a security expert reviewing PHP/Laravel code.
Focus on OWASP Top 10 vulnerabilities:
1. Injection (SQL, Command, LDAP)
2. Broken Authentication
3. Sensitive Data Exposure
4. XML External Entities (XXE)
5. Broken Access Control
6. Security Misconfiguration
7. Cross-Site Scripting (XSS)
8. Insecure Deserialization
9. Using Components with Known Vulnerabilities
10. Insufficient Logging & Monitoring

Return JSON:
{
    "issues": [
        {
            "vulnerability": "OWASP category",
            "severity": "critical|high|medium|low",
            "line": 123,
            "message": "Description",
            "remediation": "How to fix"
        }
    ]
}
PROMPT;
    }
}

Performance Review

// app/Services/CodeReview/PerformanceReviewer.php
namespace App\Services\CodeReview;

class PerformanceReviewer
{
    public function review(string $code): array
    {
        $issues = [];
        
        // N+1 Query Detection
        if (preg_match_all('/->get\(\).*foreach|foreach.*->get\(\)/', $code, $matches)) {
            $issues[] = [
                'type' => 'performance',
                'severity' => 'high',
                'message' => 'Potential N+1 query detected. Consider eager loading with with().',
            ];
        }
        
        // Missing pagination
        if (preg_match('/::all\(\)/', $code) && !preg_match('/->paginate\(/', $code)) {
            $issues[] = [
                'type' => 'performance',
                'severity' => 'medium',
                'message' => 'Using all() without pagination may cause memory issues with large datasets.',
            ];
        }
        
        // Inefficient string concatenation in loops
        if (preg_match('/foreach.*\.\=/', $code)) {
            $issues[] = [
                'type' => 'performance',
                'severity' => 'low',
                'message' => 'String concatenation in loop. Consider using array and implode().',
            ];
        }
        
        return $issues;
    }
}

Configuration

// config/code-review.php
return [
    'model' => env('CODE_REVIEW_MODEL', 'gpt-4o'),
    
    'enabled_checks' => [
        'bugs' => true,
        'security' => true,
        'performance' => true,
        'style' => true,
        'best_practices' => true,
    ],
    
    'severity_threshold' => 'medium', // Only report medium+ issues
    
    'file_patterns' => [
        'include' => ['*.php', '*.js', '*.ts', '*.vue'],
        'exclude' => ['*.test.php', 'vendor/*', 'node_modules/*'],
    ],
    
    'max_file_size' => 50000, // bytes
    
    'github' => [
        'auto_approve' => false,
        'block_on_critical' => true,
        'comment_style' => 'inline', // 'inline' or 'summary'
    ],
];

Conclusion

AI code review doesn't replace human review but complements it:

  • Speed: Review immediately when PR is opened
  • Consistency: Check same criteria every time
  • Coverage: Review every file, every line
  • Learning: Team can learn from suggestions
  1. Developer pushes code → PR
  2. AI reviews immediately
  3. CI checks (tests, lint)
  4. Human reviews if AI approves
  5. Merge when both AI and human approve

References

Comments