PHP CLI vs PHP-FPM vs PHP-CGI
PHP может работать в трёх основных режимах. Каждый предназначен для своих задач.
┌──────────────────────────────────────────────────┐
│ PHP CLI (Command Line Interface) │
│ ───────────────────────────────────── │
│ Запуск: php script.php │
│ Назначение: скрипты, cron-задачи, воркеры │
│ Особенности: │
│ • Нет ограничения по времени (max_execution_time = 0) │
│ • Нет HTTP-заголовков │
│ • STDOUT / STDERR для вывода │
│ • $argv / $argc для аргументов │
│ • Собственный php.ini (php-cli.ini) │
│ • Не буферизирует вывод по умолчанию │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ PHP-FPM (FastCGI Process Manager) │
│ ───────────────────────────────────── │
│ Запуск: через Nginx/Apache → FastCGI │
│ Назначение: веб-приложения │
│ Особенности: │
│ • Пул воркеров (pm.max_children) │
│ • max_execution_time = 30 (по умолчанию) │
│ • HTTP request/response │
│ • $_GET, $_POST, $_SERVER │
│ • Output buffering включён │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ PHP-CGI (Common Gateway Interface) │
│ ───────────────────────────────────── │
│ Запуск: веб-сервер создаёт процесс на каждый запрос │
│ Назначение: устаревший режим │
│ Особенности: │
│ • Медленный (новый процесс = fork на каждый запрос)│
│ • Практически не используется │
│ • Заменён PHP-FPM │
└──────────────────────────────────────────────────┘
Запомни: В production веб-приложения используют PHP-FPM. CLI — для скриптов, cron, воркеров очередей, миграций, консольных команд.
Базовые команды CLI
# Запуск скрипта
php script.php
# Inline-выполнение кода
php -r "echo PHP_VERSION . PHP_EOL;"
# Интерактивная оболочка (REPL)
php -a
# Проверка синтаксиса (lint) — не выполняет код
php -l script.php
# No syntax errors detected in script.php
# Рекурсивная проверка всего проекта
find src/ -name "*.php" -exec php -l {} \;
# Список загруженных модулей
php -m
# Информация о конфигурации
php --ini
# Configuration File (php.ini) Path: /etc/php/8.4/cli
# Loaded Configuration File: /etc/php/8.4/cli/php.ini
# Значение конкретной настройки
php -r "echo ini_get('memory_limit');"
# Полная информация (phpinfo)
php -i
# Встроенный веб-сервер (только для разработки!)
php -S localhost:8000
php -S localhost:8000 -t public/ # Document root = public/
php -S localhost:8000 router.php # Custom router script
# Версия PHP
php -v
Важно: Встроенный веб-сервер (
php -S) предназначен ТОЛЬКО для разработки. Он однопоточный и не подходит для production.
Аргументы командной строки: $argv и $argc
<?php
declare(strict_types=1);
// $argv — массив аргументов (string[])
// $argc — количество аргументов (int)
// $argv[0] — всегда имя скрипта
// Запуск: php script.php hello world --verbose
echo "Script: {$argv[0]}\n"; // script.php
echo "Arguments: {$argc}\n"; // 4
for ($i = 1; $i < $argc; $i++) {
echo " arg[{$i}]: {$argv[$i]}\n";
}
// arg[1]: hello
// arg[2]: world
// arg[3]: --verbose
Практический пример: простой CLI-инструмент
<?php
declare(strict_types=1);
// Simple file counter: php count-files.php /path/to/dir [--ext=php]
if ($argc < 2) {
echo "Usage: php {$argv[0]} <directory> [--ext=<extension>]\n";
exit(1);
}
$directory = $argv[1];
if (!is_dir($directory)) {
fwrite(STDERR, "Error: '{$directory}' is not a valid directory\n");
exit(1);
}
// Parse optional --ext argument
$extension = null;
foreach (array_slice($argv, 2) as $arg) {
if (str_starts_with($arg, '--ext=')) {
$extension = substr($arg, 6);
}
}
// Count files
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$directory,
FilesystemIterator::SKIP_DOTS,
),
);
$count = 0;
$totalSize = 0;
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
if ($extension !== null && $file->getExtension() !== $extension) {
continue;
}
$count++;
$totalSize += $file->getSize();
}
$sizeFormatted = number_format($totalSize / 1024, 1);
echo "Directory: {$directory}\n";
if ($extension !== null) {
echo "Extension filter: .{$extension}\n";
}
echo "Files: {$count}\n";
echo "Total size: {$sizeFormatted} KB\n";
exit(0);
getopt() — парсинг опций
Функция getopt() парсит аргументы в формате Unix: короткие (-v, -f file) и длинные (--verbose, --file=name).
<?php
declare(strict_types=1);
// Syntax for short options:
// "v" — flag (no value): -v
// "f:" — required value: -f filename
// "o::" — optional value: -o or -ovalue
// Syntax for long options:
// "verbose" — flag: --verbose
// "file:" — required value: --file=name or --file name
// "output::" — optional value: --output or --output=file
$shortOpts = 'vhf:o::';
$longOpts = [
'verbose',
'help',
'file:', // Required value
'output::', // Optional value
'dry-run',
'format:',
];
$options = getopt($shortOpts, $longOpts, $restIndex);
// $restIndex — index where non-option arguments start (PHP 7.1+)
$positionalArgs = array_slice($argv, $restIndex);
// Check flags
$verbose = isset($options['v']) || isset($options['verbose']);
$help = isset($options['h']) || isset($options['help']);
$dryRun = isset($options['dry-run']);
// Get values
$file = $options['f'] ?? $options['file'] ?? null;
$output = $options['o'] ?? $options['output'] ?? 'default.txt';
$format = $options['format'] ?? 'json';
if ($help) {
echo <<<HELP
Usage: php {$argv[0]} [options] <input>
Options:
-v, --verbose Verbose output
-h, --help Show this help
-f, --file=FILE Input file (required)
-o, --output=FILE Output file (default: default.txt)
--format=FORMAT Output format: json, csv, xml (default: json)
--dry-run Simulate without changes
Examples:
php {$argv[0]} -v --file=data.csv
php {$argv[0]} --format=xml -f input.json output.xml
HELP;
exit(0);
}
echo "Verbose: " . ($verbose ? 'yes' : 'no') . "\n";
echo "File: {$file}\n";
echo "Output: {$output}\n";
echo "Format: {$format}\n";
echo "Dry run: " . ($dryRun ? 'yes' : 'no') . "\n";
echo "Positional args: " . implode(', ', $positionalArgs) . "\n";
Поведение getopt() с повторяющимися опциями
<?php
declare(strict_types=1);
// php script.php -v -v -v
$options = getopt('v');
// Multiple same flags return an array
// -v once → $options['v'] = false
// -v -v → $options['v'] = [false, false]
// -v -v -v → $options['v'] = [false, false, false]
$verbosity = match (true) {
is_array($options['v'] ?? null) => count($options['v']),
isset($options['v']) => 1,
default => 0,
};
echo "Verbosity level: {$verbosity}\n";
Стандартные потоки: STDIN, STDOUT, STDERR
PHP CLI предоставляет три предопределённых потока:
<?php
declare(strict_types=1);
// STDIN — стандартный ввод (php://stdin)
// STDOUT — стандартный вывод (php://stdout)
// STDERR — стандартный вывод ошибок (php://stderr)
// echo / print пишут в STDOUT
echo "This goes to STDOUT\n";
// fwrite(STDERR, ...) пишет в STDERR
fwrite(STDERR, "This goes to STDERR\n");
// В shell можно разделить:
// php script.php > output.log 2> errors.log
// php script.php 2>/dev/null (скрыть ошибки)
// php script.php | grep "pattern" (pipe STDOUT)
Чтение пользовательского ввода
<?php
declare(strict_types=1);
// Simple input with fgets
echo "Enter your name: ";
$name = trim((string) fgets(STDIN));
echo "Hello, {$name}!\n";
// Read until EOF (piped input)
// echo "line1\nline2\nline3" | php script.php
$lines = [];
while (($line = fgets(STDIN)) !== false) {
$lines[] = trim($line);
}
echo "Read " . count($lines) . " lines\n";
// Read all STDIN at once
$input = file_get_contents('php://stdin');
// Or: stream_get_contents(STDIN);
// Non-blocking check (is there input available?)
$read = [STDIN];
$write = $except = [];
if (stream_select($read, $write, $except, 0) > 0) {
$data = fgets(STDIN);
echo "Got input: {$data}";
} else {
echo "No input available\n";
}
readline() — интерактивный ввод с историей
<?php
declare(strict_types=1);
// readline() provides line editing and history (if ext-readline is loaded)
// Basic input
$name = readline('Your name: ');
echo "Hello, {$name}!\n";
// History support
readline_add_history('previous command 1');
readline_add_history('previous command 2');
// User can press UP arrow to access history
$command = readline('> ');
readline_add_history($command);
// Completion (auto-complete with TAB)
$commands = ['help', 'quit', 'status', 'start', 'stop', 'restart'];
readline_completion_function(
function (string $input) use ($commands): array {
return array_filter(
$commands,
fn (string $cmd) => str_starts_with($cmd, $input),
);
},
);
// Interactive loop
while (true) {
$line = readline('app> ');
if ($line === false || $line === 'quit') {
echo "Bye!\n";
break;
}
readline_add_history($line);
match ($line) {
'help' => echo "Commands: " . implode(', ', $commands) . "\n",
'status' => echo "Running\n",
default => echo "Unknown command: {$line}\n",
};
}
// Save/load history
readline_write_history('/tmp/app_history');
readline_read_history('/tmp/app_history');
Коды выхода (Exit Codes)
<?php
declare(strict_types=1);
// Exit codes communicate success/failure to the calling process
// 0 = success (by convention)
exit(0);
// 1 = general error
exit(1);
// 2 = misuse of shell command
exit(2);
// Common conventions:
// 0 — Success
// 1 — General error
// 2 — Invalid arguments / usage error
// 126 — Permission denied
// 127 — Command not found
// 130 — Terminated by Ctrl+C (128 + SIGINT=2)
// Practical usage
function main(int $argc, array $argv): int
{
if ($argc < 2) {
fwrite(STDERR, "Usage: php {$argv[0]} <filename>\n");
return 2; // Invalid arguments
}
$filename = $argv[1];
if (!file_exists($filename)) {
fwrite(STDERR, "Error: File not found: {$filename}\n");
return 1; // General error
}
$content = file_get_contents($filename);
if ($content === false) {
fwrite(STDERR, "Error: Cannot read file: {$filename}\n");
return 1;
}
$lineCount = substr_count($content, "\n") + 1;
echo "{$lineCount}\n";
return 0; // Success
}
exit(main($argc, $argv));
// In shell:
// php script.php file.txt && echo "OK" || echo "FAILED"
// echo $? (prints exit code)
Обработка сигналов: pcntl_signal()
Сигналы POSIX позволяют процессам взаимодействовать с операционной системой. PHP может перехватывать сигналы через расширение pcntl.
<?php
declare(strict_types=1);
// Requires: ext-pcntl (usually available in CLI, NOT in FPM)
// Enable async signal handling (PHP 7.1+)
pcntl_async_signals(true);
$running = true;
// Handle SIGINT (Ctrl+C)
pcntl_signal(SIGINT, function (int $signal) use (&$running): void {
echo "\nReceived SIGINT (Ctrl+C). Graceful shutdown...\n";
$running = false;
});
// Handle SIGTERM (kill command)
pcntl_signal(SIGTERM, function (int $signal) use (&$running): void {
echo "Received SIGTERM. Shutting down...\n";
$running = false;
});
// Handle SIGHUP (terminal closed / config reload)
pcntl_signal(SIGHUP, function (int $signal): void {
echo "Received SIGHUP. Reloading configuration...\n";
// Reload config file, reopen log files, etc.
});
// Handle SIGUSR1 (custom signal for status)
pcntl_signal(SIGUSR1, function (int $signal): void {
$mem = memory_get_usage(true) / 1024 / 1024;
echo "Status: memory = {$mem} MB\n";
});
echo "Worker started. PID: " . getmypid() . "\n";
echo "Send SIGUSR1 for status, SIGINT/SIGTERM to stop\n";
// Main loop
$iteration = 0;
while ($running) {
$iteration++;
// Simulate work
echo "Working... iteration {$iteration}\n";
sleep(1);
}
echo "Worker stopped after {$iteration} iterations\n";
exit(0);
// In another terminal:
// kill -USR1 <pid> → Status report
// kill -HUP <pid> → Reload config
// kill <pid> → SIGTERM → graceful stop
// Ctrl+C → SIGINT → graceful stop
Основные сигналы
| Сигнал | Номер | Действие по умолчанию | Использование |
|---|---|---|---|
SIGINT |
2 | Завершение | Ctrl+C |
SIGTERM |
15 | Завершение | kill <pid> |
SIGKILL |
9 | Завершение (нельзя перехватить!) | kill -9 <pid> |
SIGHUP |
1 | Завершение | Reload конфигурации |
SIGUSR1 |
10 | Завершение | Пользовательский сигнал 1 |
SIGUSR2 |
12 | Завершение | Пользовательский сигнал 2 |
SIGALRM |
14 | Завершение | Таймер pcntl_alarm() |
SIGCHLD |
17 | Игнорирование | Дочерний процесс завершился |
Запомни: SIGKILL (9) НЕЛЬЗЯ перехватить. Всегда обрабатывайте SIGTERM для graceful shutdown.
Работа с процессами: pcntl_fork()
<?php
declare(strict_types=1);
// Fork creates a copy of the current process
// Returns:
// > 0 — parent process (value = child PID)
// = 0 — child process
// < 0 — fork failed
$pid = pcntl_fork();
if ($pid === -1) {
fwrite(STDERR, "Fork failed!\n");
exit(1);
}
if ($pid > 0) {
// PARENT process
echo "Parent (PID: " . getmypid() . "), child PID: {$pid}\n";
// Wait for child to finish
$status = 0;
pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) {
$exitCode = pcntl_wexitstatus($status);
echo "Child exited with code: {$exitCode}\n";
}
} else {
// CHILD process
echo "Child (PID: " . getmypid() . "), parent PID: " . posix_getppid() . "\n";
// Do some work
sleep(2);
echo "Child done\n";
exit(0); // Important: child must exit!
}
echo "Parent continues after child\n";
Параллельная обработка с fork
<?php
declare(strict_types=1);
// Process a batch of tasks in parallel using fork
/**
* @param array<string> $tasks
* @param int $maxWorkers Maximum concurrent workers
*/
function processInParallel(array $tasks, int $maxWorkers = 4): void
{
$childPids = [];
foreach ($tasks as $index => $task) {
// Wait if we hit max workers
while (count($childPids) >= $maxWorkers) {
$exitedPid = pcntl_wait($status);
if ($exitedPid > 0) {
unset($childPids[$exitedPid]);
echo "Worker {$exitedPid} finished\n";
}
}
$pid = pcntl_fork();
if ($pid === -1) {
fwrite(STDERR, "Fork failed for task {$index}\n");
continue;
}
if ($pid === 0) {
// Child process — do the work
echo "[Worker " . getmypid() . "] Processing: {$task}\n";
sleep(random_int(1, 3)); // Simulate work
echo "[Worker " . getmypid() . "] Done: {$task}\n";
exit(0);
}
// Parent — track child
$childPids[$pid] = $index;
}
// Wait for remaining children
while (!empty($childPids)) {
$exitedPid = pcntl_wait($status);
if ($exitedPid > 0) {
unset($childPids[$exitedPid]);
}
}
}
$tasks = [
'Import users.csv',
'Process images batch',
'Send email notifications',
'Generate PDF reports',
'Sync external API data',
'Rebuild search index',
];
echo "Starting parallel processing...\n";
processInParallel($tasks, maxWorkers: 3);
echo "All tasks completed!\n";
Выполнение внешних команд: proc_open()
<?php
declare(strict_types=1);
// proc_open() — most powerful way to execute external commands
// Provides full control over stdin, stdout, stderr
$descriptors = [
0 => ['pipe', 'r'], // stdin — pipe for writing
1 => ['pipe', 'w'], // stdout — pipe for reading
2 => ['pipe', 'w'], // stderr — pipe for reading
];
$process = proc_open(
command: 'sort -n', // Sort numbers numerically
descriptor_spec: $descriptors,
pipes: $pipes,
cwd: '/tmp',
env_vars: ['LANG' => 'C'],
);
if (!is_resource($process)) {
fwrite(STDERR, "Failed to start process\n");
exit(1);
}
// Write to stdin
fwrite($pipes[0], "42\n7\n13\n1\n99\n");
fclose($pipes[0]);
// Read stdout
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
// Read stderr
$errors = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// Get exit code
$exitCode = proc_close($process);
echo "Output:\n{$output}"; // 1\n7\n13\n42\n99
echo "Errors: {$errors}\n";
echo "Exit code: {$exitCode}\n";
Другие функции для выполнения команд
<?php
declare(strict_types=1);
// exec() — выполнить команду, получить последнюю строку вывода
$lastLine = exec('whoami', $outputLines, $exitCode);
echo "User: {$lastLine}\n";
echo "Exit: {$exitCode}\n";
// shell_exec() — выполнить команду, получить весь вывод как строку
$output = shell_exec('ls -la /tmp');
echo $output;
// Backtick operator — то же что shell_exec()
$output = `ls -la /tmp`;
echo $output;
// passthru() — выполнить и передать вывод напрямую в STDOUT
// Полезно для бинарных данных (изображения, архивы)
passthru('cat /tmp/image.png');
// system() — выполнить команду, вывести результат, вернуть последнюю строку
$lastLine = system('df -h', $exitCode);
// SECURITY: ALWAYS escape user input!
$userInput = 'file.txt; rm -rf /'; // Malicious input
// BAD:
// exec("cat {$userInput}"); // DANGEROUS!
// GOOD:
$safe = escapeshellarg($userInput);
exec("cat {$safe}");
// escapeshellarg() — оборачивает аргумент в кавычки
echo escapeshellarg("hello world"); // 'hello world'
// escapeshellcmd() — экранирует спецсимволы в команде
echo escapeshellcmd('ls | rm -rf /'); // ls \| rm -rf \/
Конфигурация CLI
<?php
declare(strict_types=1);
// CLI-specific php.ini differences:
// No time limit by default (web = 30 seconds)
echo "max_execution_time: " . ini_get('max_execution_time') . "\n"; // 0
// Explicit no-limit (redundant for CLI, but explicit)
set_time_limit(0);
// Memory limit (CLI may have different limit)
echo "memory_limit: " . ini_get('memory_limit') . "\n";
ini_set('memory_limit', '512M'); // Increase for heavy scripts
// No output buffering (web usually has it)
echo "output_buffering: " . ini_get('output_buffering') . "\n"; // 0 or empty
// Implicit flush (output sent immediately)
echo "implicit_flush: " . ini_get('implicit_flush') . "\n"; // 1
// HTML errors disabled
echo "html_errors: " . ini_get('html_errors') . "\n"; // 0
// Override from command line
// php -d memory_limit=2G -d display_errors=1 script.php
// Check if running in CLI mode
if (PHP_SAPI === 'cli') {
echo "Running in CLI mode\n";
}
// Alternative check
if (php_sapi_name() === 'cli') {
echo "CLI mode confirmed\n";
}
Практические примеры
Cron-скрипт для очистки данных
<?php
declare(strict_types=1);
// cleanup.php — Run as: php cleanup.php --days=30 --dry-run
pcntl_async_signals(true);
$running = true;
pcntl_signal(SIGTERM, function () use (&$running): void {
fwrite(STDERR, "[SIGTERM] Graceful shutdown requested\n");
$running = false;
});
$options = getopt('', ['days:', 'dry-run', 'verbose', 'dir:']);
$days = (int) ($options['days'] ?? 7);
$dryRun = isset($options['dry-run']);
$verbose = isset($options['verbose']);
$directory = $options['dir'] ?? '/tmp/app-cache';
if (!is_dir($directory)) {
fwrite(STDERR, "Error: Directory not found: {$directory}\n");
exit(1);
}
$cutoff = time() - ($days * 86400);
$deleted = 0;
$totalSize = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$directory,
FilesystemIterator::SKIP_DOTS,
),
RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $file) {
if (!$running) {
fwrite(STDERR, "Interrupted, stopping...\n");
break;
}
if (!$file->isFile()) {
continue;
}
if ($file->getMTime() >= $cutoff) {
continue;
}
$path = $file->getPathname();
$size = $file->getSize();
if ($verbose) {
$age = (int) ((time() - $file->getMTime()) / 86400);
echo " {$path} ({$age} days old, " . number_format($size) . " bytes)\n";
}
if (!$dryRun) {
if (@unlink($path)) {
$deleted++;
$totalSize += $size;
} else {
fwrite(STDERR, "Warning: Cannot delete {$path}\n");
}
} else {
$deleted++;
$totalSize += $size;
}
}
$sizeFormatted = number_format($totalSize / 1024 / 1024, 2);
$mode = $dryRun ? '[DRY RUN] Would delete' : 'Deleted';
echo "{$mode} {$deleted} files ({$sizeFormatted} MB) older than {$days} days\n";
exit($deleted > 0 || $dryRun ? 0 : 1);
Интерактивный мастер настройки
<?php
declare(strict_types=1);
// setup.php — Interactive setup wizard
function ask(string $prompt, string $default = ''): string
{
$displayDefault = $default !== '' ? " [{$default}]" : '';
echo "{$prompt}{$displayDefault}: ";
$input = trim((string) fgets(STDIN));
return $input !== '' ? $input : $default;
}
function confirm(string $prompt, bool $default = true): bool
{
$hint = $default ? '[Y/n]' : '[y/N]';
echo "{$prompt} {$hint}: ";
$input = strtolower(trim((string) fgets(STDIN)));
if ($input === '') {
return $default;
}
return in_array($input, ['y', 'yes', 'da'], true);
}
function choose(string $prompt, array $options): string
{
echo "{$prompt}\n";
$keys = array_keys($options);
foreach ($options as $key => $label) {
echo " [{$key}] {$label}\n";
}
while (true) {
echo "Your choice: ";
$input = trim((string) fgets(STDIN));
if (array_key_exists($input, $options)) {
return $input;
}
echo "Invalid choice. Try again.\n";
}
}
// --- Wizard ---
echo "=== Application Setup Wizard ===\n\n";
$config = [];
$config['app_name'] = ask('Application name', 'My App');
$config['app_env'] = choose('Environment', [
'dev' => 'Development',
'staging' => 'Staging',
'prod' => 'Production',
]);
echo "\n--- Database ---\n";
$config['db_host'] = ask('Database host', 'localhost');
$config['db_port'] = ask('Database port', '5432');
$config['db_name'] = ask('Database name', 'app');
$config['db_user'] = ask('Database user', 'app');
// Hide password input (Unix only)
echo "Database password: ";
system('stty -echo');
$config['db_pass'] = trim((string) fgets(STDIN));
system('stty echo');
echo "\n";
echo "\n--- Options ---\n";
$config['enable_cache'] = confirm('Enable caching?');
$config['enable_queue'] = confirm('Enable queue workers?');
$config['debug'] = confirm('Enable debug mode?', $config['app_env'] === 'dev');
// Summary
echo "\n=== Configuration Summary ===\n";
foreach ($config as $key => $value) {
$display = match (true) {
$key === 'db_pass' => str_repeat('*', strlen((string) $value)),
is_bool($value) => $value ? 'yes' : 'no',
default => (string) $value,
};
echo " {$key}: {$display}\n";
}
if (!confirm("\nApply this configuration?")) {
echo "Setup cancelled.\n";
exit(1);
}
// Generate .env file
$envContent = '';
foreach ($config as $key => $value) {
$envKey = strtoupper($key);
$envValue = is_bool($value) ? ($value ? 'true' : 'false') : $value;
$envContent .= "{$envKey}={$envValue}\n";
}
file_put_contents('.env', $envContent);
echo "\n.env file created successfully!\n";
exit(0);
Инструмент импорта данных
<?php
declare(strict_types=1);
// import.php — CSV data import with progress indication
$options = getopt('f:b:v', ['file:', 'batch:', 'verbose']);
$file = $options['f'] ?? $options['file'] ?? null;
$batchSize = (int) ($options['b'] ?? $options['batch'] ?? 100);
$verbose = isset($options['v']) || isset($options['verbose']);
if ($file === null) {
fwrite(STDERR, "Usage: php import.php -f <csv-file> [-b batch_size] [-v]\n");
exit(2);
}
if (!file_exists($file)) {
fwrite(STDERR, "Error: File not found: {$file}\n");
exit(1);
}
// Count total lines for progress
$totalLines = 0;
$handle = fopen($file, 'r');
while (fgets($handle) !== false) {
$totalLines++;
}
fclose($handle);
$totalLines--; // Subtract header
echo "Importing {$totalLines} records from {$file}...\n";
// Process file
$handle = fopen($file, 'r');
$header = fgetcsv($handle); // Read header
$processed = 0;
$errors = 0;
$batch = [];
$startTime = hrtime(true);
while (($row = fgetcsv($handle)) !== false) {
$record = array_combine($header, $row);
$batch[] = $record;
if (count($batch) >= $batchSize) {
// Process batch
$batchErrors = processBatch($batch);
$errors += $batchErrors;
$processed += count($batch);
$batch = [];
// Show progress
$percent = (int) ($processed / $totalLines * 100);
$elapsed = (hrtime(true) - $startTime) / 1_000_000_000;
$rate = $processed / max($elapsed, 0.001);
$remaining = ($totalLines - $processed) / max($rate, 1);
printf(
"\r [%s%s] %d%% (%d/%d) %.0f rec/s, ETA: %s ",
str_repeat('=', $percent / 2),
str_repeat(' ', 50 - $percent / 2),
$percent,
$processed,
$totalLines,
$rate,
formatDuration($remaining),
);
}
}
// Process remaining
if (!empty($batch)) {
$errors += processBatch($batch);
$processed += count($batch);
}
fclose($handle);
$totalTime = (hrtime(true) - $startTime) / 1_000_000_000;
echo "\n\nImport completed:\n";
echo " Processed: {$processed}\n";
echo " Errors: {$errors}\n";
echo " Time: " . number_format($totalTime, 2) . " seconds\n";
echo " Rate: " . number_format($processed / $totalTime, 0) . " records/second\n";
exit($errors > 0 ? 1 : 0);
function processBatch(array $records): int
{
// Simulate database insert
usleep(10_000); // 10ms per batch
return 0;
}
function formatDuration(float $seconds): string
{
if ($seconds < 60) {
return (int) $seconds . 's';
}
$minutes = (int) ($seconds / 60);
$secs = (int) ($seconds % 60);
return "{$minutes}m {$secs}s";
}