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
Recommended Workflow
- Developer pushes code → PR
- AI reviews immediately
- CI checks (tests, lint)
- Human reviews if AI approves
- Merge when both AI and human approve