PHP CLI ile Komut Satırında Script Çalıştırma

PHP denince aklınıza ilk gelen şey muhtemelen web sunucusunda çalışan, tarayıcıya HTML döken bir dil. Ama PHP’nin bir de karanlık tarafı var: komut satırı. PHP CLI (Command Line Interface), aslında birçok sysadmin’in hayatını kolaylaştırabilecek güçlü bir araç. Cron job’lardan sistem otomasyonuna, veritabanı migrasyonlarından log analizine kadar PHP CLI’yi kullanabileceğiniz onlarca senaryo mevcut. Bu yazıda PHP CLI’yi A’dan Z’ye ele alacağız.

PHP CLI Nedir ve Web PHP’den Farkı Ne?

PHP’nin iki farklı çalışma modu var. Birincisi, Apache veya Nginx arkasında çalışan PHP-FPM veya mod_php. İkincisi ise doğrudan terminalden çalıştırdığınız PHP CLI binary’si.

Bu iki mod arasındaki farklar sandığınızdan daha derin:

  • Zaman aşımı: Web PHP’de max_execution_time genellikle 30-60 saniye. CLI’de ise varsayılan olarak 0, yani sınırsız.
  • Bellek limiti: Web PHP genellikle 128-256MB ile sınırlıyken, CLI’de çok daha fazlasını kullanabilirsiniz.
  • HTTP başlıkları: CLI’de header() fonksiyonu işe yaramaz, kimseye gönderecek tarayıcı yok.
  • $_SERVER değişkenleri: Web’de dolup taşan $_SERVER, CLI’de çok daha sade.
  • php.ini farklılığı: CLI genellikle farklı bir php.ini dosyası kullanır.

Hangi php.ini’nin kullanıldığını görmek için:

php --ini
# Veya daha detaylı:
php -r "phpinfo();" | grep "Loaded Configuration"

PHP CLI Kurulumu ve Versiyon Yönetimi

Birçok Linux dağıtımında PHP CLI zaten yüklü gelir ama özellikle Ubuntu/Debian sistemlerde bazen sadece web modülü kurulu olur.

# Ubuntu/Debian
sudo apt update
sudo apt install php-cli

# RHEL/CentOS/Rocky Linux
sudo dnf install php-cli

# Birden fazla PHP versiyonu için (Ubuntu)
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.1-cli php8.2-cli php8.3-cli

Hangi PHP’nin aktif olduğunu kontrol etmek:

php --version
which php
ls -la /usr/bin/php*

Eğer sisteminizde birden fazla PHP versiyonu varsa ve hangisinin kullanılacağını belirlemek istiyorsanız:

# Ubuntu'da update-alternatives kullanımı
sudo update-alternatives --config php

# Veya doğrudan versiyon belirterek çalıştırma
php8.1 script.php
php8.2 script.php
/usr/bin/php8.3 script.php

İlk PHP CLI Script’iniz

Klasik Hello World’den daha işlevsel bir şeyle başlayalım. Aşağıdaki script, sunucunun temel sistem bilgilerini toplar:

cat << 'EOF' > /usr/local/bin/syscheck.php
#!/usr/bin/env php
<?php

$hostname = gethostname();
$phpVersion = phpversion();
$memTotal = shell_exec("grep MemTotal /proc/meminfo | awk '{print $2}'");
$loadAvg = sys_getloadavg();

echo "=== Sistem Bilgisi ===" . PHP_EOL;
echo "Hostname: " . $hostname . PHP_EOL;
echo "PHP Versiyon: " . $phpVersion . PHP_EOL;
echo "Toplam RAM: " . round(trim($memTotal) / 1024) . " MB" . PHP_EOL;
echo "Load Average: " . implode(", ", array_map(fn($v) => round($v, 2), $loadAvg)) . PHP_EOL;
echo "Tarih/Saat: " . date("Y-m-d H:i:s") . PHP_EOL;
EOF

chmod +x /usr/local/bin/syscheck.php
php /usr/local/bin/syscheck.php

Dikkat edin, ilk satırdaki #!/usr/bin/env php shebang satırı. Bu sayede script’i doğrudan ./syscheck.php şeklinde de çalıştırabilirsiniz.

Komut Satırı Argümanlarını İşlemek

PHP CLI’nin en güçlü özelliklerinden biri argüman işleme. $argv ve $argc değişkenleri ile birlikte getopt() fonksiyonu size tam kontrol sağlar.

cat << 'EOF' > backup_check.php
#!/usr/bin/env php
<?php

// Argüman yoksa kullanım bilgisi göster
if ($argc < 2) {
    echo "Kullanım: php backup_check.php [--path=/backup/dir] [--days=7] [--verbose]" . PHP_EOL;
    exit(1);
}

// Uzun ve kısa opsiyonları tanımla
$options = getopt("vp:d:", ["verbose", "path:", "days:"]);

