Externe Skripte in PHP sicher ausführen
Das Automatisieren von Aufgaben in PHP kann Ihren Workflow erheblich straffen, manuellen Aufwand reduzieren und die Fähigkeiten Ihrer Anwendung erweitern. Ein wirkungsvoller Weg dorthin besteht darin, externe eigene Skripte direkt aus Ihrem PHP-Code auszuführen. In dieser Anleitung zeigen wir, wie Sie externe Skripte effektiv ausführen, Aufgaben automatisieren und Ihre PHP-Automatisierung mit der Symfony-Process-Komponente verbessern.
Einführung
Das Ausführen externer Skripte aus PHP heraus erlaubt es Entwicklern, vorhandene Kommandozeilen-Tools zu nutzen, Skripte auszuführen, die in anderen Sprachen geschrieben sind, oder komplexe Vorgänge in isolierten Prozessen durchzuführen. Das ist nützlich, um wiederkehrende Aufgaben zu automatisieren, Funktionen zu erweitern und komplexe Workflows in Ihren PHP-Anwendungen zu vereinfachen.
Voraussetzungen
Bevor wir einsteigen, stellen Sie sicher, dass Sie Folgendes haben:
- PHP 8.2 oder neuer installiert (Symfony Process 7.x erfordert PHP 8.2+)
- Composer für die Verwaltung von Abhängigkeiten installiert
- Grundkenntnisse im PHP-Scripting und auf der Kommandozeile
- Die PHP-Funktion
proc_openist aktiviert; PCNTL- und POSIX-Erweiterungen werden von Symfony Process nicht benötigt
Die Umgebung einrichten
Erstellen Sie zunächst ein neues Projektverzeichnis und initialisieren Sie Composer:
mkdir php-external-scripts
cd php-external-scripts
composer init --name=example/php-external-scripts --description="PHP External Scripts Example" --require="symfony/process:^7.0" --no-interaction
composer install
Wir verwenden die Process-Komponente von Symfony, um externe Skripte sicher und effizient auszuführen. Diese Komponente bietet einen robusten objektorientierten Wrapper für die Prozessverwaltung.
Ein eigenes Skript zum Ausführen schreiben
Erstellen wir ein einfaches PHP-Skript namens hello.php, das wir aus einem anderen PHP-Skript heraus ausführen:
<?php
// hello.php
$name = $argv[1] ?? 'World';
echo "Hello, {$name}!\n";
Dieses Skript nimmt ein optionales Kommandozeilenargument entgegen und gibt eine Begrüßung aus.
Das Skript aus PHP ausführen
Erstellen Sie eine neue PHP-Datei namens run-script.php, um Ihr eigenes Skript mit der
Symfony-Process-Komponente auszuführen:
<?php
// run-script.php
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
$scriptPath = __DIR__ . '/hello.php';
$nameArgument = 'Developer'; // Example argument
// Basic validation: Check if the script exists and is readable
if (!file_exists($scriptPath) || !is_readable($scriptPath)) {
throw new RuntimeException('Script file not found or not readable: ' . $scriptPath);
}
// Create the process instance
// Pass arguments safely as separate array elements
$process = new Process(['php', $scriptPath, $nameArgument]);
// Set timeouts for security and resource management
$process->setTimeout(60); // Maximum execution time (seconds)
$process->setIdleTimeout(30); // Maximum time allowed without any output (seconds)
try {
// Execute the process
$process->mustRun();
// Get the output
echo $process->getOutput();
} catch (ProcessFailedException $exception) {
error_log('Greeting process failed with exit code ' . $process->getExitCode());
throw new RuntimeException('The script execution failed.', 0, $exception);
}
Der Befehl php muss auf eine vertrauenswürdige ausführbare PHP-CLI-Datei verweisen. Verwenden Sie
den absoluten Pfad, wenn Ihre Deployment-Umgebung einen eingeschränkten PATH hat, insbesondere beim
Aufruf aus PHP-FPM.
Führen Sie dieses Skript in Ihrem Terminal aus:
php run-script.php
Sie sollten die Ausgabe von hello.php sehen:
Hello, Developer!
Praktische Beispiele
Dateiverarbeitung automatisieren
Angenommen, Sie möchten die Größenänderung von Bildern mit dem Kommandozeilen-Tool ImageMagick
automatisieren. Dafür könnten Sie ein PHP-Skript namens resize-image.php erstellen, das dies steuert:
<?php
// resize-image.php
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
$inputFile = 'input.jpg'; // Potentially user-provided
$outputFile = 'output-' . bin2hex(random_bytes(16)) . '.jpg'; // New name for each run
$targetWidth = 200;
$targetHeight = 200;
// These roots are fixed by the application, never supplied by the caller.
$inputRoot = realpath(__DIR__ . '/private/input');
$outputRoot = realpath(__DIR__ . '/private/output');
if ($inputRoot === false || $outputRoot === false || !is_writable($outputRoot)) {
throw new RuntimeException('Private processing directories are not configured.');
}
// Validate the whole name. Reject paths, leading options, and ImageMagick pseudo-filenames.
$filenamePattern = '/\A[a-zA-Z0-9][a-zA-Z0-9_-]*\.(?:jpg|jpeg|png)\z/i';
if (!preg_match($filenamePattern, $inputFile) || !preg_match($filenamePattern, $outputFile)) {
throw new InvalidArgumentException('Invalid filename provided.');
}
$inputPath = realpath($inputRoot . DIRECTORY_SEPARATOR . $inputFile);
if ($inputPath === false || !is_file($inputPath) || !is_readable($inputPath) ||
!str_starts_with($inputPath, $inputRoot . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Input file is not available in the trusted directory.');
}
$outputPath = $outputRoot . DIRECTORY_SEPARATOR . $outputFile;
if (file_exists($outputPath) || is_link($outputPath)) {
throw new RuntimeException('Output already exists. Choose a new output name.');
}
// Use modern ImageMagick `magick` command
// **Security**: Pass arguments as separate array elements to Process constructor
// This avoids shell injection vulnerabilities; escapeshellarg() is not needed here.
$command = [
'magick',
$inputPath,
'-resize',
"{$targetWidth}x{$targetHeight}",
$outputPath
];
$process = new Process($command);
$process->setTimeout(120); // Longer timeout for potentially slow image processing
// ImageMagick can be quiet while working; rely on the total timeout here.
try {
echo "Attempting to resize '{$inputFile}' to '{$outputFile}'...\n";
$process->mustRun();
echo "Image resized successfully!\n";
} catch (ProcessFailedException $exception) {
error_log('Image resizing failed with exit code ' . $process->getExitCode());
throw new RuntimeException('The image resizing command failed.', 0, $exception);
}
Stellen Sie sicher, dass ImageMagick installiert und der Befehl magick in Ihrem
vertrauenswürdigen System-PATH verfügbar ist. Legen Sie private/input und private/output außerhalb des Webroots an und
hinterlegen Sie ein echtes JPEG unter private/input/input.jpg. Halten Sie beide Verzeichnisse unter Kontrolle
der Anwendung, damit nicht vertrauenswürdige Benutzer zwischen Validierung und Ausführung keine
Dateien oder Symlinks ersetzen können. Die Ausgabe darf noch nicht existieren. Jeder Lauf erzeugt
einen neuen Ausgabedateinamen, sodass geplante Läufe frühere Ergebnisse nicht überschreiben.
Wenden Sie auf diese Ausgaben eine Aufbewahrungsrichtlinie sowie die Ressourcen- und
Sicherheitsrichtlinien von ImageMagick für nicht vertrauenswürdige Bilder an.
Geplante Aufgaben
Sie können Cronjobs (unter Linux/macOS) oder die Aufgabenplanung (unter Windows) nutzen, um die Ausführung Ihrer PHP-Skripte zu planen, die andere Prozesse starten.
Beispiel für einen Cronjob-Eintrag (mit crontab -e bearbeiten):
# Run the image resizing script every hour
0 * * * * /usr/bin/php /full/path/to/your/project/resize-image.php >> /full/path/to/your/logs/resize.log 2>&1
Ersetzen Sie /usr/bin/php durch den korrekten Pfad zu Ihrer ausführbaren PHP-Datei und verwenden Sie
absolute Pfade für Ihr Skript und Ihre Logdatei. Der Teil >> ... 2>&1 hängt sowohl die Standardausgabe
als auch die Standardfehlerausgabe an die Logdatei an.
Fehlerbehandlung und bewährte Verfahren
Sicherheitsaspekte
- Eingabevalidierung: Vertrauen Sie externen Eingaben (etwa Dateinamen oder Argumenten, die an Skripte übergeben werden) niemals direkt. Validieren und bereinigen Sie Eingaben stets sorgfältig. Verwenden Sie reguläre Ausdrücke oder konkrete Allow-Lists für Dateinamen. Prüfen Sie, ob Dateien existieren, und kontrollieren Sie die Berechtigungen.
- Shell-Ausführung vermeiden: Verwenden Sie nach Möglichkeit
Processmit einem Array aus Befehlsbestandteilen (z. B.new Process(['command', 'arg1', 'arg2'])) statt einer einzelnen Befehlszeichenkette. Das umgeht die Shell und verhindert Shell-Injection, wodurch Funktionen wieescapeshellarg()oderescapeshellcmd()für die Argumente selbst überflüssig werden. Wenn Sie einen Befehl über die Shell ausführen müssen, seien Sie beim Escaping äußerst sorgfältig. Argument-Arrays verhindern weder Option-Injection noch die eigene Dateinamen-Syntax eines Tools; validieren Sie diese Werte separat. - Timeouts setzen: Legen Sie ein Gesamt-Timeout für die Ausführung fest (
setTimeout). Verwenden SiesetIdleTimeoutnur für Tools, von denen regelmäßige Ausgaben zu erwarten sind; andernfalls kann ein einwandfrei, aber still laufender Prozess beendet werden. - Berechtigungen einschränken: Führen Sie Ihre PHP-Skripte und die von ihnen gestarteten Prozesse mit den minimal notwendigen Rechten aus. Vermeiden Sie die Ausführung als root.
Umfassende Fehlerbehandlung
Protokollieren Sie den Vorgang und den Exit-Code und halten Sie dabei Befehlsargumente sowie ungefilterte Ausgaben aus den Logs heraus, wenn diese Geheimnisse oder nicht vertrauenswürdige Inhalte enthalten können. Geben Sie in einer Webanwendung eine feste, für Benutzer sichtbare Fehlermeldung zurück und belassen Sie Exceptions und Stacktraces auf dem Server. Dieser Auszug verwendet die oben genannten Imports weiter:
<?php
try {
$process->mustRun();
// Process finished successfully
$output = $process->getOutput();
// Log success or process output
} catch (ProcessFailedException $exception) {
error_log('Exit Code: ' . $exception->getProcess()->getExitCode());
// Implement alerting or recovery logic here
throw $exception; // Re-throw if needed
} catch (Exception $e) {
// Catch other potential exceptions (e.g., LogicException from Process)
error_log('Process error: ' . get_class($e));
throw $e;
}
Ressourcenverwaltung
Für lang laufende oder ressourcenintensive Skripte:
-
Speicherlimits: Übergeben Sie die PHP-Direktive
memory_limitmit-d, wenn Sie einen PHP-Kindprozess erstellen. Das begrenzt den von PHP verwalteten Speicher; ImageMagick oder andere Programme werden dadurch nicht eingeschränkt.<?php $process = new Process(['php', '-d', 'memory_limit=256M', $scriptPath, $nameArgument]); -
Hintergrundprozesse: Führen Sie Aufgaben, die keine sofortigen Ergebnisse benötigen, asynchron aus.
<?php $process->start(); // Do independent work here, then wait before inspecting the result or exiting. $process->wait(); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); }start()ist innerhalb der Lebensdauer dieses PHP-Prozesses asynchron. Für Arbeiten, die das Beenden des Elternprozesses überdauern müssen, verwenden Sie einen Queue-Worker oder einen Service-Supervisor; der Destruktor von Process beendet einen laufenden Kindprozess.
Fazit
Das Ausführen externer Skripte aus PHP mit der Symfony-Process-Komponente ist eine wirkungsvolle Technik, um Aufgaben zu automatisieren, andere Tools einzubinden und die Möglichkeiten Ihrer PHP-Anwendungen zu erweitern. Wenn Sie bewährte Sicherheitsverfahren befolgen (Eingabevalidierung, Verzicht auf Shell-Ausführung, Setzen von Timeouts) und eine robuste Fehlerbehandlung umsetzen, können Sie die Ausführung externer Skripte zuverlässig in Ihre Workflows integrieren.
Mit diesen Techniken bauen Sie ausgefeilte Automatisierungssysteme, straffen die Entwicklung und reduzieren manuellen Aufwand in Ihren PHP-Projekten.
Für komplexe Workflows zur Medienverarbeitung und Automatisierung können Sie auch Dienste wie Transloadit in Betracht ziehen, die eine verwaltete Infrastruktur für Aufgaben wie Video-Encoding und Bildbearbeitung über APIs bereitstellen.
