Xây Dựng AI-Powered CLI Tools với Laravel Zero
·
13 min read
Laravel Zero là micro-framework cho CLI apps standalone. Kết hợp với LLM APIs, bạn tạo được công cụ mạnh — code reviewers, content generators, data analyzers — tất cả phân phối dưới dạng một file binary. Không cần deploy web server, không cần browser.
Bài viết này build 3 tools thực tế: code reviewer, interactive chat, và content generator — từ setup đến PHAR binary.
Tại Sao Laravel Zero?
| Feature | Laravel Zero | Vanilla PHP | Symfony Console |
|---|---|---|---|
| Setup | 1 command | Manual | Moderate |
| DI Container | Có (Laravel) | Không | Có |
| HTTP Client | Http:: facade |
cURL/Guzzle | Guzzle |
| Interactive prompts | Laravel Prompts | readline() | QuestionHelper |
| PHAR binary | Built-in | Manual | Manual |
| Ecosystem | Laravel packages | DIY | Symfony packages |
Laravel Zero = Laravel power cho CLI. Bạn có service container, config, logging, HTTP client — tất cả trong CLI app.
Cài Đặt
composer create-project laravel-zero/laravel-zero ai-cli
cd ai-cli
# Cài đặt components cần thiết
php ai-cli app:install http # HTTP client (gọi APIs)
php ai-cli app:install dotenv # .env support (API keys)
php ai-cli app:install log # Logging
Environment
# .env
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_MODEL=gpt-4o
DEFAULT_LANGUAGE=vi
Config
// config/ai.php
return [
'api_key' => env('OPENAI_API_KEY'),
'model' => env('OPENAI_MODEL', 'gpt-4o'),
'max_tokens' => env('AI_MAX_TOKENS', 4096),
'temperature' => env('AI_TEMPERATURE', 0.7),
];
AI Service — Shared Foundation
// app/Services/AiService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class AiService
{
public function chat(array $messages, float $temperature = null): string
{
$response = Http::withToken(config('ai.api_key'))
->timeout(120)
->post('https://api.openai.com/v1/chat/completions', [
'model' => config('ai.model'),
'messages' => $messages,
'max_tokens' => config('ai.max_tokens'),
'temperature' => $temperature ?? config('ai.temperature'),
]);
if ($response->failed()) {
throw new \RuntimeException(
"AI API call failed: {$response->status()} - {$response->body()}"
);
}
return $response->json('choices.0.message.content', 'AI response failed');
}
/**
* Stream response — yield từng chunk text
*/
public function stream(array $messages): \Generator
{
$response = Http::withToken(config('ai.api_key'))
->withOptions(['stream' => true])
->timeout(120)
->post('https://api.openai.com/v1/chat/completions', [
'model' => config('ai.model'),
'messages' => $messages,
'max_tokens' => config('ai.max_tokens'),
'stream' => true,
]);
$body = $response->getBody();
$buffer = '';
while (!$body->eof()) {
$buffer .= $body->read(1024);
while (($pos = strpos($buffer, "\n")) !== false) {
$line = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 1);
if (!str_starts_with($line, 'data: ')) continue;
$data = substr($line, 6);
if ($data === '[DONE]') return;
$chunk = json_decode($data, true);
$content = $chunk['choices'][0]['delta']['content'] ?? '';
if ($content !== '') yield $content;
}
}
}
}
Tool 1: Code Review
// app/Commands/ReviewCommand.php
namespace App\Commands;
use App\Services\AiService;
use LaravelZero\Framework\Commands\Command;
use function Laravel\Prompts\{select, spin, info, error, note};
class ReviewCommand extends Command
{
protected $signature = 'review {file} {--focus=general : Loại review: security, performance, general}';
protected $description = 'AI-powered code review cho PHP/Laravel files';
public function handle(AiService $ai): int
{
$filePath = $this->argument('file');
if (!file_exists($filePath)) {
error("File không tồn tại: {$filePath}");
return Command::FAILURE;
}
$code = file_get_contents($filePath);
$language = $this->detectLanguage($filePath);
$focus = $this->option('focus');
$fileSize = strlen($code);
if ($fileSize > 50_000) {
error("File quá lớn ({$fileSize} bytes). Tối đa 50KB.");
return Command::FAILURE;
}
note("Reviewing: {$filePath} ({$language}, {$fileSize} bytes, focus: {$focus})");
$systemPrompt = $this->buildSystemPrompt($focus, $language);
$review = spin(
callback: fn () => $ai->chat([
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => "Review code sau:\n\n```{$language}\n{$code}\n```"],
], temperature: 0.3), // Low temperature cho analysis chính xác
message: 'AI đang review code...',
);
$this->newLine();
$this->line($review);
$this->newLine();
// Lưu review vào file nếu muốn
if ($this->confirm('Lưu review vào file?', false)) {
$reviewFile = $filePath . '.review.md';
file_put_contents($reviewFile, "# Code Review: {$filePath}\n\n{$review}");
info("Review đã lưu: {$reviewFile}");
}
return Command::SUCCESS;
}
private function buildSystemPrompt(string $focus, string $language): string
{
$base = "Bạn là expert {$language} code reviewer. Phân tích kỹ lưỡng và đưa ra gợi ý cụ thể, actionable.";
return match ($focus) {
'security' => "{$base}\n\nTập trung vào:\n" .
"- OWASP Top 10 vulnerabilities\n" .
"- SQL injection, XSS, CSRF\n" .
"- Authentication/authorization issues\n" .
"- Sensitive data exposure\n" .
"- Input validation\n" .
"Đánh giá severity: Critical, High, Medium, Low",
'performance' => "{$base}\n\nTập trung vào:\n" .
"- N+1 queries\n" .
"- Unnecessary loops/iterations\n" .
"- Missing database indexes\n" .
"- Memory usage\n" .
"- Cache opportunities\n" .
"Ước lượng impact: High, Medium, Low",
default => "{$base}\n\nPhân tích:\n" .
"1. Code quality và readability\n" .
"2. Design patterns và architecture\n" .
"3. Potential bugs\n" .
"4. Testing gaps\n" .
"5. Laravel best practices\n" .
"6. Quick wins để cải thiện",
};
}
private function detectLanguage(string $filePath): string
{
return match (pathinfo($filePath, PATHINFO_EXTENSION)) {
'php' => 'php',
'js', 'mjs' => 'javascript',
'ts', 'tsx' => 'typescript',
'vue' => 'vue',
'py' => 'python',
'blade.php' => 'blade',
default => 'text',
};
}
}
Sử dụng:
ai-cli review app/Http/Controllers/UserController.php
ai-cli review app/Models/Order.php --focus=security
ai-cli review app/Services/PaymentService.php --focus=performance
Tool 2: Interactive Chat Mode
// app/Commands/ChatCommand.php
namespace App\Commands;
use App\Services\AiService;
use LaravelZero\Framework\Commands\Command;
use function Laravel\Prompts\{text, info, note};
class ChatCommand extends Command
{
protected $signature = 'chat {--system= : Custom system prompt} {--context= : File path làm context}';
protected $description = 'Interactive AI chat với streaming responses';
private array $messages = [];
public function handle(AiService $ai): int
{
$systemPrompt = $this->option('system') ?? 'Bạn là expert Laravel/PHP developer. Trả lời ngắn gọn, chính xác.';
$this->messages[] = ['role' => 'system', 'content' => $systemPrompt];
// Load file context nếu có
if ($contextFile = $this->option('context')) {
if (file_exists($contextFile)) {
$content = file_get_contents($contextFile);
$this->messages[] = [
'role' => 'system',
'content' => "Context file ({$contextFile}):\n```\n{$content}\n```",
];
note("Loaded context: {$contextFile}");
}
}
info('AI Chat (gõ "exit" để thoát, "/clear" để reset, "/save" để lưu)');
$this->newLine();
while (true) {
$input = text(
label: 'Bạn',
placeholder: 'Hỏi gì đó... (exit để thoát)',
required: true,
);
$input = trim($input);
if ($input === 'exit' || $input === 'quit') {
info('Tạm biệt!');
break;
}
if ($input === '/clear') {
$this->messages = [$this->messages[0]]; // Giữ system prompt
info('Đã xóa hội thoại.');
continue;
}
if ($input === '/save') {
$this->saveConversation();
continue;
}
if (str_starts_with($input, '/file ')) {
$filePath = trim(substr($input, 6));
if (file_exists($filePath)) {
$content = file_get_contents($filePath);
$this->messages[] = [
'role' => 'user',
'content' => "Đây là file {$filePath}:\n```\n{$content}\n```",
];
note("File loaded: {$filePath}");
continue;
}
error("File không tồn tại: {$filePath}");
continue;
}
$this->messages[] = ['role' => 'user', 'content' => $input];
// Stream response
$this->output->write('<fg=green>AI:</> ');
$fullResponse = '';
foreach ($ai->stream($this->messages) as $chunk) {
$this->output->write($chunk);
$fullResponse .= $chunk;
}
$this->newLine(2);
$this->messages[] = ['role' => 'assistant', 'content' => $fullResponse];
// Trim conversation nếu quá dài
if (count($this->messages) > 42) {
$system = $this->messages[0];
$recent = array_slice($this->messages, -40);
$this->messages = [$system, ...$recent];
}
}
return Command::SUCCESS;
}
private function saveConversation(): void
{
$filename = 'chat-' . now()->format('Y-m-d-His') . '.md';
$content = "# AI Chat Session\n\n";
foreach ($this->messages as $msg) {
if ($msg['role'] === 'system') continue;
$role = $msg['role'] === 'user' ? '**Bạn:**' : '**AI:**';
$content .= "{$role}\n{$msg['content']}\n\n---\n\n";
}
file_put_contents($filename, $content);
info("Conversation saved: {$filename}");
}
}
Commands trong chat:
exit — Thoát
/clear — Reset conversation
/save — Lưu conversation ra file markdown
/file path — Load file vào context
Sử dụng:
ai-cli chat
ai-cli chat --system="Bạn là DevOps expert"
ai-cli chat --context=app/Models/User.php
Tool 3: Content Generator
// app/Commands/GenerateCommand.php
class GenerateCommand extends Command
{
protected $signature = 'generate {type : blog, readme, commit, changelog} {--context=} {--output=}';
protected $description = 'AI content generator cho developers';
public function handle(AiService $ai): int
{
$type = $this->argument('type');
$context = $this->loadContext();
$output = $this->option('output');
$prompt = $this->buildPrompt($type, $context);
$result = spin(
callback: fn () => $ai->chat([
['role' => 'system', 'content' => $this->systemPromptForType($type)],
['role' => 'user', 'content' => $prompt],
]),
message: "Đang tạo {$type}...",
);
if ($output) {
file_put_contents($output, $result);
info("Output saved: {$output}");
} else {
$this->newLine();
$this->line($result);
}
return Command::SUCCESS;
}
private function loadContext(): string
{
$context = $this->option('context') ?? '';
if ($context && file_exists($context)) {
return file_get_contents($context);
}
// Nếu stdin có data (pipe), đọc từ stdin
if (!posix_isatty(STDIN)) {
return stream_get_contents(STDIN);
}
return $context;
}
private function buildPrompt(string $type, string $context): string
{
return match ($type) {
'commit' => "Tạo conventional commit message cho diff sau:\n```\n{$context}\n```\n\n" .
"Format: type(scope): description\n\nTypes: feat, fix, docs, style, refactor, test, chore",
'readme' => "Tạo README.md chuyên nghiệp cho project. Dựa trên cấu trúc/code:\n```\n{$context}\n```\n\n" .
"Bao gồm: badges, installation, usage, configuration, contributing, license.",
'blog' => "Viết blog post kỹ thuật chi tiết (tiếng Việt) về: {$context}\n\n" .
"Format: YAML frontmatter + markdown. Bao gồm code examples, giải thích chi tiết, gotchas.",
'changelog' => "Tạo CHANGELOG entry từ commits sau:\n```\n{$context}\n```\n\n" .
"Format: Keep a Changelog style. Nhóm theo: Added, Changed, Fixed, Removed.",
default => throw new \InvalidArgumentException("Unknown type: {$type}"),
};
}
private function systemPromptForType(string $type): string
{
return match ($type) {
'commit' => 'Bạn tạo conventional commit messages rõ ràng, ngắn gọn. Chỉ output commit message, không giải thích.',
'readme' => 'Bạn tạo README.md chuyên nghiệp. Output chỉ markdown, không wrap trong code block.',
'blog' => 'Bạn là technical writer cho blog Laravel/PHP tiếng Việt. Viết chi tiết, có code examples.',
'changelog' => 'Bạn tạo CHANGELOG entries. Output chỉ markdown, theo Keep a Changelog format.',
default => 'Bạn là helpful developer assistant.',
};
}
}
Sử dụng:
# Commit message từ git diff
git diff --staged | ai-cli generate commit
# README từ project structure
find . -name "*.php" -path "*/app/*" | head -50 | ai-cli generate readme --output=README.md
# Blog post
ai-cli generate blog --context="Laravel Middleware deep dive"
# Changelog từ git log
git log --oneline v1.0..HEAD | ai-cli generate changelog
Build & Phân Phối Binary
# Build PHAR binary
php ai-cli app:build ai-cli
# Output: builds/ai-cli (single executable file)
ls -la builds/ai-cli
# -rwxr-xr-x 1 user 8.5M builds/ai-cli
Cài Đặt Cho Users
# Download
chmod +x ai-cli && mv ai-cli /usr/local/bin/
# Tạo .env cho API key
echo "OPENAI_API_KEY=sk-your-key" > ~/.ai-cli.env
# Sử dụng
ai-cli review app/Http/Controllers/UserController.php --focus=security
ai-cli chat --system="Bạn là Laravel expert"
ai-cli generate commit --context="$(git diff --staged)"
Self-Update (Optional)
// Cài đặt auto-update component
php ai-cli app:install self-update
// Users update bằng:
ai-cli self-update
Error Handling & UX
// Trong command
public function handle(AiService $ai): int
{
if (!config('ai.api_key')) {
error('OPENAI_API_KEY chưa được cấu hình.');
note('Tạo file .env với OPENAI_API_KEY=sk-your-key');
return Command::FAILURE;
}
try {
// ... main logic
} catch (\Illuminate\Http\Client\ConnectionException $e) {
error('Không thể kết nối API. Kiểm tra internet.');
return Command::FAILURE;
} catch (\Illuminate\Http\Client\RequestException $e) {
if ($e->response->status() === 429) {
error('Rate limited. Chờ vài giây rồi thử lại.');
} elseif ($e->response->status() === 401) {
error('API key không hợp lệ. Kiểm tra OPENAI_API_KEY.');
} else {
error("API error: {$e->getMessage()}");
}
return Command::FAILURE;
}
}
Testing
class ReviewCommandTest extends TestCase
{
public function test_review_command_requires_valid_file(): void
{
$this->artisan('review', ['file' => 'nonexistent.php'])
->assertExitCode(Command::FAILURE);
}
public function test_review_detects_language_correctly(): void
{
$command = new ReviewCommand();
$this->assertEquals('php', $command->detectLanguage('test.php'));
$this->assertEquals('javascript', $command->detectLanguage('app.js'));
$this->assertEquals('typescript', $command->detectLanguage('types.ts'));
}
public function test_generate_commit_parses_diff(): void
{
// Mock AI service
Http::fake([
'api.openai.com/*' => Http::response([
'choices' => [['message' => ['content' => 'feat(auth): add login endpoint']]],
]),
]);
$this->artisan('generate', [
'type' => 'commit',
'--context' => 'Added login controller',
])->assertExitCode(Command::SUCCESS);
}
}
Kết Luận
Laravel Zero + LLM APIs = CLI tools mạnh mẽ, phân phối dễ dàng. Thành phần chính:
- AiService — shared foundation cho tất cả commands (chat + stream)
- Streaming — hiển thị response realtime, không chờ full response
- Interactive prompts — dùng Laravel Prompts cho input đẹp, professional
- Single binary — phân phối PHAR, users không cần cài Laravel/PHP dependencies
- Focused commands — mỗi tool một việc (review, chat, generate)
- Pipe-friendly — đọc stdin cho pipeline
git diff | ai-cli generate commit
Ý tưởng tools khác:
ai-cli explain app/Models/Order.php— giải thích codeai-cli test app/Actions/CreateUser.php— generate testsai-cli migrate— generate migration từ descriptionai-cli translate content/posts/vi/... --to=en— translate blog posts