AI Code Review Tự Động: Tích Hợp vào CI/CD với Laravel
·
15 min read
Giới Thiệu
Code review là một trong những practices quan trọng nhất để đảm bảo chất lượng code. Tuy nhiên, manual code review tốn thời gian và không consistent. AI code review có thể bổ sung cho human review bằng cách:
- Phát hiện bugs và security issues
- Kiểm tra coding standards
- Đề xuất improvements
- Review 24/7 không mệt mỏi
AI Code Review vs Human Review
| Yếu tố | AI Review | Human Review |
|---|---|---|
| Tốc độ | Seconds | Hours/Days |
| Availability | 24/7 | Working hours |
| Consistency | 100% | Varies |
| Context understanding | Limited | Excellent |
| Business logic | Basic | Deep |
| Creativity | Low | High |
Best practice: Kết hợp cả hai - AI review trước, human review sau.
Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ GitHub │────▶│ GitHub │────▶│ Laravel │
│ PR/Push │ │ Actions │ │ API │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ OpenAI/ │
│ Claude │
└─────────────┘
│
▼
┌─────────────┐
│ GitHub │
│ Comment │
└─────────────┘
Setup Laravel API
Routes và 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;
}
}
Review Dashboard
// app/Http/Controllers/ReviewDashboardController.php
namespace App\Http\Controllers;
use App\Models\CodeReview;
use Illuminate\Http\Request;
class ReviewDashboardController extends Controller
{
public function index()
{
$stats = [
'total_reviews' => CodeReview::count(),
'issues_found' => CodeReview::sum('issues_count'),
'critical_issues' => CodeReview::sum('critical_count'),
'avg_score' => CodeReview::avg('score'),
];
$recentReviews = CodeReview::with('pullRequest')
->latest()
->take(20)
->get();
$issuesByType = CodeReview::selectRaw('
JSON_EXTRACT(issues, "$[*].type") as types,
COUNT(*) as count
')
->groupBy('types')
->get();
return view('dashboard.reviews', compact('stats', 'recentReviews', 'issuesByType'));
}
public function show(CodeReview $review)
{
return view('dashboard.review-detail', compact('review'));
}
}
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'
],
];
Kết Luận
AI code review không thay thế human review mà bổ sung cho nó:
- Tốc độ: Review ngay khi PR được mở
- Consistency: Kiểm tra cùng tiêu chí mỗi lần
- Coverage: Review mọi file, mọi line
- Learning: Team có thể học từ suggestions
Workflow Recommended
- Developer push code → PR
- AI review ngay lập tức
- CI checks (tests, lint)
- Human review nếu AI approve
- Merge khi cả AI và human approve