MidТеория14 min

PHP в командной строке

CLI режим, $argv, getopt, STDIN/STDOUT, процессы, сигналы, интерактивный ввод

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";
}

Проверь себя

5 из 10

Чем proc_open() отличается от exec()?

Что означает двоеточие после буквы в getopt('f:v')?

Для чего нужен pcntl_async_signals(true)?

Какое значение max_execution_time по умолчанию в CLI?

Какой SAPI используется при запуске PHP из терминала?