Table Of Contents
- Why Build CLI Applications in PHP? My Evolution
- Basic CLI Application Structure: My First Automation Script
- Working with Arguments and Options
- Using Symfony Console Component: Building Production-Ready Tools
- Interactive CLI Applications: The Setup Wizard That Changed Everything
- Database CLI Tools: The Migration Manager That Saved My Career
- Performance Monitoring CLI: The Load Testing Tool That Proved Value
- Best Practices for CLI Applications: Hard-Learned Lessons
- Conclusion: From Manual Labor to Automation Mastery
CLI applications are often overlooked in PHP development, but they're incredibly powerful tools for automation, data processing, and system administration. After 10 years of Laravel development, I've built countless CLI tools that have saved hundreds of hours of manual work and automated complex processes that would have been error-prone if done manually.
My CLI journey began out of necessity. I was tired of manually deploying Laravel applications, running database migrations by hand, and processing CSV files through the browser interface. What started as simple bash scripts evolved into sophisticated PHP CLI applications that became indispensable to my development workflow.
The transformation was dramatic: tasks that used to take hours of careful manual work became one-line commands that ran reliably every time. When I started working with larger teams, I realized that CLI tools weren't just personal productivity boosters - they were essential for creating repeatable, reliable processes that the entire team could use.
Why Build CLI Applications in PHP? My Evolution
PHP CLI applications offer several advantages that became clear through my development journey:
- Leverage existing PHP knowledge and libraries - I didn't need to learn a new language just to automate Laravel application tasks
- Easy integration with web applications - My CLI tools could share models, configs, and database connections with my Laravel apps
- Powerful built-in functions for file and system operations - PHP's file handling and string processing capabilities were perfect for data processing tasks
- Excellent third-party libraries like Symfony Console - Laravel's Artisan is built on Symfony Console, so learning it felt natural
- Cross-platform compatibility - My tools worked on my Mac development environment and Linux production servers
- Adherence to modern PHP PSR standards - Using established conventions made my CLI tools maintainable and team-friendly
The real breakthrough came when I realized that building CLI tools in PHP meant I could reuse all my Laravel expertise. Instead of writing separate Python scripts or bash utilities, I could create PHP tools that integrated seamlessly with my existing applications.
Basic CLI Application Structure: My First Automation Script
Let's start with a simple CLI application. This exact pattern was my first successful CLI script - a Laravel log analyzer that saved me from manually parsing log files during debugging sessions:
#!/usr/bin/env php
<?php
// hello.php
// Check if running from CLI
if (php_sapi_name() !== 'cli') {
die('This script must be run from the command line');
}
// Simple argument handling
$name = $argv[1] ?? 'World';
echo "Hello, $name!\n";
// Exit with success code
exit(0);
Make it executable:
chmod +x hello.php
./hello.php "PHP Developer"
Working with Arguments and Options
#!/usr/bin/env php
<?php
// file-processor.php
function parseArguments(): array
{
global $argv;
$options = [
'input' => null,
'output' => null,
'verbose' => false,
'dry-run' => false,
'help' => false
];
for ($i = 1; $i < count($argv); $i++) {
$arg = $argv[$i];
switch ($arg) {
case '-i':
case '--input':
$options['input'] = $argv[++$i] ?? null;
break;
case '-o':
case '--output':
$options['output'] = $argv[++$i] ?? null;
break;
case '-v':
case '--verbose':
$options['verbose'] = true;
break;
case '--dry-run':
$options['dry-run'] = true;
break;
case '-h':
case '--help':
$options['help'] = true;
break;
default:
echo "Unknown option: $arg\n";
exit(1);
}
}
return $options;
}
function showHelp(): void
{
echo "File Processor\n";
echo "Usage: file-processor.php [options]\n";
echo "\nOptions:\n";
echo " -i, --input FILE Input file path\n";
echo " -o, --output FILE Output file path\n";
echo " -v, --verbose Enable verbose output\n";
echo " --dry-run Show what would be done without doing it\n";
echo " -h, --help Show this help message\n";
}
function processFile(string $input, string $output, bool $verbose, bool $dryRun): void
{
if ($verbose) {
echo "Processing file: $input\n";
}
if (!file_exists($input)) {
echo "Error: Input file does not exist: $input\n";
exit(1);
}
$content = file_get_contents($input);
$processed = strtoupper($content);
if ($dryRun) {
echo "Would write " . strlen($processed) . " bytes to: $output\n";
return;
}
if (file_put_contents($output, $processed) === false) {
echo "Error: Could not write to output file: $output\n";
exit(1);
}
if ($verbose) {
echo "Successfully processed file to: $output\n";
}
}
// Main execution
$options = parseArguments();
if ($options['help']) {
showHelp();
exit(0);
}
if (!$options['input'] || !$options['output']) {
echo "Error: Input and output files are required\n";
echo "Use --help for usage information\n";
exit(1);
}
processFile(
$options['input'],
$options['output'],
$options['verbose'],
$options['dry-run']
);
Using Symfony Console Component: Building Production-Ready Tools
For more complex CLI applications, Symfony Console is the gold standard. This is where my CLI development really took off - the moment I discovered I could build Laravel Artisan-style commands for any project. Following clean code principles when building these tools ensured they remained maintainable as they grew in complexity:
composer require symfony/console
#!/usr/bin/env php
<?php
// app.php
require_once 'vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Helper\ProgressBar;
class ProcessFilesCommand extends Command
{
protected static $defaultName = 'process:files';
protected static $defaultDescription = 'Process files with various options';
protected function configure(): void
{
$this
->setDescription('Process files in a directory')
->addArgument('directory', InputArgument::REQUIRED, 'Directory to process')
->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format', 'json')
->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Process recursively')
->addOption('pattern', 'p', InputOption::VALUE_REQUIRED, 'File pattern to match', '*.txt')
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Output file');
// Using modern PHP 8.x features for cleaner syntax - learn more about [modern PHP 8.x features](https://mycuriosity.blog/modern-php-8x-features-and-future)
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$directory = $input->getArgument('directory');
$format = $input->getOption('format');
$recursive = $input->getOption('recursive');
$pattern = $input->getOption('pattern');
$outputFile = $input->getOption('output');
if (!is_dir($directory)) {
$io->error("Directory does not exist: $directory");
return Command::FAILURE;
}
$io->title('File Processor');
$io->text("Processing directory: $directory");
$io->text("Pattern: $pattern");
$io->text("Format: $format");
$io->text("Recursive: " . ($recursive ? 'Yes' : 'No'));
$files = $this->findFiles($directory, $pattern, $recursive);
if (empty($files)) {
$io->warning('No files found matching the pattern');
return Command::SUCCESS;
}
$io->text("Found " . count($files) . " files");
// Process files with progress bar
$progressBar = new ProgressBar($output, count($files));
$progressBar->start();
$results = [];
foreach ($files as $file) {
$results[] = $this->processFile($file, $format);
$progressBar->advance();
}
$progressBar->finish();
$io->newLine(2);
if ($outputFile) {
$this->saveResults($results, $outputFile, $format);
$io->success("Results saved to: $outputFile");
} else {
$this->displayResults($results, $io);
}
return Command::SUCCESS;
}
private function findFiles(string $directory, string $pattern, bool $recursive): array
{
$files = [];
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
: new DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isFile() && fnmatch($pattern, $file->getFilename())) {
$files[] = $file->getPathname();
}
}
return $files;
}
private function processFile(string $filePath, string $format): array
{
$info = [
'path' => $filePath,
'size' => filesize($filePath),
'modified' => date('Y-m-d H:i:s', filemtime($filePath)),
'extension' => pathinfo($filePath, PATHINFO_EXTENSION),
'lines' => count(file($filePath, FILE_IGNORE_NEW_LINES))
];
if ($format === 'detailed') {
$info['permissions'] = substr(sprintf('%o', fileperms($filePath)), -4);
$info['owner'] = posix_getpwuid(fileowner($filePath))['name'] ?? 'unknown';
}
return $info;
}
private function saveResults(array $results, string $outputFile, string $format): void
{
$content = match ($format) {
'json' => json_encode($results, JSON_PRETTY_PRINT),
'csv' => $this->convertToCsv($results),
'xml' => $this->convertToXml($results),
default => json_encode($results, JSON_PRETTY_PRINT)
};
file_put_contents($outputFile, $content);
}
private function displayResults(array $results, SymfonyStyle $io): void
{
$headers = ['Path', 'Size', 'Modified', 'Extension', 'Lines'];
$rows = [];
foreach ($results as $result) {
$rows[] = [
$result['path'],
number_format($result['size']),
$result['modified'],
$result['extension'],
$result['lines']
];
}
$io->table($headers, $rows);
}
private function convertToCsv(array $results): string
{
if (empty($results)) {
return '';
}
$csv = '';
$headers = array_keys($results[0]);
$csv .= implode(',', $headers) . "\n";
foreach ($results as $row) {
$csv .= implode(',', array_values($row)) . "\n";
}
return $csv;
}
private function convertToXml(array $results): string
{
$xml = new SimpleXMLElement('<files/>');
foreach ($results as $result) {
$file = $xml->addChild('file');
foreach ($result as $key => $value) {
$file->addChild($key, htmlspecialchars($value));
}
}
return $xml->asXML();
}
}
// Create application
$application = new Application('File Processor', '1.0.0');
$application->add(new ProcessFilesCommand());
$application->run();
Interactive CLI Applications: The Setup Wizard That Changed Everything
Building interactive CLI applications transformed how I approached application configuration. This exact setup wizard pattern eliminated the tedious process of manually editing config files for new Laravel deployments and complemented our Git workflow for professional development processes:
#!/usr/bin/env php
<?php
// interactive-setup.php
require_once 'vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class InteractiveSetupCommand extends Command
{
protected static $defaultName = 'setup:interactive';
protected static $defaultDescription = 'Interactive application setup';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$helper = $this->getHelper('question');
$io->title('Application Setup Wizard');
// Basic information
$nameQuestion = new Question('What is your name? ');
$nameQuestion->setValidator(function ($value) {
if (empty(trim($value))) {
throw new \Exception('Name cannot be empty');
}
return $value;
});
$name = $helper->ask($input, $output, $nameQuestion);
$emailQuestion = new Question('What is your email address? ');
$emailQuestion->setValidator(function ($value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email address');
}
return $value;
});
$email = $helper->ask($input, $output, $emailQuestion);
// Choice question
$envQuestion = new ChoiceQuestion(
'Select your environment:',
['development', 'staging', 'production'],
0
);
$environment = $helper->ask($input, $output, $envQuestion);
// Confirmation
$enableDebugQuestion = new ConfirmationQuestion(
'Enable debug mode? (y/N) ',
false
);
$enableDebug = $helper->ask($input, $output, $enableDebugQuestion);
// Password (hidden input)
$passwordQuestion = new Question('Enter a password: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$password = $helper->ask($input, $output, $passwordQuestion);
// Display configuration
$io->section('Configuration Summary');
$io->definitionList(
['Name' => $name],
['Email' => $email],
['Environment' => $environment],
['Debug Mode' => $enableDebug ? 'Enabled' : 'Disabled'],
['Password' => str_repeat('*', strlen($password))]
);
// Final confirmation
$confirmQuestion = new ConfirmationQuestion(
'Save this configuration? (y/N) ',
false
);
if (!$helper->ask($input, $output, $confirmQuestion)) {
$io->warning('Setup cancelled');
return Command::SUCCESS;
}
// Save configuration
$config = [
'name' => $name,
'email' => $email,
'environment' => $environment,
'debug' => $enableDebug,
'password_hash' => password_hash($password, PASSWORD_DEFAULT)
];
file_put_contents('config.json', json_encode($config, JSON_PRETTY_PRINT));
$io->success('Configuration saved successfully!');
return Command::SUCCESS;
}
}
$application = new Application('Setup Tool', '1.0.0');
$application->add(new InteractiveSetupCommand());
$application->run();
Database CLI Tools: The Migration Manager That Saved My Career
Database CLI tools became essential when I needed to manage complex Laravel migrations across multiple environments. This migration tool pattern saved me from countless deployment disasters and became a crucial part of our DevOps culture and continuous integration/deployment pipeline:
#!/usr/bin/env php
<?php
// database-tool.php
require_once 'vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Helper\Table;
class DatabaseMigrationCommand extends Command
{
protected static $defaultName = 'db:migrate';
protected static $defaultDescription = 'Run database migrations';
private PDO $pdo;
public function __construct()
{
parent::__construct();
// Initialize database connection
$this->pdo = new PDO(
'mysql:host=localhost;dbname=test',
'username',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
protected function configure(): void
{
$this
->addOption('rollback', 'r', InputOption::VALUE_NONE, 'Rollback last migration')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be migrated')
->addOption('step', 's', InputOption::VALUE_OPTIONAL, 'Number of migrations to run', 1);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$rollback = $input->getOption('rollback');
$dryRun = $input->getOption('dry-run');
$step = (int) $input->getOption('step');
$io->title('Database Migration Tool');
// Create migrations table if it doesn't exist
$this->createMigrationsTable();
if ($rollback) {
return $this->rollbackMigrations($io, $dryRun, $step);
}
return $this->runMigrations($io, $dryRun, $step);
}
private function createMigrationsTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)";
$this->pdo->exec($sql);
}
private function runMigrations(SymfonyStyle $io, bool $dryRun, int $step): int
{
$migrationFiles = $this->getMigrationFiles();
$executedMigrations = $this->getExecutedMigrations();
$pendingMigrations = array_diff($migrationFiles, $executedMigrations);
$migrationsToRun = array_slice($pendingMigrations, 0, $step);
if (empty($migrationsToRun)) {
$io->info('No pending migrations');
return Command::SUCCESS;
}
$io->text("Found " . count($migrationsToRun) . " pending migrations:");
foreach ($migrationsToRun as $migration) {
$io->text(" - $migration");
}
if ($dryRun) {
$io->note('Dry run mode - no migrations were actually executed');
return Command::SUCCESS;
}
$io->newLine();
$io->text('Executing migrations...');
foreach ($migrationsToRun as $migration) {
try {
$this->executeMigration($migration);
$io->text("✓ Executed: $migration");
} catch (Exception $e) {
$io->error("Failed to execute $migration: " . $e->getMessage());
return Command::FAILURE;
}
}
$io->success('All migrations executed successfully!');
return Command::SUCCESS;
}
private function rollbackMigrations(SymfonyStyle $io, bool $dryRun, int $step): int
{
$executedMigrations = $this->getExecutedMigrations();
$migrationsToRollback = array_slice(array_reverse($executedMigrations), 0, $step);
if (empty($migrationsToRollback)) {
$io->info('No migrations to rollback');
return Command::SUCCESS;
}
$io->text("Rolling back " . count($migrationsToRollback) . " migrations:");
foreach ($migrationsToRollback as $migration) {
$io->text(" - $migration");
}
if ($dryRun) {
$io->note('Dry run mode - no migrations were actually rolled back');
return Command::SUCCESS;
}
$io->newLine();
$io->text('Rolling back migrations...');
foreach ($migrationsToRollback as $migration) {
try {
$this->rollbackMigration($migration);
$io->text("✓ Rolled back: $migration");
} catch (Exception $e) {
$io->error("Failed to rollback $migration: " . $e->getMessage());
return Command::FAILURE;
}
}
$io->success('All migrations rolled back successfully!');
return Command::SUCCESS;
}
private function getMigrationFiles(): array
{
$files = glob('migrations/*.sql');
return array_map('basename', $files);
}
private function getExecutedMigrations(): array
{
$stmt = $this->pdo->query('SELECT migration FROM migrations ORDER BY executed_at');
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
private function executeMigration(string $migration): void
{
$sql = file_get_contents("migrations/$migration");
$this->pdo->exec($sql);
$stmt = $this->pdo->prepare('INSERT INTO migrations (migration) VALUES (?)');
$stmt->execute([$migration]);
}
private function rollbackMigration(string $migration): void
{
// This would execute the down migration
$downFile = str_replace('.sql', '_down.sql', $migration);
if (file_exists("migrations/$downFile")) {
$sql = file_get_contents("migrations/$downFile");
$this->pdo->exec($sql);
}
$stmt = $this->pdo->prepare('DELETE FROM migrations WHERE migration = ?');
$stmt->execute([$migration]);
}
}
class DatabaseStatusCommand extends Command
{
protected static $defaultName = 'db:status';
protected static $defaultDescription = 'Show database migration status';
private PDO $pdo;
public function __construct()
{
parent::__construct();
$this->pdo = new PDO(
'mysql:host=localhost;dbname=test',
'username',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Database Status');
// Get all migration files
$migrationFiles = glob('migrations/*.sql');
$migrationFiles = array_map('basename', $migrationFiles);
// Get executed migrations
$executedMigrations = [];
try {
$stmt = $this->pdo->query('SELECT migration, executed_at FROM migrations ORDER BY executed_at');
$executedMigrations = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$io->warning('Migrations table does not exist');
}
$executedMigrationNames = array_column($executedMigrations, 'migration');
// Create status table
$table = new Table($output);
$table->setHeaders(['Migration', 'Status', 'Executed At']);
foreach ($migrationFiles as $migration) {
$executed = in_array($migration, $executedMigrationNames);
$executedAt = '';
if ($executed) {
$key = array_search($migration, $executedMigrationNames);
$executedAt = $executedMigrations[$key]['executed_at'];
}
$table->addRow([
$migration,
$executed ? '✓ Executed' : '✗ Pending',
$executedAt
]);
}
$table->render();
$pendingCount = count($migrationFiles) - count($executedMigrationNames);
$io->note("Total migrations: " . count($migrationFiles));
$io->note("Executed: " . count($executedMigrationNames));
$io->note("Pending: $pendingCount");
return Command::SUCCESS;
}
}
// Create application
$application = new Application('Database Tool', '1.0.0');
$application->add(new DatabaseMigrationCommand());
$application->add(new DatabaseStatusCommand());
$application->run();
Performance Monitoring CLI: The Load Testing Tool That Proved Value
Performance monitoring became crucial when I needed to validate Laravel application performance before major releases. This load testing tool helped identify bottlenecks that would have caused production issues and worked perfectly alongside our debugging techniques and tools:
#!/usr/bin/env php
<?php
// performance-monitor.php
require_once 'vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Helper\ProgressBar;
class PerformanceMonitorCommand extends Command
{
protected static $defaultName = 'monitor:performance';
protected static $defaultDescription = 'Monitor application performance';
protected function configure(): void
{
$this
->addArgument('url', InputArgument::REQUIRED, 'URL to monitor')
->addOption('requests', 'r', InputOption::VALUE_OPTIONAL, 'Number of requests', 10)
->addOption('concurrency', 'c', InputOption::VALUE_OPTIONAL, 'Concurrent requests', 1)
->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Output file for results');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$url = $input->getArgument('url');
$requests = (int) $input->getOption('requests');
$concurrency = (int) $input->getOption('concurrency');
$outputFile = $input->getOption('output');
$io->title('Performance Monitor');
$io->text("URL: $url");
$io->text("Requests: $requests");
$io->text("Concurrency: $concurrency");
$io->newLine();
$results = $this->runLoadTest($url, $requests, $concurrency, $io);
$this->displayResults($results, $io);
if ($outputFile) {
$this->saveResults($results, $outputFile);
$io->success("Results saved to: $outputFile");
}
return Command::SUCCESS;
}
private function runLoadTest(string $url, int $requests, int $concurrency, SymfonyStyle $io): array
{
$io->text('Starting load test...');
$progressBar = new ProgressBar($io, $requests);
$progressBar->start();
$results = [];
$batches = array_chunk(range(1, $requests), $concurrency);
foreach ($batches as $batch) {
$batchResults = $this->processBatch($url, $batch);
$results = array_merge($results, $batchResults);
$progressBar->advance(count($batch));
}
$progressBar->finish();
$io->newLine(2);
return $results;
}
private function processBatch(string $url, array $batch): array
{
$multiHandle = curl_multi_init();
$curlHandles = [];
$results = [];
// Initialize curl handles
foreach ($batch as $i) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Performance Monitor/1.0');
$curlHandles[$i] = $ch;
curl_multi_add_handle($multiHandle, $ch);
}
// Execute requests
$running = null;
do {
curl_multi_exec($multiHandle, $running);
curl_multi_select($multiHandle);
} while ($running > 0);
// Collect results
foreach ($curlHandles as $i => $ch) {
$response = curl_multi_getcontent($ch);
$info = curl_getinfo($ch);
$results[] = [
'request_id' => $i,
'url' => $url,
'http_code' => $info['http_code'],
'total_time' => $info['total_time'],
'connect_time' => $info['connect_time'],
'download_speed' => $info['speed_download'],
'size_download' => $info['size_download'],
'timestamp' => microtime(true)
];
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}
private function displayResults(array $results, SymfonyStyle $io): void
{
$io->section('Performance Results');
// Calculate statistics
$totalTime = array_sum(array_column($results, 'total_time'));
$avgTime = $totalTime / count($results);
$minTime = min(array_column($results, 'total_time'));
$maxTime = max(array_column($results, 'total_time'));
$successfulRequests = count(array_filter($results, fn($r) => $r['http_code'] >= 200 && $r['http_code'] < 300));
$failedRequests = count($results) - $successfulRequests;
$totalSize = array_sum(array_column($results, 'size_download'));
$avgSize = $totalSize / count($results);
// Display statistics
$io->definitionList(
['Total Requests' => count($results)],
['Successful' => $successfulRequests],
['Failed' => $failedRequests],
['Success Rate' => round(($successfulRequests / count($results)) * 100, 2) . '%'],
['Average Response Time' => round($avgTime * 1000, 2) . 'ms'],
['Min Response Time' => round($minTime * 1000, 2) . 'ms'],
['Max Response Time' => round($maxTime * 1000, 2) . 'ms'],
['Average Response Size' => round($avgSize / 1024, 2) . 'KB'],
['Total Data Transferred' => round($totalSize / 1024 / 1024, 2) . 'MB']
);
// Response time distribution
$io->section('Response Time Distribution');
$this->displayResponseTimeDistribution($results, $io);
// HTTP status codes
$io->section('HTTP Status Codes');
$this->displayStatusCodes($results, $io);
}
private function displayResponseTimeDistribution(array $results, SymfonyStyle $io): void
{
$times = array_column($results, 'total_time');
sort($times);
$percentiles = [50, 75, 90, 95, 99];
$distribution = [];
foreach ($percentiles as $percentile) {
$index = (int) ceil(($percentile / 100) * count($times)) - 1;
$distribution["P$percentile"] = round($times[$index] * 1000, 2) . 'ms';
}
$io->definitionList(...array_map(fn($k, $v) => [$k => $v], array_keys($distribution), $distribution));
}
private function displayStatusCodes(array $results, SymfonyStyle $io): void
{
$statusCodes = array_count_values(array_column($results, 'http_code'));
foreach ($statusCodes as $code => $count) {
$io->text("$code: $count requests");
}
}
private function saveResults(array $results, string $outputFile): void
{
$report = [
'timestamp' => date('Y-m-d H:i:s'),
'url' => $results[0]['url'] ?? 'N/A',
'total_requests' => count($results),
'successful_requests' => count(array_filter($results, fn($r) => $r['http_code'] >= 200 && $r['http_code'] < 300)),
'failed_requests' => count(array_filter($results, fn($r) => $r['http_code'] < 200 || $r['http_code'] >= 300)),
'average_response_time' => array_sum(array_column($results, 'total_time')) / count($results),
'min_response_time' => min(array_column($results, 'total_time')),
'max_response_time' => max(array_column($results, 'total_time')),
'total_data_transferred' => array_sum(array_column($results, 'size_download')),
'detailed_results' => $results
];
file_put_contents($outputFile, json_encode($report, JSON_PRETTY_PRINT));
}
}
$application = new Application('Performance Monitor', '1.0.0');
$application->add(new PerformanceMonitorCommand());
$application->run();
Best Practices for CLI Applications: Hard-Learned Lessons
These practices emerged from real production challenges and user feedback from my CLI tools:
- Error Handling: Always provide meaningful error messages - users will thank you when commands fail gracefully with clear guidance
- Exit Codes: Use appropriate exit codes (0 for success, non-zero for errors) - essential for integrating with CI/CD pipelines
- Progress Indicators: Show progress for long-running operations - nothing frustrates users more than commands that appear frozen
- Configuration: Support configuration files and environment variables - makes tools usable across different environments
- Logging: Implement proper logging for debugging and monitoring - you'll need this when commands fail in production
- Testing: Write tests for your CLI commands - CLI tools are code too and deserve the same testing rigor
- Documentation: Provide comprehensive help and usage information - following technical writing best practices ensures users can effectively utilize your tools
- Containerization: Consider containerization with Docker for consistent execution environments across different systems
Conclusion: From Manual Labor to Automation Mastery
PHP CLI applications are powerful tools that can automate complex tasks, process data, and integrate with system operations. From simple scripts to complex interactive applications, PHP provides all the tools needed to build professional command-line tools that transform development workflows.
My CLI transformation journey: What started as frustration with repetitive Laravel deployment tasks became a passion for building automation tools that made entire teams more productive. The journey from manually editing config files to running one-command deployments was revolutionary for both my personal productivity and team collaboration.
Real-world impact: Throughout my Laravel development career, I've built CLI tools for everything from deployment automation to data processing pipelines. These tools didn't just save time - they eliminated entire categories of human error and made complex processes accessible to junior developers.
Key lessons learned:
Start Simple, Evolve Gradually: My most successful CLI tools began as simple scripts that solved immediate pain points. The sophisticated features came later as needs evolved and users provided feedback.
Integration is Everything: The CLI tools that gained the most adoption were those that integrated seamlessly with existing Laravel workflows. Building PHP CLI tools meant leveraging existing models, configurations, and team knowledge while following established PHP design patterns for maintainable code.
User Experience Matters: CLI tools aren't just functional utilities - they're user interfaces. The difference between a tool that gets used and one that gets abandoned often comes down to helpful error messages, clear progress indicators, and intuitive command structures.
Solve Real Problems: The most valuable CLI tools I've built were born from genuine frustration with manual processes. They weren't theoretical solutions but practical tools that addressed specific workflow pain points.
My advice for Laravel developers: Don't underestimate the power of custom CLI tools. Every repetitive task in your development workflow is an opportunity for automation. Start with simple scripts for your own productivity, then gradually build tools that benefit your entire team. Consider contributing to open source projects to share your CLI innovations with the broader PHP community.
The Symfony Console component is an excellent choice for building professional CLI applications, providing features like argument parsing, interactive prompts, progress bars, and styled output. Combined with PHP's powerful built-in functions and Laravel's ecosystem, you can build CLI tools that rival those written in any other language.
The bigger picture: Good CLI applications are not just functional – they're force multipliers that make good developers more productive and help teams scale efficiently. The time you invest in building quality CLI tools pays dividends in reduced errors, faster deployments, and more reliable processes.
When you master CLI development, you're not just learning to build tools - you're learning to identify automation opportunities and create solutions that make software development more efficient and enjoyable.
Add Comment
No comments yet. Be the first to comment!