firefly-import-preprocessor/bin/transformer.php
Reindl David (IT-PTR-CEN2-SL10) 170b2d2016 release 1.0
2026-05-02 17:53:19 +02:00

544 lines
18 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* Firefly Import Preprocessor - Kommandozeilen-Einstiegspunkt
*
* PHP 8.1+ Tool zur Transformation von UBS E-Banking CSV-Exporten
* in ein Firefly III kompatibles Format.
*
* Voraussetzung: composer install
*/
require_once __DIR__ . '/../vendor/autoload.php';
use UbsCsvTransformer\TransformerEngine;
use UbsCsvTransformer\ConfigurationLoader;
use UbsCsvTransformer\FireflyImporter;
// ============================================================================
// CLI-Argument-Verarbeitung
// ============================================================================
$argc = $_SERVER['argc'] ?? 0;
$argv = $_SERVER['argv'] ?? [];
if ($argc < 2) {
showHelp();
exit(0);
}
// Debug-Modus aktivierbar
$debug = in_array('--debug', $argv) || in_array('-d', $argv);
// Extrahiere Kommando
$command = $argv[1];
try {
match ($command) {
'test' => handleTest($argc, $argv),
'transform' => handleTransform($argc, $argv),
'validate' => handleValidate($argc, $argv),
'auto-import' => handleAutoImport($argc, $argv),
'help', '-h', '--help' => showHelp(),
default => throw new Exception("Unbekanntes Kommando: $command"),
};
} catch (Exception $e) {
fwrite(STDERR, "\n❌ ERROR: " . $e->getMessage() . "\n\n");
exit(1);
}
// ============================================================================
// COMMAND HANDLERS
// ============================================================================
/**
* Zeige Hilfe und Verwendungsanleitung
*/
function showHelp(): void
{
echo <<<'HELP'
╔════════════════════════════════════════════════════════════════════════════╗
║ Firefly Import Preprocessor - Kommandozeilen-Tool ║
║ ║
║ Ein schlankes PHP 8 Tool zur Transformation von UBS E-Banking Exporten ║
║ in ein Firefly III kompatibles Format. ║
╚════════════════════════════════════════════════════════════════════════════╝
VERWENDUNG:
transformer [command] [options]
KOMMANDOS:
test [input] [config] [options]
Testet die Transformation mit limitierter Zeilenzahl
Optionen:
--rows=N Nur N Zeilen verarbeiten (Standard: 10)
--output=FILE, -o Ergebnis auch in Datei schreiben
Beispiel:
transformer test ubs-export.csv config.json --rows=5
transformer test ubs-export.csv config.json -o test-output.csv
transform [input] [config] [options]
Transformiert eine komplette CSV-Datei
Optionen:
--output=FILE, -o Output-Pfad (Standard: input-transformed.csv)
--no-import Nicht automatisch in Firefly III importieren
Beispiel:
transformer transform ubs-export.csv config.json
transformer transform ubs-export.csv config.json -o import.csv
validate [config] [options]
Validiert die Konfigurationsdatei
Optionen:
--strict Strikte Validierung (empfohlen)
Beispiel:
transformer validate config.json
transformer validate config.json --strict
auto-import [config] [options]
Überwacht Quellverzeichnis und verarbeitet neue Dateien
Optionen:
--watch Kontinuierliche Überwachung (Daemon-Modus)
--interval=SEC Prüfintervall in Sekunden (Standard: 60)
--dry-run Zeigt was gemacht würde (keine echte Verarbeitung)
Beispiel:
transformer auto-import config.json
transformer auto-import config.json --watch --interval=30
help, -h, --help
Zeige diese Hilfe
GLOBALE OPTIONEN:
--debug, -d Aktiviere Debug-Modus (detaillierte Ausgaben)
INSTALLATION:
1. PHP 8.1+ muss installiert sein
php --version
2. Autoloader-Setup (wähle eins):
Option A: Mit Composer (empfohlen)
composer install
Option B: Manuell - Dateien in Verzeichnisstruktur:
ff-imp-preprocessor/
├── bin/transformer.php
├── src/*.php
└── config/config.json
3. Ausführbar machen:
chmod +x bin/transformer.php
4. Konfiguration anpassen:
cp config/config.example.json config/config.json
nano config/config.json
BEISPIELE:
# Transformation mit Test (erste 5 Zeilen)
./bin/transformer test data/ubs-export.csv config/config.json --rows=5
# Komplette Transformation
./bin/transformer transform data/ubs-export.csv config/config.json \
--output=output/firefly-import.csv
# Konfiguration validieren
./bin/transformer validate config/config.json --strict
# Auto-Import mit Überwachung starten
./bin/transformer auto-import config/config.json --watch
# Nur nächste Datei verarbeiten
./bin/transformer auto-import config/config.json
KONFIGURATION:
Die config.json muss folgende Struktur haben:
{
"metadata": { "extractionRules": {...} },
"csvStructure": { "delimiter": ";", ... },
"columnTransformations": { ... },
"fireflyImport": { "apiUrl": "...", "apiKey": "..." },
"directories": {
"source": "./import/source",
"output": "./import/output",
"archive": "./import/archive",
"error": "./import/error"
}
}
DOKUMENTATION:
Siehe README.md und UBS_Transformer_Guide.md für vollständige Dokumentation
LIZENZ:
MIT License
HELP;
}
/**
* Expandiert ~ zu absolutem Home-Verzeichnis und löst relative Pfade auf
*/
function expandPath(string $path): string
{
if (str_starts_with($path, '~/') || $path === '~') {
$home = getenv('HOME') ?: posix_getpwuid(posix_getuid())['dir'];
$path = $home . substr($path, 1);
}
// Relative Pfade gegen cwd auflösen (ohne realpath, damit nicht-existierende Dirs erlaubt sind)
if (!str_starts_with($path, '/')) {
$path = getcwd() . '/' . $path;
}
return $path;
}
/**
* Parse CLI-Optionen in assoziatives Array
*/
function parseOptions(array $argv, int $startIndex = 0): array
{
$options = [];
for ($i = $startIndex; $i < count($argv); $i++) {
if (strpos($argv[$i], '--') === 0) {
$parts = explode('=', substr($argv[$i], 2), 2);
$options[$parts[0]] = $parts[1] ?? true;
} elseif (strpos($argv[$i], '-') === 0 && strlen($argv[$i]) > 1) {
$options[substr($argv[$i], 1)] = true;
}
}
return $options;
}
/**
* Teste Transformation mit begrenzter Zeilenzahl
*/
function handleTest($argc, $argv): void
{
if ($argc < 4) {
throw new Exception("Usage: transformer test [input-file] [config-file] [options]");
}
$inputFile = $argv[2];
$configFile = $argv[3];
$options = parseOptions($argv, 4);
$debug = isset($options['debug']) || isset($options['d']);
$maxRows = isset($options['rows']) ? (int)$options['rows'] : 10;
$outputFile = $options['output'] ?? $options['o'] ?? null;
if (!file_exists($inputFile)) {
throw new Exception("Input-Datei nicht gefunden: $inputFile");
}
if (!file_exists($configFile)) {
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
}
echo "\n📊 TEST-MODUS: Verarbeite max. $maxRows Zeilen\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$configLoader = new ConfigurationLoader($configFile);
$config = $configLoader->load();
$engine = new TransformerEngine($configLoader, $debug);
$result = $engine->transform($inputFile, $maxRows);
// Ausgabe Statistiken
echo "\n✅ STATISTIKEN:\n";
echo " Verarbeitete Zeilen: " . $result['rowsProcessed'] . "\n";
echo " Metadaten extrahiert: " . count($result['metadata'] ?? []) . "\n";
echo " Output-Spalten: " . $result['outputColumns'] . "\n";
if (!empty($result['metadata'])) {
echo "\n📋 EXTRAHIERTE METADATEN:\n";
foreach ($result['metadata'] as $key => $value) {
$display = substr($value, 0, 50);
if (strlen($value) > 50) {
$display .= "...";
}
echo " $key: $display\n";
}
}
if (!empty($result['sampleData'])) {
$sampleCount = min(5, count($result['sampleData']));
echo "\n📝 BEISPIEL-DATEN ($sampleCount Zeilen):\n";
foreach (array_slice($result['sampleData'], 0, $sampleCount) as $index => $row) {
echo " Zeile " . ($index + 1) . ": ";
foreach ($row as $col => $value) {
$val = substr($value, 0, 30);
if (strlen($value) > 30) {
$val .= "...";
}
//if (!is_int($col)) {
echo "$col=$val | ";
//}
}
echo "\n";
}
}
if ($outputFile) {
echo "\n💾 Output-Datei: $outputFile\n";
}
echo "\n✅ Test erfolgreich!\n\n";
}
/**
* Transformiere komplette CSV-Datei
*/
function handleTransform($argc, $argv): void
{
if ($argc < 4) {
throw new Exception("Usage: transformer transform [input-file] [config-file] [options]");
}
$inputFile = $argv[2];
$configFile = $argv[3];
$options = parseOptions($argv, 4);
$debug = isset($options['debug']) || isset($options['d']);
$outputFile = $options['output'] ?? $options['o'] ?? null;
if (!file_exists($inputFile)) {
throw new Exception("Input-Datei nicht gefunden: $inputFile");
}
if (!file_exists($configFile)) {
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
}
echo "\n🚀 TRANSFORMATION STARTEN\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$configLoader = new ConfigurationLoader($configFile);
$configLoader->load();
// --output überschreibt Zielverzeichnis und Dateiname aus der Konfiguration
if ($outputFile !== null) {
$outputFile = expandPath($outputFile);
$configLoader->set('directories.output', dirname($outputFile));
$configLoader->set('csvStructure.outputFilename', basename($outputFile));
}
$engine = new TransformerEngine($configLoader, $debug);
$result = $engine->transform($inputFile);
echo "✅ Transformation erfolgreich!\n";
echo " Output-Datei: " . ($result['outputFile'] ?? 'N/A') . "\n";
echo " Zeilen transformiert: " . ($result['rowsProcessed'] ?? 0) . "\n";
echo "\n✅ Fertig!\n\n";
}
/**
* Validiere Konfigurationsdatei
*/
function handleValidate($argc, $argv): void
{
if ($argc < 3) {
throw new Exception("Usage: transformer validate [config-file] [options]");
}
$configFile = $argv[2];
$options = parseOptions($argv, 3);
$strict = isset($options['strict']);
if (!file_exists($configFile)) {
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
}
echo "\n✔️ KONFIGURATION VALIDIEREN\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$configLoader = new ConfigurationLoader($configFile);
try {
$config = $configLoader->load();
// Basis-Validierung
echo "✅ JSON-Format valide\n";
$required = ['metadata', 'csvStructure', 'columnTransformations'];
$missing = [];
foreach ($required as $key) {
if (isset($config[$key])) {
echo "✅ Abschnitt '$key' vorhanden\n";
} else {
echo "⚠️ Abschnitt '$key' fehlt\n";
$missing[] = $key;
}
}
// Firefly-Validierung
if (isset($config['fireflyImport'])) {
echo "✅ Firefly III Konfiguration vorhanden\n";
if (empty($config['fireflyImport']['apiUrl'])) {
echo "⚠️ Firefly III API-URL fehlt\n";
if ($strict) {
throw new Exception("Strikte Validierung: Firefly III API-URL erforderlich");
}
}
if (empty($config['fireflyImport']['apiKey'])) {
echo "⚠️ Firefly III API-Key fehlt\n";
if ($strict) {
throw new Exception("Strikte Validierung: Firefly III API-Key erforderlich");
}
}
} else {
echo "⚠️ Firefly III Konfiguration nicht vorhanden (optional)\n";
}
// Verzeichnisse-Validierung
if (isset($config['directories'])) {
echo "✅ Verzeichnisse konfiguriert\n";
$dirs = ['source', 'output', 'archive', 'error'];
foreach ($dirs as $dir) {
if (!empty($config['directories'][$dir])) {
echo "$dir: " . $config['directories'][$dir] . "\n";
}
}
}
if (empty($missing)) {
echo "\n✅ Konfiguration ist VALIDE!\n\n";
} else {
if ($strict) {
throw new Exception("Strikte Validierung: " . count($missing) . " erforderliche Abschnitte fehlen");
}
echo "\n⚠️ Konfiguration hat Warnungen aber ist funktional\n\n";
}
} catch (Exception $e) {
throw new Exception("Validierungsfehler: " . $e->getMessage());
}
}
/**
* Auto-Import mit Verzeichnis-Überwachung
*/
function handleAutoImport($argc, $argv): void
{
if ($argc < 3) {
throw new Exception("Usage: transformer auto-import [config-file] [options]");
}
$configFile = $argv[2];
$options = parseOptions($argv, 3);
$debug = isset($options['debug']) || isset($options['d']);
if (!file_exists($configFile)) {
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
}
$configLoader = new ConfigurationLoader($configFile);
$config = $configLoader->load();
$sourceDir = $config['directories']['source'] ?? './import/source';
$outputDir = $config['directories']['output'] ?? './import/output';
$archiveDir = $config['directories']['archive'] ?? './import/archive';
$errorDir = $config['directories']['error'] ?? './import/error';
$dryRun = isset($options['dry-run']);
$watch = isset($options['watch']);
$interval = isset($options['interval']) ? (int)$options['interval'] : 60;
// Verzeichnisse erstellen
foreach ([$sourceDir, $outputDir, $archiveDir, $errorDir] as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
echo "\n🔍 AUTO-IMPORT GESTARTET\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo " Quellverzeichnis: $sourceDir\n";
echo " Output-Verzeichnis: $outputDir\n";
echo " Archiv-Verzeichnis: $archiveDir\n";
if ($watch) {
echo " Mode: WATCH (kontinuierlich)\n";
echo " Intervall: {$interval}s\n";
} else {
echo " Mode: EINMALIG\n";
}
if ($dryRun) {
echo " Dry-Run: JA (keine echten Operationen)\n";
}
echo "\n";
if ($watch) {
echo "⏳ Drücke Ctrl+C zum Beenden.\n\n";
while (true) {
processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug);
sleep($interval);
}
} else {
processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug);
}
}
/**
* Verarbeite Verzeichnis mit CSV-Dateien
*/
function processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun = false, $debug = false): void
{
if (!is_dir($sourceDir)) {
return;
}
$files = glob($sourceDir . '/*.csv');
if (empty($files)) {
return;
}
foreach ($files as $file) {
$basename = basename($file);
try {
echo "📄 Verarbeite: $basename ... ";
$configLoader = new ConfigurationLoader($configFile);
$config = $configLoader->load();
$engine = new TransformerEngine($configLoader, $debug);
$outputFile = $outputDir . '/' . str_replace('.csv', '-transformed.csv', $basename);
if (!$dryRun) {
$result = $engine->transform($file);
$outputFile = $result['outputFile'] ?? $outputFile;
// Archiviere Original-Datei
$archiveFile = $archiveDir . '/' . $basename;
if (!rename($file, $archiveFile)) {
throw new Exception("Konnte nicht archivieren");
}
// Firefly Import
if (!empty($config['fireflyImport'])) {
$importer = new FireflyImporter($config['fireflyImport']);
$importer->import($outputFile);
}
}
echo "\n";
} catch (Exception $e) {
echo "" . $e->getMessage() . "\n";
if (!$dryRun) {
// Verschiebe zu Error-Verzeichnis
$errorFile = $errorDir . '/' . $basename;
@rename($file, $errorFile);
}
}
}
}