$verbose = isset($options['v']) || isset($options['verbose']);
$path = $options['p'] ?? $options['path'] ?? '/var/backups';
$days = (int)($options['d'] ?? $options['days'] ?? 7);

if ($verbose) {
    echo "Kontrol edilen dizin: $path" . PHP_EOL;
    echo "Kontrol süresi: Son $days gün" . PHP_EOL;
}

if (!is_dir($path)) {
    fwrite(STDERR, "Hata: $path dizini bulunamadı!" . PHP_EOL);
    exit(2);
}

$cutoffTime = time() - ($days * 86400);
$files = glob($path . '/*.{tar,gz,zip,sql}', GLOB_BRACE);
$recentBackups = array_filter($files, fn($f) => filemtime($f) > $cutoffTime);

echo "Son $days günde " . count($recentBackups) . " backup dosyası bulundu." . PHP_EOL;

if (count($recentBackups) === 0) {
    fwrite(STDERR, "UYARI: Hiç güncel backup yok!" . PHP_EOL);
    exit(3);
}
EOF

Bu script’i şu şekilde kullanabilirsiniz:

php backup_check.php --path=/mnt/backups --days=3 --verbose
php backup_check.php -p /mnt/backups -d 3 -v

STDIN, STDOUT ve STDERR Kullanımı

CLI script’lerinde standart giriş/çıkış akışlarını doğru kullanmak çok önemli. Özellikle pipe ile başka komutlarla entegrasyon yapıyorsanız bu kritik.

cat << 'EOF' > log_parser.php
#!/usr/bin/env php
<?php

// STDIN'den okuma - pipe ile kullanım için
$errorCount = 0;
$warningCount = 0;
$criticalLines = [];

$handle = fopen("php://stdin", "r");

while (($line = fgets($handle)) !== false) {
    $line = trim($line);
    
    if (stripos($line, 'ERROR') !== false) {
        $errorCount++;
        // STDOUT'a yaz
        echo "[HATA BULUNDU] $line" . PHP_EOL;
    } elseif (stripos($line, 'WARNING') !== false) {
        $warningCount++;
    } elseif (stripos($line, 'CRITICAL') !== false) {
        $criticalLines[] = $line;
        // STDERR'e yaz - bu önemli!
        fwrite(STDERR, "[KRİTİK] $line" . PHP_EOL);
    }
}

fclose($handle);

// Özet raporu STDOUT'a
echo PHP_EOL . "=== Özet ===" . PHP_EOL;
echo "Toplam Hata: $errorCount" . PHP_EOL;
echo "Toplam Uyarı: $warningCount" . PHP_EOL;
echo "Kritik Satır: " . count($criticalLines) . PHP_EOL;

// Exit code ile sonuç bildir
exit(count($criticalLines) > 0 ? 1 : 0);
EOF

# Kullanım:
tail -n 1000 /var/log/nginx/error.log | php log_parser.php
# Veya dosyadan:
php log_parser.php < /var/log/nginx/error.log

Gerçek Dünya Senaryosu: Veritabanı Temizleme Script’i

İşte tam anlamıyla işe yarar bir script. MySQL veritabanındaki eski kayıtları temizleyen, transaction kullanan ve her şeyi loglayan bir örnek:

cat << 'EOF' > /etc/cron.daily/db_cleanup.php
#!/usr/bin/env php
<?php

define('LOG_FILE', '/var/log/db_cleanup.log');
define('BATCH_SIZE', 1000);

function writeLog(string $message, string $level = 'INFO'): void {
    $timestamp = date('Y-m-d H:i:s');
    $logLine = "[$timestamp] [$level] $message" . PHP_EOL;
    file_put_contents(LOG_FILE, $logLine, FILE_APPEND);
    
    if ($level === 'ERROR') {
        fwrite(STDERR, $logLine);
    } else {
        echo $logLine;
    }
}

// Ortam değişkenlerinden veya config dosyasından al
$config = [
    'host' => getenv('DB_HOST') ?: 'localhost',
    'dbname' => getenv('DB_NAME') ?: 'production',
    'user' => getenv('DB_USER') ?: 'cleaner',
    'pass' => getenv('DB_PASS') ?: '',
];

try {
    $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4";
    $pdo = new PDO($dsn, $config['user'], $config['pass'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);
    
    writeLog("Veritabanı bağlantısı kuruldu");
    
    $totalDeleted = 0;
    $cutoffDate = date('Y-m-d', strtotime('-90 days'));
    
    // Batch halinde sil - büyük tablolarda lock sorununu önle
    do {
        $pdo->beginTransaction();
        
        $stmt = $pdo->prepare("
            DELETE FROM session_logs 
            WHERE created_at < :cutoff 
            LIMIT :batch_size
        ");
        $stmt->bindValue(':cutoff', $cutoffDate);
        $stmt->bindValue(':batch_size', BATCH_SIZE, PDO::PARAM_INT);
        $stmt->execute();
        
        $deleted = $stmt->rowCount();
        $totalDeleted += $deleted;
        
        $pdo->commit();
        
        if ($deleted > 0) {
            writeLog("$deleted kayıt silindi (toplam: $totalDeleted)");
            sleep(1); // Sunucuya nefes aldır
        }
        
    } while ($deleted === BATCH_SIZE);
    
    writeLog("Temizlik tamamlandı. Toplam silinen: $totalDeleted kayıt");
    exit(0);
    
} catch (PDOException $e) {
    writeLog("Veritabanı hatası: " . $e->getMessage(), 'ERROR');
    exit(1);
} catch (Exception $e) {
    writeLog("Genel hata: " . $e->getMessage(), 'ERROR');
    exit(1);
}
EOF

chmod +x /etc/cron.daily/db_cleanup.php

Cron Job’larla Entegrasyon

PHP CLI script’lerini cron ile kullanırken dikkat etmeniz gereken bazı noktalar var. En büyük sorun genellikle PATH ve ortam değişkenleri.

# Crontab düzenle
crontab -e

Crontab içeriği şöyle olmalı:

# PHP'nin tam yolunu kullan, 'php' yazmak yetmeyebilir
# Her gece 2:30'da çalıştır, çıktıyı logla
30 2 * * * /usr/bin/php8.2 /var/scripts/db_cleanup.php >> /var/log/cleanup_cron.log 2>&1

# Ortam değişkeni gerekiyorsa
30 3 * * * DB_HOST=localhost DB_NAME=prod /usr/bin/php /var/scripts/db_cleanup.php

# Lock dosyası ile çakışmayı önle
*/5 * * * * flock -n /tmp/my_script.lock /usr/bin/php /var/scripts/worker.php

# Script başarısız olursa e-posta gönder
[email protected]
0 4 * * * /usr/bin/php /var/scripts/report.php || echo "Script başarısız oldu!"

Exit code’ların önemi burada ortaya çıkıyor. Cron, script’inizin başarılı olup olmadığını exit code’a bakarak anlıyor. exit(0) başarı, exit(1) ve üzeri hata demek.

Interactive CLI ve Kullanıcı Girişi

Bazen script’inizin kullanıcıdan girdi alması gerekir. Örneğin şifre sorması ya da onay beklemesi:

cat << 'EOF' > interactive_deploy.php
#!/usr/bin/env php
<?php

function prompt(string $question, bool $required = true): string {
    echo $question . " ";
    $input = trim(fgets(STDIN));
    
    if ($required && empty($input)) {
        echo "Bu alan boş bırakılamaz!" . PHP_EOL;
        return prompt($question, $required);
    }
    return $input;
}

function confirm(string $question): bool {
    echo $question . " [e/h]: ";
    $input = strtolower(trim(fgets(STDIN)));
    return in_array($input, ['e', 'evet', 'y', 'yes']);
}

function readPassword(string $prompt): string {
    echo $prompt . " ";
    // Terminal echo'yu kapat
    system('stty -echo');
    $password = trim(fgets(STDIN));
    system('stty echo');
    echo PHP_EOL;
    return $password;
}

// Kullanım
$env = prompt("Deploy ortamı (staging/production):");
$branch = prompt("Git branch:", true);

if ($env === 'production') {
    $password = readPassword("Production şifresi:");
    // Şifre doğrulama mantığı...
}

if (!confirm("$env ortamına $branch branch'ini deploy etmek istiyor musunuz?")) {
    echo "Deploy iptal edildi." . PHP_EOL;
    exit(0);
}

echo "Deploy başlıyor..." . PHP_EOL;
// Deploy mantığı buraya...
EOF

PHP CLI’de Performans ve Bellek Yönetimi

Uzun süre çalışan script’lerde bellek sızıntısı gerçek bir sorun. Web PHP’de her istek sonrası bellek temizlenirken, CLI’de script sonlanana kadar her şey bellekte tutulur.

cat << 'EOF' > memory_safe_processor.php
#!/usr/bin/env php
<?php

// Başlangıç bellek kullanımını göster
echo "Başlangıç belleği: " . round(memory_get_usage() / 1024 / 1024, 2) . " MB" . PHP_EOL;

// Büyük CSV dosyasını satır satır işle (tamamını belleğe alma!)
$csvFile = $argv[1] ?? '/tmp/large_data.csv';

if (!file_exists($csvFile)) {
    fwrite(STDERR, "Dosya bulunamadı: $csvFile" . PHP_EOL);
    exit(1);
}

$handle = fopen($csvFile, 'r');
$processedCount = 0;
$headers = fgetcsv($handle); // İlk satır header

while (($row = fgetcsv($handle)) !== false) {
    $data = array_combine($headers, $row);
    
    // İşlemi yap
    processRow($data);
    $processedCount++;
    
    // Her 1000 satırda bir durum raporu
    if ($processedCount % 1000 === 0) {
        $memUsage = round(memory_get_usage() / 1024 / 1024, 2);
        echo "rİşlenen: $processedCount | Bellek: {$memUsage} MB";
        
        // Bellek çok fazla şişiyorsa uyar
        if (memory_get_usage() > 200 * 1024 * 1024) {
            fwrite(STDERR, "Bellek limiti yaklaşıyor!" . PHP_EOL);
        }
    }
}

fclose($handle);
echo PHP_EOL . "Tamamlandı. Toplam: $processedCount satır" . PHP_EOL;
echo "Peak bellek: " . round(memory_get_peak_usage() / 1024 / 1024, 2) . " MB" . PHP_EOL;

function processRow(array $data): void {
    // İşlem mantığı
    // Önemli: fonksiyon scope'u bitince lokal değişkenler temizlenir
}
EOF

Faydalı PHP CLI Parametreleri

Günlük kullanımda işinize yarayacak parametreler:

  • -r “kod”: Tek satır PHP kodu çalıştırır
  • -a: Interactive mod (REPL), satır satır PHP girebilirsiniz
  • -l dosya.php: Syntax kontrolü yapar, çalıştırmaz
  • -f dosya.php: Dosyayı çalıştırır (-f opsiyoneldir, sadece php dosya.php de çalışır)
  • -d ayar=deger: php.ini ayarını geçersiz kılar
  • -e: Extended bilgi üretir (Xdebug için)
  • -n: php.ini dosyasını kullanmadan çalıştırır
  • -S host:port: Dahili web sunucu başlatır (geliştirme için)
  • –ri uzanti: Uzantı bilgisini gösterir

Pratik örnekler:

# Hızlı hesaplama
php -r "echo round(1024 * 1024 * 1024 / 1000000, 2) . ' MB';"

# Syntax kontrolü (deploy öncesi)
find /var/www/html -name "*.php" -exec php -l {} ; 2>&1 | grep -v "No syntax errors"

# Memory limitini geçici artır
php -d memory_limit=512M /var/scripts/heavy_process.php

# Xdebug ile çalıştır
php -d xdebug.mode=debug -d xdebug.start_with_request=yes script.php

# php.ini olmadan çalıştır (temiz ortam)
php -n -r "var_dump(ini_get('error_reporting'));"

Script’leri Servis Olarak Çalıştırmak

Uzun süre çalışan PHP CLI script’lerini (queue worker, daemon gibi) doğrudan cron’a değil, systemd’ye bırakmak çok daha doğru bir yaklaşım.

# /etc/systemd/system/php-worker.service oluştur
cat << 'EOF' > /etc/systemd/system/php-worker.service
[Unit]
Description=PHP Queue Worker
After=network.target mysql.service

[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/php /var/scripts/queue_worker.php
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment="DB_HOST=localhost"
Environment="DB_NAME=production"

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable php-worker
systemctl start php-worker
systemctl status php-worker
journalctl -u php-worker -f

Sonuç

PHP CLI, web geliştiricilerin genellikle görmezden geldiği ama sysadmin’lerin sevebileceği bir araç. Özellikle ekibinizde zaten PHP bilen geliştiriciler varsa, otomasyon script’lerini Bash yerine PHP ile yazmak ciddi bir avantaj sağlayabilir. Exception handling, OOP, PDO gibi modern araçlara erişim, tip güvenliği ve test edilebilirlik gibi konularda Bash’e göre çok daha güçlü.

Tabii PHP CLI’nin dezavantajları da yok değil. Bash kadar evrensel değil, her sunucuda PHP kurulu olmayabilir ve başlangıç maliyeti Bash’e göre daha yüksek. Ama veritabanı işlemleri, JSON/XML parsing, karmaşık iş mantığı gerektiren script’lerde PHP CLI rakipsiz.

En önemli ipuçları:

  • Script’lerinizde her zaman anlamlı exit code’lar kullanın
  • STDERR’i hata mesajları için, STDOUT’u normal çıktı için ayırın
  • Büyük veri setlerini satır satır işleyin, belleğe toptan yüklemeyin
  • Cron job’larında PHP’nin tam yolunu belirtin
  • Uzun çalışan servisler için systemd kullanın
  • Şifre gibi hassas bilgileri argüman olarak değil, ortam değişkeni olarak geçirin

PHP CLI’yi bir kez iş akışınıza dahil ettiğinizde, “bunu nasıl Bash ile yapardım” diye düşündüğünüz pek çok senaryoda kendinizi PHP yazarken bulacaksınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir