PHP
ZUGFeRD mit PHP: Wie ich das horstoeko/zugferd-Paket lokal vorbereitet und ohne Composer-Zugriff auf den Server gebracht habe
Wer schon einmal versucht hat, das ZUGFeRD-Format mit PHP umzusetzen, wird früher oder später auf das Projekt horstoeko/zugferd stoßen. Es bietet eine mächtige Möglichkeit, ZUGFeRD-konforme Rechnungsdaten zu erstellen und in PDF-Dokumente einzubetten. Doch gerade am Anfang lauern einige Stolpersteine: Composer, Pfadprobleme, Server ohne Shell-Zugriff. Dieser Beitrag zeigt, wie ich mir mit einem lokalen Setup, GitKraken und einem simplen Upload-Trick geholfen habe, um trotz aller Einschränkungen produktiv arbeiten zu können.
Bevor ich das Paket überhaupt einbinden konnte, musste Composer einmal lokal installiert werden – ganz ohne kommt man nicht aus. Ich habe mich für den Weg über die offizielle Installationsanleitung entschieden:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php composer-setup.php php -r "unlink('composer-setup.php');"
Es gibt aber auch fertige Pakete als *.exe für Windows.
GitKraken, Composer & das Terminal
Ich arbeite gerne visuell, und daher ist GitKraken mein bevorzugter Git-Client. Doch ein oft unterschätzter Vorteil: GitKraken bringt ein eigenes Terminal mit. Dieses habe ich genutzt, um Composer lokal zu verwenden – ohne die globale Composer-Installation auf meinem Server-System anfassen zu müssen.

# Im Terminal von GitKraken composer require horstoeko/zugferd
Dabei habe ich mich bewusst für die 1.x
-Version entschieden, da diese eine stabilere und besser dokumentierte Grundlage für den Einsatz ohne komplexes Setup bietet. Zudem ist dort der ZugferdDocumentPdfBuilder
enthalten, der es erlaubt, das gesamte PDF-Handling im PHP-Kosmos zu belassen. Soweit ich gesehen habe, gibt es wohl auch DEV-Versionen, aber ich war mir nicht sicher wie weit diese nutzbar sind.
Der Upload-Trick: Alles lokal vorbereiten
Da mein Zielserver keinen Composer-Zugriff bietet, musste ich alles lokal vorbereiten. Ich nutze für meine Testumgebung einen einfachen Server von AllInk. Das ist extrem kostengünstig, aber eigene Software installieren, Fehlanzeige.
Der Trick: Ich habe den gesamten vendor
-Ordner inklusive composer.json
und composer.lock
gezippt und manuell auf den Server übertragen. Das spart nicht nur Zeit, sondern funktioniert in jeder Hostingumgebung.
# Lokaler Aufbau my-project/ ├── src/ ├── vendor/ ├── composer.json ├── composer.lock
Dann per SFTP oder FTP hochladen und sicherstellen, dass im PHP-Code folgender Autoloader korrekt eingebunden wird:
require __DIR__ . '/vendor/autoload.php';
Vorsicht, Pfade: Die Sache mit dem “/src”-Unterordner
Ein Stolperstein war die Struktur des horstoeko-Pakets. Die Klassen liegen nicht direkt im Projektverzeichnis, sondern verstecken sich unter:
/vendor/horstoeko/zugferd/src/...
Der PSR-4-Autoloader von Composer ist darauf vorbereitet, aber wer manuell Klassen einbindet oder den Autoloader nicht korrekt referenziert, bekommt Fehler. Ein Test mit:
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
funktionierte erst, nachdem ich sicher war, dass der Autoloader geladen war und keine Pfade fehlten.
Endlich produktiv: Der erste Builder-Lauf
Nachdem alles hochgeladen und die Autoloading-Probleme beseitigt waren, konnte ich mein erstes ZUGFeRD-Dokument bauen:
$builder = new ZugferdDocumentPdfBuilder(); $builder->setDocumentFile("./rechnung.pdf"); $builder->setZugferdXml("./debug_12345.xml"); $builder->saveDocument("./zugferd_12345_final.pdf");
Und siehe da: eine ZUGFeRD-konforme PDF-Datei, direkt aus PHP erzeugt. Kein Java, kein PDF/A-Tool von Adobe, keine Blackbox. Wichtig, das ganze ist per ZIP auf jeden Kundenserver übertragbar.
Warum kein Java?
Ich habe bewusst darauf verzichtet, Java-Tools wie Apache PDFBox oder gar die offizielle ZUGFeRD Java Library zu nutzen – aus einem ganz einfachen Grund: Ich wollte die Lösung so nah wie möglich an meiner bestehenden PHP-Infrastruktur halten. Keine zusätzliche Runtime, keine komplexen Abhängigkeiten, keine Übersetzungsprobleme zwischen Systemen. PHP allein reicht – wenn man die richtigen Werkzeuge nutzt.
Häufige Fehlermeldungen und ihre Lösungen
Gerade beim Einstieg in das horstoeko/zugferd-Paket können einige typische Fehlermeldungen auftreten:
Fehler: Class 'horstoeko\zugferd\ZugferdDocumentPdfBuilder' not found
// Lösung: require_once __DIR__ . '/vendor/autoload.php';
Fehler: Cannot open file ./debug_12345.xml
// Lösung: Pfad prüfen! Gerade bei relativen Pfaden kann es helfen, alles absolut zu machen: $builder->setZugferdXml(__DIR__ . '/debug_12345.xml');
Fehler: Output file cannot be written
// Lösung: Schreibrechte auf dem Zielverzeichnis prüfen! Ein chmod 775 oder 777 (mit Bedacht!) kann helfen.
Fazit: Wer wie ich auf Servern ohne Composer arbeiten muss oder will, kann sich mit einem lokalen Setup, GitKraken und einem Zip-Upload wunderbar behelfen. Wichtig ist, auf die Pfade zu achten, den Autoloader korrekt einzubinden und nicht vor kleinen Hürden zurückzuschrecken. Die Möglichkeiten, die das horstoeko/zugferd-Paket bietet, machen die Mühe mehr als wett.
Zumal das ganze Setup, 1 zu 1, auf einen Kundenserver übertragen werden kann. Die eigentlichen Daten kommen aus FileMaker, dieser holt sich die PDF und das XML auch wieder vom Server ab. Somit ist die Erstellung der ZUGFeRD-PDF und der XML mit einen FileMaker-Script abzudecken. Für die Erstellung auf dem Server bedarf es zweier PHP-Scripte. Dazu das Horstoeko/zugferd-Paket.
automatisierter UBL-XML-Generator in PHP in Kombination mit FileMaker

Während meiner Arbeit an der serverseitigen PDF-Generierung mit ZUGFeRD wurde mir schnell klar, dass viele Kunden zunehmend auf den UBL-Standard setzen – gerade im internationalen Kontext oder in Verbindung mit elektronischen Rechnungsplattformen. Also habe ich kurzerhand ein eigenes PHP-Skript geschrieben, das auf POST-Daten aus FileMaker oder anderen Quellen reagiert und daraus eine gültige UBL-Rechnung im XML-Format erstellt.
Wie so oft war der Aufbau des XML-Dokuments der anspruchsvollste Teil. Viele Details wie Namespaces, Pflichtfelder und ISO-konforme Datums- und Betragsformate mussten exakt stimmen. Außerdem wollte ich vermeiden, dass mein System bei fehlenden Daten abstürzt – darum habe ich Fallbacks eingebaut und ein eigenes Logging-System integriert.
Das Skript liest die Rechnungsdaten, Kunden- und Lieferantendaten sowie die Rechnungspositionen ein, berechnet die Summen und schreibt daraus ein vollständiges XML-Dokument nach dem UBL 2.1-Standard, das sich z. B. auch für die XRechnung weiterverwenden lässt. Die resultierende Datei ist kompatibel mit Plattformen wie PEPPOL, eRechnung.gv.at oder Zentralplattformen der öffentlichen Hand.
Die Daten werden in FileMaker gesammelt, das ganze klassisch über schleifen. Die Daten werden über ein einfaches application/x-www-form-urlencoded-POST-Request übergeben. Alle Felder werden als Key-Value-Paare übermittelt. Die Rechnungspositionen (line items) sind dabei als kompaktes Raw-String-Feld lineItemsRaw codiert, das einzelne Positionen mit | trennt und innerhalb der Position durch ; strukturiert ist.
FileMaker bietet zwar mittlerweile solide Funktionen für JSON-Manipulation – aber bei 25+ Feldern und einer schlichten Punkt-zu-Punkt-Kommunikation mit meinem PHP-Skript war mir das einfach zu umständlich. Ich wollte keine JSON-Parser-Bastelei, sondern einfach Daten senden. Daher nutze ich application/x-www-form-urlencoded, was mit curl ohnehin besser lesbar ist und mir in PHP direkt über $_POST zur Verfügung steht.
“-X POST " & “–header "Content-Type: application/x-www-form-urlencoded" " & “–data " & Zitat ( “invoiceNumber=” & $invoiceNumber & “&invoiceDate=” & $invoiceDate & “&invoiceCurrencyCode=” & $invoiceCurrencyCode & “&invoiceTypeCode=” & $invoiceTypeCode & “&dueDate=” & $dueDate & “&paymentTerms=” & $paymentTerms & “&deliveryTerms=” & $deliveryTerms &
“&sellerName=” & $sellerName & “&sellerStreet=” & $sellerStreet & “&sellerPostalCode=” & $sellerPostalCode & “&sellerCity=” & $sellerCity & “&sellerCountryCode=” & $sellerCountryCode & “&sellerTaxID=” & $sellerTaxID &
“&lieferschein_nr=” & $lieferschein_nr & “&kunden_nr=” & $kunden_nr &
“&buyerName=” & $buyerName & “&buyerStreet=” & $buyerStreet & “&buyerPostalCode=” & $buyerPostalCode & “&buyerCity=” & $buyerCity & “&buyerCountryCode=” & $buyerCountryCode & “&buyerTaxID=” & $buyerTaxID &
“&paymentMeansCode=” & $paymentMeansCode & “&payeeFinancialInstitution=” & $payeeFinancialInstitution & “&payeeIBAN=” & $payeeIBAN & “&payeeBIC=” & $payeeBIC & “&paymentReference=” & $paymentReference &
“&taxRate=” & $taxRate & “&taxAmount=” & $taxAmount & “&taxableAmount=” & $taxableAmount & “&taxCategoryCode=” & $taxCategoryCode &
“&totalNetAmount=” & $totalNetAmount & “&totalTaxAmount=” & $totalTaxAmount & “&totalGrossAmount=” & $totalGrossAmount & “&lineItemsRaw=” & $lineItemsRaw )
$dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $invoice = $dom->createElementNS( 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', 'Invoice' ); $invoice->setAttribute('xmlns:cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); $invoice->setAttribute('xmlns:cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'); $dom->appendChild($invoice); // Standardfelder setzen $invoice->appendChild($dom->createElement('cbc:UBLVersionID', '2.1')); $invoice->appendChild($dom->createElement('cbc:CustomizationID', 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0')); $invoice->appendChild($dom->createElement('cbc:ID', $invoiceNumber)); $invoice->appendChild($dom->createElement('cbc:IssueDate', $invoiceDate)); $invoice->appendChild($dom->createElement('cbc:InvoiceTypeCode', '380')); $invoice->appendChild($dom->createElement('cbc:DocumentCurrencyCode', 'EUR'));
Der Aufbau geht dann weiter über Verkäufer- und Käuferdaten, Zahlungsinformationen, steuerliche Angaben und natürlich die Rechnungspositionen, die als cac:InvoiceLine-Blöcke angelegt werden.
Die Daten werden direkt auf dem Server verarbeitet, im Anschluss kann ich die XML-Datei wieder in ein FileMaker-Feld laden (Aus URL einfügen).
Da ich mit horstoeko/zugferd arbeite, wird in der Folgeversion noch die Validierung erfolgen. Die derzeit händische, zeigt alle Werte, keine Fehler, keine Warnungen.

ZUGFeRD mit horstoeko/zugferd und FileMaker

Nach unzähligen Versuchen mit TCPDF, FPDI und verschiedensten Merge-Strategien, habe ich mich letztlich für einen pragmatischeren Weg entschieden: Ich lasse sowohl das PDF als auch das XML direkt auf dem Server erzeugen – ohne nachträglichen Merge. Die Lösung basiert auf dem PHP-Paket horstoeko/zugferd, welches sich nach einigen Stolpersteinen als zuverlässig herausgestellt hat – sobald man seine Eigenheiten akzeptiert.
Im ersten Szenario habe ich die fertige PDF aus FileMaker auf den Server, dort dann die Daten als POST-Parameter empfangen. Dann versucht die beiden Dateien, XML und PDF zu verschmelzen. Keine Change, bin fast verzweifelt, kenne die Doku zu Horstoeko/Zugferd aus dem FF. Aber es hat nicht geklappt. Also jetzt der pragmatische Ansatz. Die Daten werden übertragen und dann wird auf dem Server alles erzeugt.
Der FileMaker-Teil übergibt die notwendigen Daten per POST an ein PHP-Skript auf dem Server. Dieses Skript generiert daraus die ZUGFeRD-konforme XML, erzeugt gleichzeitig ein einfaches PDF mit den wichtigsten Rechnungsdaten (z. B. Rechnungstitel, Nummer etc.) – und bindet die XML direkt beim Erzeugen ein. Kein nachträgliches Anhängen mehr nötig. Kein Merge-Objekt, kein Zwischenschritt. Die erzeugte Datei ist PDF/A-3B und enthält die eingebettete Rechnung als XML. Dabei wird die PDF gleich im PHP etwas angepasst. Vermutlich wird das ganze noch etwas schicker mit css Implementierung, aber für den Anfang reicht es so.
Hier ist der komplette PHP-Code, den ich aktuell produktiv im Testsystem einsetze:
$_POST['sellerName'] ?? '',
'street' => $_POST['sellerStreet'] ?? '',
'zip' => $_POST['sellerPostalCode'] ?? '',
'city' => $_POST['sellerCity'] ?? '',
'country' => $_POST['sellerCountryCode'] ?? 'DE',
'tax_id' => $_POST['sellerTaxID'] ?? ''
];
// Rechnungsempfänger-Daten
$buyer = [
'name' => $_POST['buyerName'] ?? '',
'street' => $_POST['buyerStreet'] ?? '',
'zip' => $_POST['buyerPostalCode'] ?? '',
'city' => $_POST['buyerCity'] ?? '',
'country' => $_POST['buyerCountryCode'] ?? 'DE',
'tax_id' => $_POST['buyerTaxID'] ?? ''
];
// Zahlungsinformationen
$payment = [
'means_code' => $_POST['paymentMeansCode'] ?? '',
'financial_institution' => $_POST['payeeFinancialInstitution'] ?? '',
'iban' => $_POST['payeeIBAN'] ?? '',
'bic' => $_POST['payeeBIC'] ?? '',
'reference' => $_POST['paymentReference'] ?? ''
];
// Steuerinformationen
$tax = [
'rate' => floatval(str_replace(',', '.', $_POST['taxRate'] ?? '19')),
'amount' => floatval(str_replace(',', '.', $_POST['taxAmount'] ?? '0')),
'taxable_amount' => floatval(str_replace(',', '.', $_POST['taxableAmount'] ?? '0')),
'category_code' => $_POST['taxCategoryCode'] ?? 'S'
];
// Positionen aus lineItemsRaw extrahieren
$positions = [];
$lineItems = explode('|', $_POST['lineItemsRaw'] ?? '');
foreach ($lineItems as $lineItem) {
if (empty($lineItem)) continue;
$parts = explode(';', $lineItem);
if (count($parts) >= 7) {
$quantity = floatval($parts[3]);
$netPrice = floatval(str_replace(',', '.', $parts[5]));
$total = $quantity * $netPrice;
$orderDate = isset($parts[8]) ? date('d.m.Y', strtotime($parts[8])) : '';
$positions[] = [
'position' => $parts[0],
'description' => $parts[1],
'article_number' => $parts[2],
'quantity' => $quantity,
'unit' => $parts[4],
'net_price' => $netPrice,
'tax_rate' => floatval($parts[6]),
'total' => $total,
'order_date' => $orderDate
];
}
}
// Summen neu berechnen
$totalNet = 0;
$totalTax = 0;
foreach ($positions as $position) {
$totalNet += $position['total'];
$totalTax += $position['total'] * ($position['tax_rate'] / 100);
}
$totals = [
'net' => $totalNet,
'tax' => $totalTax,
'gross' => $totalNet + $totalTax
];
// Pfade definieren
$pdfPath = $uploadDir . $invoiceNumber . '.pdf';
$xmlPath = $uploadDir . 'inv_' . $invoiceNumber . '.xml';
$outputPath = $uploadDir . $invoiceNumber . '_ZUGFeRD.pdf';
// PDF-Datei überprüfen
if (!checkFile($pdfPath, "PDF-Datei")) {
throw new Exception("PDF-Datei nicht verfügbar");
}
// ZUGFeRD-Dokument erstellen
logMessage("Erstelle ZUGFeRD-Dokument...");
$document = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_BASIC);
logMessage("DocumentBuilder initialisiert");
// Basisinformationen setzen
logMessage("Setze Basisinformationen...");
$document->setDocumentInformation(
$invoiceTypeCode, // Dokumenttyp (380 = Rechnung)
$invoiceNumber, // Rechnungsnummer
new DateTime($invoiceDate), // Rechnungsdatum
$currency // Währung
);
logMessage("Basisinformationen gesetzt");
// Rechnungssteller setzen
logMessage("Setze Rechnungssteller...");
$document->setDocumentSeller(
$seller['name'], // Name
$seller['zip'], // PLZ
$seller['city'], // Stadt
$seller['street'], // Straße
$seller['country'] // Land
);
logMessage("Rechnungssteller gesetzt");
// Rechnungsempfänger setzen
logMessage("Setze Rechnungsempfänger...");
$document->setDocumentBuyer(
$buyer['name'], // Name
$buyer['zip'], // PLZ
$buyer['city'], // Stadt
$buyer['street'], // Straße
$buyer['country'] // Land
);
logMessage("Rechnungsempfänger gesetzt");
// Positionen hinzufügen
logMessage("Füge Positionen hinzu...");
foreach ($positions as $index => $position) {
$document->addNewPosition((string)($index + 1));
$document->setDocumentPositionProductDetails($position['description']);
$document->setDocumentPositionNetPrice($position['net_price']);
$document->setDocumentPositionQuantity($position['quantity'], $position['unit']);
$document->addDocumentPositionTax("S", "VAT", $position['tax_rate']);
$document->setDocumentPositionLineSummation($position['total']);
logMessage("Position {$position['description']} hinzugefügt");
}
// PDF mit Rechnungsdaten erstellen
logMessage("🔄 Erstelle PDF mit Rechnungsdaten...");
try {
$pdf = new FPDF();
$pdf->AddPage();
$pdf->SetAutoPageBreak(true, 20);
// Schriftarten definieren
$pdf->SetFont('Arial', '', 10);
// Absenderadresse
$pdf->SetXY(20, 20);
$pdf->Cell(0, 5, $seller['name'], 0, 1);
$pdf->Cell(0, 5, 'GmbH & Co. KG', 0, 1);
$pdf->Cell(0, 5, $seller['street'], 0, 1);
$pdf->Cell(0, 5, 'D-' . $seller['zip'] . ' ' . $seller['city'], 0, 1);
// Rechnungsinformationen rechts oben
$pdf->SetXY(120, 20);
$pdf->SetFont('Arial', '', 9);
// Rechte Spalte mit Informationen
$rightColumnData = [
['Nummer', $invoiceNumber],
['Datum', date('d.m.Y', strtotime($invoiceDate))],
['Kunden Nr.', $_POST['customerNumber'] ?? '16105'],
['Lieferschein', $_POST['deliveryNote'] ?? ''],
['Lief. Datum', date('d.m.Y', strtotime($_POST['deliveryDate'] ?? $invoiceDate))]
];
foreach ($rightColumnData as $row) {
$pdf->SetX(120);
$pdf->Cell(30, 5, $row[0], 0, 0);
$pdf->Cell(40, 5, $row[1], 0, 1);
}
// Überschrift "Rechnung"
$pdf->SetFont('Arial', '', 14);
$pdf->SetXY(20, 70);
$pdf->Cell(0, 10, 'Rechnung', 0, 1);
// Positionen Header
$pdf->SetFont('Arial', '', 8);
$pdf->SetFillColor(240, 240, 240);
$pdf->SetY($pdf->GetY() + 5);
// Spaltenbreiten
$colWidths = [
'auftrag' => 25,
'bestellung' => 35,
'kommission' => 35,
'artikel' => 25,
'bezeichnung' => 60,
'menge' => 20,
'preis' => 25,
'gesamt' => 25
];
// Positionen ausgeben
foreach ($positions as $index => $position) {
// Grauer Balken für Auftragskopf
$pdf->SetFillColor(240, 240, 240);
$y = $pdf->GetY();
// Auftragskopf mit Datum
$pdf->Cell($colWidths['auftrag'], 5, 'Auftrag: ' . $position['article_number'] . ' / ' . $position['order_date'], 0, 0, 'L', true);
$pdf->Cell($colWidths['bestellung'], 5, 'Ihre Bestellung: ' . $_POST['orderNumber'], 0, 0, 'L', true);
$pdf->Cell($colWidths['kommission'], 5, 'Ihre Kommission:', 0, 1, 'L', true);
// Positionsdetails
$pdf->SetFont('Arial', '', 8);
$pdf->Cell($colWidths['artikel'], 5, $position['article_number'], 0, 0);
$pdf->Cell($colWidths['bezeichnung'], 5, $position['description'], 0, 0);
$pdf->Cell($colWidths['menge'], 5, $position['quantity'] . ' ' . $position['unit'], 0, 0, 'R');
$pdf->Cell($colWidths['preis'], 5, number_format($position['net_price'], 2, ',', '.') . ' €', 0, 0, 'R');
$pdf->Cell($colWidths['gesamt'], 5, number_format($position['total'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->Ln(2);
}
// Summen am Ende
$pdf->SetY(-60);
$pdf->SetFont('Arial', '', 9);
// Zahlungsbedingungen
$pdf->Cell(0, 5, 'Rechnungsbetrag zahlbar bis ' . date('d.m.Y', strtotime($dueDate)), 0, 1);
$pdf->Cell(0, 5, 'Bei Zahlung bis ' . date('d.m.Y', strtotime($dueDate)) . ' Skonto 3 %', 0, 1);
// Summen rechtsbündig
$pdf->SetX(-80);
$pdf->Cell(30, 5, 'Summe netto', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['net'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->SetX(-80);
$pdf->Cell(30, 5, 'USt. ' . number_format($tax['rate'], 0) . ' %', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['tax'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->SetX(-80);
$pdf->SetFont('Arial', 'B', 9);
$pdf->Cell(30, 5, 'Rechnungsbetrag', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['gross'], 2, ',', '.') . ' €', 0, 1, 'R');
// Bestellnummer
$pdf->SetFont('Arial', '', 9);
$pdf->SetY(-30);
$pdf->Cell(0, 5, 'Zu Kommission: Bestell-Nr.: ' . ($_POST['orderReference'] ?? ''), 0, 1);
// Horizontale Linie am Ende
$pdf->SetY(-20);
$pdf->Line(20, $pdf->GetY(), 190, $pdf->GetY());
$pdf->Output('F', $outputPath);
logMessage("PDF erfolgreich generiert: $outputPath");
if (file_exists($outputPath)) {
logMessage("ZUGFeRD-PDF erfolgreich erstellt: " . basename($outputPath));
logMessage("Dateigröße: " . filesize($outputPath) . " Bytes");
} else {
throw new Exception("ZUGFeRD-PDF wurde nicht erstellt!");
}
} catch (Exception $e) {
logMessage(" Fehler beim PDF-Generieren: " . $e->getMessage());
logMessage(" Stack Trace:\n" . $e->getTraceAsString());
throw $e;
}
} catch (Exception $e) {
logMessage(" Fehler beim Erstellen der ZUGFeRD-PDF: " . $e->getMessage());
logMessage(" Stack Trace:\n" . $e->getTraceAsString());
// Debug-Informationen
logMessage("\nDebug-Informationen:");
logMessage("PHP Version: " . PHP_VERSION);
logMessage("Memory Limit: " . ini_get('memory_limit'));
logMessage("Max Execution Time: " . ini_get('max_execution_time'));
logMessage("Upload Directory Permissions: " . substr(sprintf('%o', fileperms($uploadDir)), -4));
// XML-Datei prüfen
if (file_exists($xmlPath)) {
logMessage("XML-Datei existiert: " . filesize($xmlPath) . " Bytes");
logMessage("XML-Inhalt (erste 200 Zeichen):\n" . substr(file_get_contents($xmlPath), 0, 200));
} else {
logMessage("XML-Datei existiert nicht");
}
}
Natürlich ist das so erst der Anfang, so fehlt die Überprüfung des fertigen PDF-A, aber bei händischen Tests, wurde alles akzeptiert.
Die Vorgehensweise aus FileMaker heraus ist klar. Daten sammeln und als POST übertragen. Der POST schaut in etwa so aus. Aus URL einfügen:
“-X POST " & “–header "Content-Type: application/x-www-form-urlencoded" " & “–data " & Zitat ( “invoiceNumber=” & $invoiceNumber & “&invoiceDate=” & $invoiceDate & “&invoiceCurrencyCode=” & $invoiceCurrencyCode & “&invoiceTypeCode=” & $invoiceTypeCode & “&dueDate=” & $dueDate & “&paymentTerms=” & $paymentTerms & “&deliveryTerms=” & $deliveryTerms &
“&sellerName=” & $sellerName & “&sellerStreet=” & $sellerStreet & “&sellerPostalCode=” & $sellerPostalCode & “&sellerCity=” & $sellerCity & “&sellerCountryCode=” & $sellerCountryCode & “&sellerTaxID=” & $sellerTaxID &
“&buyerName=” & $buyerName & “&buyerStreet=” & $buyerStreet & “&buyerPostalCode=” & $buyerPostalCode & “&buyerCity=” & $buyerCity & “&buyerCountryCode=” & $buyerCountryCode & “&buyerTaxID=” & $buyerTaxID &
“&paymentMeansCode=” & $paymentMeansCode & “&payeeFinancialInstitution=” & $payeeFinancialInstitution & “&payeeIBAN=” & $payeeIBAN & “&payeeBIC=” & $payeeBIC & “&paymentReference=” & $paymentReference &
“&taxRate=” & $taxRate & “&taxAmount=” & $taxAmount & “&taxableAmount=” & $taxableAmount & “&taxCategoryCode=” & $taxCategoryCode &
“&totalNetAmount=” & $totalNetAmount & “&totalTaxAmount=” & $totalTaxAmount & “&totalGrossAmount=” & $totalGrossAmount & “&lineItemsRaw=” & $lineItemsRaw
)
Nun geht es darum, die Form der Rechnung anzupassen, die Rechnung schon auf dem System zu validieren. Im Anschluss muss diese wieder im FileMaker-System zugänglich sein. Was ich aber jetzt schon sagen kann, es geht auch ohne MBS-Plugin und ohne die dort auflaufenden Lizenz-Kosten. Der Weg bis zu diesem Punkt war wirklich sehr steinig, vieles funktionierte nicht so wie dokumentiert. Jetzt bin ich froh,
Sequentielle Suche mit FileMaker und PHP
Manchmal sind es nicht die großen Frameworks oder hochkomplexen API-Integrationen, die den Unterschied machen, sondern ein bewusst einfacher, robuster Ansatz, der sich nahtlos in vorhandene Workflows integriert. Genau das war der Ausgangspunkt für eine kleine aber wirkungsvolle Lösung, die FileMaker mit einem PHP-basierten Webinterface verbindet – mit dem Ziel, eine sequentielle Suche über eine größere Datenmenge performant und flexibel umzusetzen.
Das derzeitige Problem beim Kunden, vor Jahren habe ich eine Sequentielle Suche nativ in FileMaker umgesetzt. Die übliche Vorgehensweise, ein Script sucht bei jedem Tastenanschlag, genutzt werden alle Felder die in der Schnellsuche eingeschlossen sind. Über die Jahre wuchs der Datenbestand, es wurden Felder innerhalb von Ausschnitten erfasst. Es musste so kommen, der Kunde konnte die Suche kaum mehr nutzen, tippen, warten, tippen. Sehr unschön und langsam.
Dann kam mir im Zuge einer Listenansicht, in die ich eine Suche integriert habe, die Idee. FileMaker überträgt per -Aus URL einfügen- Tabellendaten an ein PHP-Script. Im WebViewer wird dann die Tabelle angezeigt, nebst einem Suchfeld. Also frisch ans Werk gemacht und schnell festgestellt, die Felder in Variablen zu verpacken, macht in der Menge keinen Spaß. Also das ganze per Schleife, Feldnamen holen, die Werte dazu sammeln. In Schleife, ist das gut machbar, aber trotzdem ein nicht unerheblicher Zeitaufwand. Dann eine Eingebung, exportiere die Tabelle und verarbeite die Daten direkt auf dem Server per PHP.

Vorgehen: FileMaker exportiert eine strukturierte Datenmenge als CSV – diese Datei wird im Hintergrund automatisch an ein kleines PHP-Script übermittelt. Dort wird sie interpretiert, analysiert und in einer visuellen Oberfläche dargestellt, über die eine freie Volltextsuche möglich ist. Der Clou: Mit jedem Tastendruck wird die Ergebnisliste dynamisch reduziert – und bei Bedarf lassen sich über die Enter-Taste direkt Projektnummern an ein FileMaker-Script zurückgeben, das dann wiederum die interne Detailansicht aktualisiert. Ganz ohne Datenbankabfragen im Webserver, ganz ohne MySQL, Redis oder externe Services.
Die PHP-Logik bleibt dabei angenehm überschaubar. Ein Beispiel für das Parsen und Darstellen der Daten sieht so aus:
<?php $csvData = []; $data = file_get_contents("php://input"); if ($data && strlen($data) > 0) { $lines = preg_split('/\r\n|\r|\n/', $data); foreach ($lines as $line) { if (trim($line) !== "") { $csvData[] = str_getcsv($line, "\t"); // Tab-getrennt } } if (!empty($csvData) && empty(array_filter(end($csvData)))) { array_pop($csvData); } } $spaltenIndizes = range(0, count($csvData[0] ?? []) - 1); ?>
In der Darstellung im WebViewer werden alle Datensätze tabellarisch angezeigt. Der Clou kommt mit JavaScript: Dort wird bei jeder Eingabe automatisch geprüft, welche Zeilen noch zum aktuellen Suchbegriff passen. Zusätzlich hebt ein kleiner Style-Block die passenden Zellen farblich hervor, um die Treffer visuell zu unterstützen. Und weil alles clientseitig passiert, bleibt es schnell – auch bei mehreren tausend Einträgen.
Besonders elegant wirkt die Integration in FileMaker: Die Projektnummern der sichtbaren Zeilen werden bei einem Enter-Klick gesammelt und per fmp://-URL an ein FileMaker-Script übergeben. Diese Direktverbindung ermöglicht, das Webinterface wie eine native Erweiterung der Datenbank zu nutzen – ohne Performanceverlust, ohne Redundanz, ohne Hürden.
document.getElementById("searchInput").addEventListener("keypress", function(event) { if (event.key === "Enter") { event.preventDefault(); const rows = document.querySelectorAll("#csvTable tbody tr:not(.hide)"); const ids = []; rows.forEach(row => { const id = row.querySelectorAll("td")[0]?.textContent.trim(); // Erste Spalte = ID if (id) ids.push(id); }); if (ids.length > 0) { const param = encodeURIComponent(ids.join("|")); const url = `fmp://$/AVAGmbH?script=Projekt_LIST_Suche_PHP¶m=${param}`; window.location.href = url; } } });
Nach dem Klick, startet das FM-Script. Wir holen uns die ID,s nach üblicher Vorgangsweise und suchen in Schleife alle ID,s zusammen. In dem Zug, wird natürlich auch das Suchfenster in FileMaker geschlossen.

Diese Form der sequentiellen Suche hat sich im Test als stabil und pflegeleicht erwiesen – gerade in Szenarien, in denen FileMaker allein bei umfangreichen Datensätzen an die Grenzen kommt, etwa bei mehrdimensionalen Suchen über unstrukturierte Felder oder bei extern generierten Listen.
Und auch wenn es kein High-End-AI-Suchcluster ist: Die Lösung hat Charme. Weil sie genau das tut, was sie soll. Weil sie den Workflow nicht verbiegt, sondern erweitert. Und weil sie etwas bietet, das man oft zu selten hat: unmittelbare Rückmeldung und Kontrolle über den gesamten Prozess.
Jetzt wird nur noch ein wenig mit CSS das ganze verschönt, dann kann der Kunde damit arbeiten.
Große Datenmengen performant aus FileMaker übertragen und anzeigen – ohne Wartezeit

In FileMaker große Datenmengen zu verarbeiten, kann schnell zu Performance-Problemen führen. Besonders wenn FileMaker noch im Hintergrund sortiert oder aggregiert, kann die Benutzeroberfläche einfrieren oder es dauert mehrere Sekunden, bis die Daten sichtbar sind. Das Problem stellt sich immer wieder bei Altlösungen die 10 oder 20 Jahre alt sind. Häufig sind diese Datenbanken bis ins kleinste an die Kunden-Prozesse angepasst. Ein Neubau unrealistisch bis unbezahlbar. Performance-Probleme nur in den langen Listenansichten mit Unmengen an Datensätzen, diese dann noch mit Sortierungen und Formelfeldern.
Doch es gibt eine Möglichkeit, Daten sofort anzuzeigen, auch wenn FileMaker noch weiterarbeitet: Daten per POST an einen WebViewer übergeben und dort asynchron anzeigen.
Warum nicht einfach FileMaker-Listen?
Standardmäßig lädt FileMaker Listenansichten synchron – das heißt, es wartet, bis alle Datensätze verarbeitet sind. Das führt zu Problemen, wenn: • Tausende Datensätze geladen werden, • FileMaker noch sortiert, • eine Suche viele Treffer hat, • Zusatzinformationen aus mehreren Tabellen geladen werden müssen.
Die Lösung: Daten mit -X POST effizient an eine externe Webanwendung übergeben und dort direkt rendern – unabhängig davon, ob FileMaker noch weiter rechnet.
Datenübergabe aus FileMaker: So geht’s richtig
Wir nutzen den Insert from URL-Befehl, um die Daten per POST an eine Webanwendung zu übergeben. Hierbei setzen wir auf application/x-www-form-urlencoded, da diese Methode stabil mit großen Datenmengen arbeitet.
Set Variable [ $url ; Value: "http://meinserver.de/daten.php" ] Set Variable [ $payload ; Value: "projects=" & $id_projects & "&staff=" & $id_staff & "&anlage=" & $anlage & "&status=" & $status & "&task=" & $id_task & "&image=" & $image & "&inhalt=" & $inhalt & "&historyColors=" & $historyColors & "&planungsmonat=" & $planungsmonat & "&date_von=" & $date_von & "&date_bis=" & $date_bis & "&zeit_von=" & $zeit_von & "&zeit_bis=" & $zeit_bis & "&anzahl_staff=" & $anzahl_staff & "&staff2=" & $id_staff_2 & "&staff3=" & $id_staff_3 & "&erstellt=" & $erstellt & "&prio=" & $prio & "¬es=" & $notes ] Insert from URL [ $url ; "-X POST " & "--header \"Content-Type: application/x-www-form-urlencoded\" " & "--data " & Zitat ( $payload ) ]
Der Vorteil, ich kann unabhängig von URL-Begrenzungen Daten übergeben, ich erspare mir den Aufbau komplexer JSON-Strukturen innerhalb von FileMaker. Datentrennung geschieht bei mir vorzugsweise mit einem Pipe. Es ist natürlich auch anderes möglich. Datensammlung innerhalb von FileMaker über Schleifen, das verspricht bessere Kontrolle oder wenn es mal schnell gehen soll, geht natürlich auch die List-Funktion. Das ist aber Geschmacksache.
Unser PHP-Script empfängt die Daten und kann mit der Verarbeitung beginnen. Sobald die Daten per POST
an PHP gesendet wurden, werden sie in einzelne Arrays zerlegt, sodass sie flexibel weiterverarbeitet werden können. Durch das entdecken von Strings anhand eines Trennzeichens (z. B. |
) lassen sich in einer einzigen Anfrage übertragen:
$projects = isset($_POST['projects']) ? explode('|', $_POST['projects']) : []; $staff = isset($_POST['staff']) ? explode('|', $_POST['staff']) : []; $anlage = isset($_POST['anlage']) ? explode('|', $_POST['anlage']) : []; $status = isset($_POST['status']) ? explode('|', $_POST['status']) : []; $task = isset($_POST['task']) ? explode('|', $_POST['task']) : []; $image = isset($_POST['image']) ? explode('|', $_POST['image']) : []; $inhalt = isset($_POST['inhalt']) ? explode('|', $_POST['inhalt']) : []; $planungsmonat = isset($_POST['planungsmonat']) ? explode('|', $_POST['planungsmonat']) : [];
Die Daten aus PHP werden dann über eine Schleife in HTML überführt. In diesem Beispiel erzeugen wir eine dynamische Tabelle, die aus den übergebenen Daten generiert wird:
<table border="1"> <tr> <th>Projekt</th> <th>Aufgabe</th> <th>Mitarbeiter</th> <th>Status</th> </tr> <?php foreach ($task as $index => $taskName): ?> <tr> <td><?= htmlspecialchars($projects[$index] ?? 'Unbekannt') ?></td> <td><?= htmlspecialchars($taskName) ?></td> <td><?= htmlspecialchars($staff[$index] ?? 'Kein Mitarbeiter') ?></td> <td><?= htmlspecialchars($status[$index] ?? 'Unbekannt') ?></td> </tr> <?php endforeach; ?> </table>
Mit ein wenig CSS kann die Liste angepasst werden, Zwischesortierungen, Statusfarben, nichts was nicht möglich ist.
.task-list { width: calc(100% - 20px); /* Verhindert das Überlaufen nach rechts / max-width: 100%; margin: 10px auto; background: white; padding: 10px; border-radius: 5px; box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); font-size: 14px; overflow-x: hidden; / Falls nötig, um Überlauf zu vermeiden */ }
.task-group {
margin-top: 15px;
padding: 8px;
background: #3773B5;
font-size: 14px;
font-weight: bold;
color: white;
border-radius: 5px;
text-align: left;
}
.task-item {
width: 98%; /* Damit es nicht über den Container hinausragt */
max-width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 10px;
border-radius: 3px;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
font-size: 13px;
line-height: 1.2;
background-color: white;
flex-wrap: wrap;
box-sizing: border-box; /* Sorgt dafür, dass Padding berücksichtigt wird */
}
Eine sequentielle Suche, die in ihrer Schnelligkeit niemals in FileMaker abzubilden ist, runden die Tabelle ab.
<script> document.getElementById("taskSearch").addEventListener("keyup", function() { let searchQuery = this.value.toLowerCase(); let taskGroups = document.querySelectorAll(".task-group-container"); let resultCount = 0; // Zähler für die Anzahl der gefundenen Datensätze taskGroups.forEach(group => { let hasMatch = false; let tasks = group.querySelectorAll(".task-item"); tasks.forEach(task => { let text = task.innerText.toLowerCase(); // Gesamten Inhalt durchsuchen if (text.includes(searchQuery)) { task.style.display = ""; // Zeigen hasMatch = true; resultCount++; // Treffer zählen } else { task.style.display = "none"; // Verstecken } }); // Gruppe ausblenden, wenn keine Aufgabe übrig ist group.style.display = hasMatch ? "" : "none"; }); // Anzeige der Trefferanzahl aktualisieren document.getElementById("searchResultsCount").textContent = resultCount + " Ergebnisse gefunden"; }); </script>
Natürlich darf die Möglichkeit aus dem WebViewer heraus mit FileMaker zu kommunizieren nicht fehlen. In diesem Fall noch mit einer Bedingung.
document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll(".status-dropdown").forEach(function (dropdown) { dropdown.addEventListener("change", function () { let taskId = this.getAttribute("data-task-id"); let newStatus = this.value; if (newStatus === "Angebot folgt") { // PopOver anzeigen let modal = document.getElementById("offerModal"); modal.style.display = "flex"; // Speichern-Button im PopOver document.getElementById("confirmOffer").onclick = function () { let priority = document.getElementById("offerPriority").value; let note = document.getElementById("offerNote").value.trim(); if (note === "") { alert(" Bitte eine Notiz eingeben!"); return; } modal.style.display = "none"; // Fenster schließen // FileMaker-Skript aufrufen mit Priorität & Notiz let fileMakerScriptURL = "fmp://$/AVAGmbH?script=UpdateTaskStatus¶m=" + encodeURIComponent(taskId + "|" + newStatus + "|" + priority + "|" + note); console.log(" FileMaker-Skript aufrufen:", fileMakerScriptURL); window.location = fileMakerScriptURL; }; // Abbrechen-Button im PopOver document.getElementById("cancelOffer").onclick = function () { modal.style.display = "none"; // Fenster schließen dropdown.value = "In Planung"; // Status zurücksetzen }; return; // Stoppe die normale Ausführung, solange PopOver offen ist } // Falls NICHT "Angebot folgt", normales Verhalten let fileMakerScriptURL = "fmp://$/AVAGmbH?script=UpdateTaskStatus¶m=" + encodeURIComponent(taskId + "|" + newStatus); console.log("FileMaker-Skript aufrufen:", fileMakerScriptURL); window.location = fileMakerScriptURL; // Seite nach kurzer Verzögerung aktualisieren setTimeout(function () { window.location.reload(); }, 1000); }); }); });
Es gibt unendliche Optimierungsmöglichkeiten. Das wichtigste ist aber, wir können mit recht wenig Aufwand, alte schwergewichtige FileMaker-Anwendungen wieder flott machen.
Teil Zwei der Kanban Entwicklung in FileMaker, Umstellung von GET auf POST
Die Evolution einer Kanban-Ansicht in FileMaker: Vom GET zum POST
In der kontinuierlichen Weiterentwicklung von Arbeitsprozessen und deren Darstellung in FileMaker ergibt sich oft die Notwendigkeit, bestehende Lösungen zu optimieren und an neue Anforderungen anzupassen. Genau dieser Gedanke begleitete die Entwicklung der Kanban-Ansicht, die sich ursprünglich auf GET-Parameter zur Datenübertragung stützte, nun aber auf POST umgestellt wurde. Ein scheinbar kleiner Wechsel, der jedoch große Auswirkungen hat – sowohl auf die Performance als auch auf die Flexibilität der Anwendung. Der wichtigste Aspekt, alle weiteren Daten die der Kunde jetzt in noch in den Kacheln sehen möchte, kann ich ohne Probleme übertragen. Ob 100 oder nur 50 Aufgaben. Das System benötigt keinerlei Vorfilterung innerhalb von FileMaker.
Ursprünglich wurde die Kanban-Ansicht über eine URL-basierte GET-Abfrage mit Daten versorgt. Die Vorteile lagen auf der Hand: einfache Implementierung, transparente Debugging-Möglichkeiten und eine schnelle Integration in den bestehenden Webviewer von FileMaker. Doch mit wachsendem Datenvolumen und steigenden Anforderungen an die übertragene Information stieß diese Methode an ihre Grenzen. Die maximale URL-Länge wurde zu einem Problem, und die Strukturierung komplexer Daten – insbesondere in Bezug auf Statusfarben, Aufträge und historische Änderungen – wurde zunehmend unübersichtlich.
Die logische Konsequenz war der Wechsel auf eine POST-Übertragung, die nicht nur größere Datenmengen bewältigen kann, sondern auch eine flexiblere Handhabung ermöglicht. Dabei stellte sich heraus, dass die Umstellung zwar in der Theorie simpel erschien, in der Praxis aber einige Herausforderungen mit sich brachte. FileMaker musste so konfiguriert werden, dass die Daten nicht mehr direkt als URL-Parameter übergeben wurden, sondern als strukturierte POST-Daten in die Webanwendung flossen. Das bedeutete, dass die gesamte Datenkette angepasst werden musste – von der Generierung der Werte in FileMaker über die Verarbeitung in PHP bis hin zur Darstellung im Webviewer.
Nach einigen Anpassungen und Tests zeigte sich schnell: Die POST-Methode brachte nicht nur eine sauberere und robustere Datenübertragung mit sich, sondern auch eine spürbare Verbesserung der Ladegeschwindigkeit. Während GET oft dazu führte, dass die URL unnötig aufgebläht wurde, ermöglicht POST eine strukturierte und sichere Übermittlung, die auch größere Datensätze problemlos verarbeitet. Gerade im Kontext eines Kanban-Boards mit zahlreichen Aufgaben, Statusänderungen und Nutzerinteraktionen ist das ein entscheidender Vorteil.
Ein besonders interessanter Nebeneffekt der Umstellung war die Vereinfachung der Datenstruktur. Während bei der GET-Variante viele Variablen direkt in der URL codiert und dekodiert werden mussten, erlaubt die POST-Methode eine klarere Trennung zwischen Client und Server. Die Farbmarkierungen, Statushistorien und Task-IDs ließen sich nun effizienter verarbeiten, und die Fehlersuche gestaltete sich deutlich angenehmer. Zudem eröffnete sich durch die Umstellung die Möglichkeit, noch weitere Daten ohne zusätzliche URL-Komplikationen zu übertragen – ein Schritt, der langfristig gesehen weitere Optimierungen ermöglichen wird.
Rückblickend zeigt diese Anpassung einmal mehr, wie wichtig es ist, Prozesse stetig zu hinterfragen und weiterzuentwickeln. Was gestern noch gut funktionierte, kann heute durch eine andere Herangehensweise erheblich verbessert werden. Die Migration von GET zu POST mag auf den ersten Blick nur eine technische Feinheit sein, doch in der Praxis trägt sie entscheidend zu einer performanten und skalierbaren Lösung bei. Die Kanban-Ansicht in FileMaker ist damit nicht nur flexibler, sondern auch zukunftssicherer geworden – und das ist letztlich das Ziel jeder guten Softwareentwicklung.
Der eigentliche Vorgang innerhalb von FileMaker ist klar. Sammeln der Daten per Schleife unter Beachtung der spezifischen Vorstellungen und Kundenwünsche. Das eigentlich interessante Prozedere, der Befehl aus URL einfügen.




Kanban mit FileMaker und PHP

Die Verwaltung von Aufgaben kann schnell unübersichtlich werden, wenn viele Prozesse gleichzeitig laufen und verschiedene Mitarbeiter involviert sind. Eine klassische Methode zur Visualisierung solcher Abläufe ist die Kanban-Tafel, die durch ihre intuitive Struktur einen schnellen Überblick über den Status der einzelnen Aufgaben bietet. Doch während viele Tools auf dem Markt existieren, soll in diesem Artikel eine maßgeschneiderte Lösung direkt in FileMaker realisiert werden – mit Hilfe von PHP und JavaScript in einem Webviewer. Es gibt natürlich eine FileMaker-Eigene Implementierung über die Add-ons. Mir persönlich ist diese Möglichkeit aber zu eingeschränkt. So sind die Tafeln nicht Dynamisch, ich kann z.B. keine Symbole oder Bilder mit integrieren und die Steuerung der Abläufe ist nicht richtig konsistent.
Deshalb die Idee, ich setze das einfach mit PHP, Java-Script über den WebViewer von FileMaker um. Das Ziel ist es, eine interaktive Kanban-Tafel direkt in FileMaker zu integrieren, die alle relevanten Aufgaben übersichtlich darstellt und es ermöglicht, diese per Drag & Drop zwischen den verschiedenen Status-Spalten zu verschieben. Diese Statusänderungen sollen dann automatisch an FileMaker zurückgemeldet werden, sodass keine doppelte Pflege von Daten notwendig ist. Die Techniken dahinter: -PHP als Schnittstelle zur Verarbeitung und Bereitstellung der Daten. -JavaScript mit jQuery UI für die interaktive Drag & Drop-Funktionalität. -FileMaker-Skripte, um die Änderungen zurück in die FileMaker-Datenbank zu schreiben.
Als erstes müssen wir uns klar werden wie die Daten an das PHP-Script übergeben werden. Ich persönlich nutze gern die Möglichkeit Einträge mit einem Pipe getrennt zu übertragen. Das lässt sich in FileMaker wunderbar über eine Schleife realisieren. In meinem Beispiel werden verschiedene ID,s übertragen.
Damit die Kanban-Ansicht ihre Inhalte aus FileMaker beziehen kann, müssen die relevanten Daten über eine URL in PHP übergeben werden. Dazu werden die folgenden Parameter als GET-Request an eine PHP-Datei gesendet:
projects: Enthält die Auftragsnummern. staff: Enthält die zugeordneten Mitarbeiter. anlage: Enthält die Referenz zur Anlage. status: Gibt den aktuellen Status der Aufgabe an (z. B. “In Planung” oder “Erledigt”). task: Enthält die eindeutige Task-ID (Aufgaben ID) zur Identifikation. image: Gibt den Namen des Symbols an, das in der Kanban-Karte angezeigt werden soll.
Meine fertige URL schaut ungefähr so aus: https://maeine_url_.de/cap/kanban.php?projects=24944|24945|24946&staff=STA020|STA023|STA025&anlage=B14C380C-4329-E54B-ACA1-B2054C5D4B3A|B14C380C-4329-E54B-ACA1-B2054C5D4B3A&status=Erledigt|In Planung|Material bestellt&task=TSK004729|TSK004730|TSK004731&image=Blau|Gelb|Rot
Der Weg diese zu generieren sollte klar sein. Per Schleife durch die Aufgaben, entweder gefiltert nach Datum oder Zeitraum etc. Nun wird dieser Wert in eine Globale Variable geschrieben z.B. $$URL. Dann der FileMaker Befehl, aus URL einfügen. Dabei generieren wir eine Variable z.B. $$URL_WEBVIEWER. Diese kommt dann als Inhalt in unseren Webviewer den wir uns innerhalb eines FileMaker-Layouts platzieren.
Unser Script:
<?php
// Header für UTF-8 Encoding
header("Content-Type: text/html; charset=UTF-8");
// Daten aus der URL holen
$projects = isset($_GET['projects']) ? explode('|', $_GET['projects']) : [];
$staff = isset($_GET['staff']) ? explode('|', $_GET['staff']) : [];
$anlage = isset($_GET['anlage']) ? explode('|', $_GET['anlage']) : [];
$status = isset($_GET['status']) ? explode('|', $_GET['status']) : [];
$task = isset($_GET['task']) ? explode('|', $_GET['task']) : [];
$images = isset($_GET['image']) ? explode('|', $_GET['image']) : [];
// Basis-URL für die Icons
$imageBaseUrl = "http://Deine_URL.de/GoogleMarkers/";
// Status und Farben definieren
$status_colors = [
"Material bestellt" => "#FFD700", // Gelb
"In Planung" => "#FFA500", // Orange
"Verplant" => "#FF4500", // Dunkelorange
"Ausgeführt" => "#008000", // Grün
"Angebot folgt" => "#FF0000", // Rot
"Erledigt" => "#808080" // Grau
];
// Spalten für das Kanban-Board
$columns = ["Material bestellt", "In Planung", "Verplant", "Ausgeführt", "Angebot folgt", "Erledigt"];
// Aufgaben nach Status sortieren
$tasks_by_status = [];
foreach ($status as $index => $task_status) {
$tasks_by_status[$task_status][] = [
"id" => $task[$index] ?? "TSK-Unbekannt",
"project" => $projects[$index] ?? "Unbekannt",
"staff" => $staff[$index] ?? "Kein Mitarbeiter",
"anlage" => $anlage[$index] ?? "Keine Anlage",
"image" => isset($images[$index]) && !empty($images[$index]) ? $imageBaseUrl . $images[$index] . ".png" : "",
"status" => $task_status
];
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Aufgaben Board</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
.kanban-board {
display: flex;
justify-content: space-between;
padding: 20px;
gap: 10px;
}
.kanban-column {
width: 16%;
min-height: 400px;
background: #ddd;
padding: 10px;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
}
.kanban-card {
background: white;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
cursor: grab;
width: 90%;
text-align: center;
transition: background-color 0.3s;
position: relative;
}
.kanban-card img {
position: absolute;
top: 5px;
left: 5px;
width: 20px;
height: 20px;
}
.sortable {
width: 100%;
min-height: 50px;
}
.sortable-placeholder {
background: #ccc;
height: 50px;
border-radius: 5px;
margin-bottom: 10px;
width: 90%;
}
</style>
</head>
<body>
<h2 style="text-align: center;">Aufgaben Board</h2>
<div class="kanban-board">
<?php foreach ($columns as $column): ?>
<div class="kanban-column" id="<?= htmlspecialchars($column) ?>">
<h3><?= htmlspecialchars($column) ?></h3>
<div class="sortable" id="sortable-<?= htmlspecialchars($column) ?>">
<?php if (!empty($tasks_by_status[$column])): ?>
<?php foreach ($tasks_by_status[$column] as $task): ?>
<div class="kanban-card"
id="<?= htmlspecialchars($task['id']) ?>"
data-task-id="<?= htmlspecialchars($task['id']) ?>"
data-status="<?= htmlspecialchars($task['status']) ?>"
style="background-color: <?= $status_colors[$column] ?? '#ffffff' ?>;">
<?php if (!empty($task['image'])): ?>
<img src="<?= $task['image'] ?>" alt="Symbol">
<?php endif; ?>
<strong>Auftrag:</strong> <?= htmlspecialchars($task["project"]) ?><br>
<strong>Anlage:</strong> <?= htmlspecialchars($task["anlage"]) ?><br>
<strong>Mitarbeiter:</strong> <?= htmlspecialchars($task["staff"]) ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<script>
// Status und zugehörige Farben
var statusColors = {
"Material bestellt": "#FFD700", // Gelb
"In Planung": "#FFA500", // Orange
"Verplant": "#FF4500", // Dunkelorange
"Ausgeführt": "#008000", // Grün
"Angebot folgt": "#FF0000", // Rot
"Erledigt": "#808080" // Grau
};
$(document).ready(function() {
console.log(" jQuery und jQuery-UI wurden geladen.");
$(".sortable").sortable({
connectWith: ".sortable",
placeholder: "sortable-placeholder",
revert: true,
start: function(event, ui) {
console.log("Drag gestartet für:", ui.item.attr("id"));
},
stop: function(event, ui) {
var newStatus = ui.item.closest(".kanban-column").attr("id");
var taskId = ui.item.attr("id");
console.log("Aufgabe verschoben:", taskId, "Neuer Status:", newStatus);
// Neue Farbe setzen
if (statusColors[newStatus]) {
ui.item.css("background-color", statusColors[newStatus]);
}
// FileMaker-Skript über fmp:// URL aufrufen
var fileMakerScriptURL = "fmp://$/Deine_Datei?script=UpdateTaskStatus¶m=" + encodeURIComponent(taskId + "|" + newStatus);
console.log(" FileMaker-Skript aufrufen:", fileMakerScriptURL);
// WebViewer öffnet FileMaker-Skript
window.location = fileMakerScriptURL;
}
}).disableSelection();
});
</script>
</body>
</html>
Die Updates der Änderungen werden über ein kleines weiteres PHP Script abgefangen.
<?php
// POST-Daten empfangen
$taskId = $_POST['id'] ?? null;
$newStatus = $_POST['status'] ?? null;
if ($taskId && $newStatus) {
// Hier würdest du die Daten in FileMaker oder einer Datenbank speichern
file_put_contents("kanban_log.txt", "ID: $taskId -> $newStatus\n", FILE_APPEND);
echo "OK";
} else {
echo "Fehler: Ungültige Daten";
}
?>
Führe ich nun eine Änderung durch, wird das FileMaker-Script -UpdateTaskStatus- aufgerufen, es übergibt als Parameter die Task-ID und den neuen Status. Damit habe ich natürlich die Möglichkeit im Hintergrund sofort die Änderungen zu verarbeiten. Also, neues Fenster, 1x1 px, bei -30000px. Dort suche ich die Task-ID und ändere den Status. Fenster schließen und der Vorgang ist abgeschlossen.
Dynamische Listenanzeige in FileMaker Web Viewern: Automatische Schriftgrößen- und Zeilenhöhenanpassung
FileMaker bietet mit Portalen eine gute Möglichkeit, Listen anzuzeigen. Doch wenn der Platz begrenzt ist, stößt man schnell an Grenzen. Ein Web Viewer bietet hier eine flexible Lösung, um Inhalte dynamisch darzustellen – inklusive automatischer Anpassung der Schriftgröße und Zeilenhöhe! Hintergrund, ein Kunde hat Formulare die mit Nadeldrucker geschrieben werden. Es ist also nicht möglich die Struktur des Ausdruckes zu ändern. Alles muss auf diese Seite. Bisher war es kein Problem, es gab für bestimmte Vorgänge einen Ansprechpartner mit Telefonnummer. Nun können es aber auch 2 oder drei Ansprechpartner sein. Was nun? Dann muss die Schriftart dynamisch kleiner werden. Das ganze geht auch mit FileMaker, aber wie bekomme ich dann das Portal dynamisch kleiner. Ich weiss nicht. Also bin ich auf die Idee gekommen, die Anzeige über den WebViewer laufen zu lassen.
Dabei ist es ein ganz einfacher Prozess: 1.bauen einer Liste in einer Variablen, die Einträge Namen und Telefon 1 und Telefon zwei werden ganz einfach über einen Pipe getrennt.
-
Wir legen unds ein WebViewer in das Drucklayout, benannt z.B. -Telefonliste- , als URL bzw. Inhalt eine globale Variable, z.B. $$HTML.
-
Wir gehen wieder in unser Script, legen eine Variable an -$$HTML- die se hat folgenden Inhalt:
Set Variable [ $$html ; "data:text/html," & "" & Char(10) & "" & Char(10) & "" & Char(10) & " " & Char(10) & " " & Char(10) &“” & Char(10) & “
” & Char(10) &“
” & Substitute ( $$namen_liste ; “¶” ; Char(10) ) &” & Char(10) &
““” & Char(10) & “” ]
Wenn jetzt das Script ausgeführt wird, haben wir dynamisch die Größe der Schriftart und der Zeilenabstände. Wenn es noch nicht passt, muss nur an dieser Stelle geändert werden. Für mich eine kleine perfekte Lösung um das Platzproblem zu lösen.
Geodaten in FileMaker integrieren – Einfacher geht’s kaum
Häufig sprechen wir über Geodaten – doch wie bekommen wir diese eigentlich in unsere FileMaker-Datenbank? In vielen meiner kurzen Anrisse, geht es immer wieder um Geo-Daten. Wir übertragen sie in Richtung Google-Maps, in verschiedensten Formen. Nur wir benötigen diese im ersten Schritt. Wenn Sie eine Adresse in Latitude (Breitengrad) und Longitude (Längengrad) umwandeln möchten, bietet sich die Nutzung der Google Geocoding API an. Aber seien wir ehrlich: Niemand hat Lust, sich mit komplexen JSON-Daten oder unnötigem Ballast herumzuschlagen. Darum haben wir uns eine besonders „faule“, aber effektive Lösung überlegt: Wir lassen uns einfach nur die beiden benötigten Geodaten ausgeben – fertig. Ich erhalte eine Variable mit zwei Elementen. Diese kann ich dann in FileMaker mit zwei Scriptschritten weiterverarbeiten.
Warum Geodaten in FileMaker nutzen?
Geodaten können in einer Datenbank unglaublich vielseitig eingesetzt werden: • Visualisierung: Stellen Sie Adressen als Marker in Google Maps dar. • Navigation: Berechnen Sie Routen und Entfernungen. • Datenanalyse: Prüfen Sie, welche Kunden sich in einem bestimmten Radius befinden.
Die Herausforderung? FileMaker arbeitet am besten mit einfachen, strukturierten Daten. Darum konzentrieren wir uns auf die Essenz: Breitengrad und Längengrad.
Die „faule“ Lösung: Minimalistische Datenabfrage
Statt alle Details wie die formattierte Adresse oder die Geometrie-Typen der Google Geocoding API zu verarbeiten, nutzen wir ein PHP-Skript, das uns genau die zwei benötigten Werte zurückgibt: Latitude und Longitude, getrennt durch einen Zeilenumbruch. So lassen sich die Daten in FileMaker besonders einfach integrieren.
Mein Beispiel:
< ?php
// API-Key hier einfügen
$apiKey = "hier der Goggle API-Key";
// Parameter prüfen
if (isset($_GET['params'])) {
$params = explode('|', $_GET['params']); // Erwartet: Straße|PLZ|Stadt
if (count($params) >= 3) {
$street = trim($params[0]); // Straße und Hausnummer
$city = trim($params[2]); // Stadt
$zip = trim($params[1]); // Postleitzahl
$country = isset($params[3]) ? trim($params[3]) : "DE"; // Standard: Deutschland
// Adresse zusammenstellen
$addressComponents = array_filter([$street, $city, $zip, $country]); // Entfernt leere Werte
$address = implode(',', $addressComponents);
$url = "[maps.googleapis.com/maps/api/...](https://maps.googleapis.com/maps/api/geocode/json?address=)" . urlencode($address) . "&key={$apiKey}";
// API-Anfrage senden
$response = file_get_contents($url);
// Prüfen, ob die Antwort erfolgreich war
if ($response === false) {
echo "Fehler beim Abrufen der API\n";
exit;
}
// API-Antwort decodieren
$data = json_decode($response, true);
// Ergebnisse prüfen
if ($data['status'] === "OK" && isset($data['results'][0]['geometry']['location'])) {
$location = $data['results'][0]['geometry']['location'];
// Nur Latitude und Longitude ausgeben, getrennt durch Zeilenumbruch
echo $location['lat'] . "\n" . $location['lng'];
} else {
echo "Keine Geodaten gefunden\n";
}
} else {
echo "Ungültiges Format. Erwartet: Straße|PLZ|Stadt\n";
}
} else {
echo "Parameter 'params' fehlt. Erwartet: Straße|PLZ|Stadt\n";
}
Das ganze sind 45 Zeilen Code. In FileMaker hätte ich die JSON-Rückgabe erstmal umständlich zerlegen müssen. Über diese Form bekomme ich einen Parameter mit z.B. diesen Werten.
52.6033116 13.3534074
Dafür habe ich lediglich die Straße, PLZ und Stadt übertragen. Natürlich auch das auf ganz leichte Weise.
ihr-server.de/geocode.p… Ring 83|13435|Berlin
Getrennt durch einen Pipe, werden die Daten in eine FileMaker-Variable geschrieben. Über den FM-Befehl -aus URL einfügen- bekomme ich dann die beiten gewünschten Werte zurück und kann sie einfach in zwei FileMaker Felder schreiben.
Mein Fazit, manchmal lohnt es sich, Dinge einfach zu halten – und unsere „faule“ Lösung ist das beste Beispiel dafür. Mit nur einem Skript und einem einfachen Aufruf können Sie Geodaten in FileMaker integrieren und so Ihre Datenbank erweitern. Probieren Sie es aus und lassen Sie Ihre Adressen lebendig werden!

Individualisieren Sie Maps-Ansichten: Mitarbeiter und Auftragsstatus dynamisch visualisieren mit einfachen Symbolen
Die Darstellung von Informationen auf Karten ist ein essenzielles Werkzeug für Unternehmen mit Außendienstmitarbeitern. Warum sollten Sie sich jedoch mit Standard-Icons zufrieden geben, wenn Sie die Kartenansicht individuell anpassen können? In diesem Blog zeigen wir, wie Sie mit individuell gestalteten Symbolen auf Google Maps nicht nur Mitarbeiter und Teams visuell hervorheben, sondern auch den aktuellen Status ihrer Aufträge darstellen können. Vor einigen Jahren stand ich vor dieser Herausforderung. Also habe ich mich ans Werk gemacht. Es gab 11 Mitarbeiter die entweder allein oder mit einem weiteren Mitarbeiter unterwegs waren bzw. immer noch sind. Diese Lösung ist noch exakt so im Einsatz, muss aber im Zuge der Anforderungsänderung auf jeweils drei Mitarbeiter aufgerüstet werden. Da aber die jetzigen Anforderungen schon eine Anzahl von über 440 Symbolen notwendig macht, werde ich das über dynamische Symbolgenerierung versuchen umzusetzen.
1. Die Ausgangssituation
Stellen Sie sich vor, Ihr Unternehmen verwaltet Außendienstmitarbeiter mit einer FileMaker-Lösung. Mitarbeiter arbeiten allein oder zu zweit, und ihre Aufgaben sind klar kategorisiert: • Wartung (W) • Service (S) • Reparatur (R) • Inbetriebnahme (I)
Die Herausforderung besteht darin, auf einer Karte die folgenden Informationen klar darzustellen: 1. Wer arbeitet wo? 2. Arbeitet der Mitarbeiter alleine oder im Team? 3. Welcher Auftrag wird aktuell bearbeitet?
2. Visuelle Darstellung
Lösen konnte ich die Herausforderung, indem wir individuelle Symbole verwenden, die: • Den Mitarbeiter (oder das Team) mit einem eindeutigen farbigen T-Shirt darstellen. • Ein Badge (W, S, R, I) am Symbol anzeigen, um die Art des Auftrags zu kennzeichnen. • Für nicht verplante Aufträge ein leeres Symbol mit dem entsprechenden Badge anzeigen.
Beispiel: • Mario arbeitet alleine: Ein Symbol mit einem blauen T-Shirt. • Mario und Anna arbeiten zusammen: Ein Symbol mit zwei Figuren, Mario (blaues T-Shirt, Anna mit einem pinken Shirt) ist der Hauptmitarbeiter. • Ein unbesetzter Wartungsauftrag: Eine leere Figur mit einem Badge “W”. • Ein unbesetzter Serviceauftrag: Eine leere Figur mit einem Badge “S”.
3. Vorbereitung der Symbole
Für die Symbole erstellen wir PNG-Dateien, die folgende Elemente kombinieren: 1. Figuren: Einzel- und Doppelpersonen. 2. Farben: T-Shirt-Farben für die Mitarbeiter. 3. Badges: Kleine Icons mit W, S, R oder I.


Die Namen der Symbole haben natürlich festgelegte Namenskonventionen. So z.B. Rot_Hellbraun_M.png oder Rot_Hellbraun_W.png Die Zusammensetzung erfolgt innerhalb von FileMaker über eine Schleife der anzuzeigenden Aufträge, dabei wird dann auch der Name der +.png ermittelt. Die Übergabe erfolgt sowie schon in meinen letzten Beiträgen. Die Aufbereitung ist individuell. Möchte ich das ganze noch klickbar machen um weitere Informationen in einem InfoWindow anzuzeigen, möchte ich gleich ein FileMaker-Script starten usw.
Fazit
Die Individualisierung von Kartenansichten mit FileMaker und Google Maps ist eine tolle Möglichkeit, Außendienstmitarbeiter effizient zu verwalten. Durch die Kombination von Symbolen, Farben und Badges erhalten wir eine klare und visuell ansprechende Darstellung der aktuellen Arbeitsaufteilung. Nur sollten wir darauf achten, das bei vielen möglichen Kombinationen diese Vorgehensweise nicht mehr zeitgemäß ist.

So schaut die Übergabe in meinem Fall aus, z.T. befinden sich Daten die für eine andere Ansichtsart Verwendung finden mit in diesem Datensatz.

FileMaker & PHP – Integration, Möglichkeiten und die Evolution von Version 12 bis heute (leicht wehmütig)
Die Integration von FileMaker-Datenbanken mit PHP war lange Zeit kein zentraler Bestandteil vieler Webentwicklungsprojekte. Doch die Tools und Technologien, die hierfür zur Verfügung stehen, haben sich über die Jahre drastisch verändert. In diesem Beitrag werfen wir einen detaillierten Blick darauf, wie sich die Möglichkeiten seit FileMaker 17 weiterentwickelt haben und welche Vorteile die modernen Ansätze bieten. Dabei gehen wir auf Scriptaufrufe, Parameterübergaben und Unterschiede zwischen älteren und neuen Methoden ein.
Wie bin ich darauf gekommen, diesen kleinen Vergleich zu ziehen. Mir ist ein uraltes Projekt aus FileMaker 13 Zeiten wieder in die Hände gefallen. Ich hatte noch ein Backup in der iCloud gefunden. Zu dieser Zeit nutzte ich diese gerne um Daten zu sichern. Damals bauten wir das Backend in FileMaker, über IWP wurden die Daten im Webbrowser Teilnehmern der Semicon zur Verfügung gestellt. FileMaker diente als Klient am Counter und im Browser für Anmeldung, Kartenzahlung etc. Es war eine Katastrophe, was für ein Geraffel. Mehrere 1000 Zugriffe pro Stunde zwangen den Server immer wieder in die Knie. Aber wir haben sofort beschlossen, das geht im nächsten Jahr so nicht noch einmal. Ich nutze ab diesem Moment PHP und FileMaker in Kombination. Bis heute, für mich die perfekte Symbiose zweier Systeme.
1. Früher: FileMaker PHP API (bis FileMaker 17)
Vor der Einführung der FileMaker Data API (ab Version 17) war die FileMaker PHP API die Standardlösung, um eine Verbindung zwischen einer FileMaker-Datenbank und einer PHP-basierten Anwendung herzustellen. Diese API war eine PHP-Bibliothek, die speziell für FileMaker entwickelt wurde und direkt mit der Web Publishing Engine (WPE) des FileMaker Servers interagierte.
Merkmale der FileMaker PHP API • Eigene Klassenstruktur: Die API stellte Klassen wie FileMaker, FileMaker_Layout, und FileMaker_Command_Find bereit. • Datenabruf: Abfragen wurden durch spezielle Methoden ausgeführt. Ergebnisse kamen als serialisierte PHP-Arrays zurück. • Skriptausführung: FileMaker-Skripte konnten durch die Methode setScript() aufgerufen werden. • Beschränkt auf WPE: Die WPE musste auf dem FileMaker Server aktiviert sein, um die PHP API zu verwenden.
Mal ein kleines Beispiel:
getLayout('DeinLayout'); $request = $fm->newFindCommand($layout); $request->addFindCriterion('Feldname', 'Wert'); $request->setScript('Skriptname', 'Parameterwert'); // Skript und Parameter setzen $result = $request->execute(); if (FileMaker::isError($result)) { echo 'Fehler: ' . $result->getMessage(); } else { echo 'Skript erfolgreich ausgeführt.'; } ?>
Einschränkungen • Die API war schwergewichtig und teilweise langsam. • Abhängigkeit von der WPE machte sie weniger flexibel. • Kein modernes JSON-Format, sondern serialisierte Arrays. • Abkündigung mit der Einführung der FileMaker Data API.
Aber es hatte auch was für sich. In Gedanken bin ich immer dem FileMaker nahe geblieben. Layouts, Tabellen, Script-Namen. Als wenn ich mit FileMaker entwickeln würde, nur mit reinem Quellcode.
2. Heute: FileMaker Data API (ab FileMaker 17)
Die Einführung der FileMaker Data API markierte einen Wendepunkt in der Integration von FileMaker-Daten mit externen Anwendungen. Die neue REST-basierte API macht es einfacher, FileMaker-Daten mit Standard-HTTP-Technologien zu verwalten.
Merkmale der FileMaker Data API • RESTful Architektur: Kommunikation erfolgt über HTTP-Methoden wie GET, POST, PATCH und DELETE. • JSON-Datenformat: Anfragen und Antworten erfolgen in leicht verständlichem JSON. • Skriptausführung: Skripte können direkt über Parameter in der Anfrage ausgeführt werden. • Plattformunabhängig: Kein PHP-Modul erforderlich, funktioniert mit jedem HTTP-Client.
Skriptausführung mit der Data API
Die Data API ermöglicht die Ausführung von FileMaker-Skripten direkt während eines HTTP-Requests. Parameter können bequem als JSON übergeben werden.
Beispiel Heute:
/ Funktion zum Starten des FileMaker-Scripts function startFileMakerScript(orderNumber) { if (!orderNumber || orderNumber === "Keine") { alert("Keine gültige Auftragsnummer vorhanden."); return; } const fileMakerScriptUrl = `fmp://$/DeineDatenbank?script=DeinScript¶m=${encodeURIComponent(orderNumber)}`; console.log(`Starte FileMaker-Script mit URL: ${fileMakerScriptUrl}`); window.open(fileMakerScriptUrl, '_blank'); } if (!employees || employees.length === 0) { alert("Keine passenden Mitarbeiter gefunden."); return; }
Vorteile der Data API • Geschwindigkeit: Die API ist schneller und effizienter als die alte PHP API. • Flexibilität: Funktioniert mit jeder Programmiersprache und nicht nur mit PHP. • Standardkonformität: Nutzung moderner Webstandards (JSON, REST). • Skalierbarkeit: Unterstützt größere Datenmengen und parallele Anfragen.
Fazit: Warum auf die Data API setzen?
Die Data API bietet eine deutlich modernere, schnellere und flexiblere Möglichkeit, mit FileMaker-Daten zu arbeiten. Für neue Projekte ist sie der klare Favorit, da sie standardisierte Technologien nutzt und unabhängig von einer spezifischen Programmiersprache ist. Es macht auch unheimlichen Spaß, FileMaker Schwächen durch HTML/PHP/Java-Script zu kompensieren. Siehe meinem Beitrag zum Thema Portale innerhalb von FileMaker Portalen anzeigen.

Ein altes Bild zu diesem Projekt. Semicon 2013 oder 2014
Vergleich: Mitarbeiterzuordnung mit und ohne KI-Unterstützung
In diesem Blog vergleichen wir zwei Ansätze zur automatischen Zuordnung von Mitarbeitern basierend auf Standort und Verfügbarkeit. Der erste Ansatz nutzt eine KI, um die besten Mitarbeiter auszuwählen, während der zweite Ansatz dies mithilfe von reinem PHP realisiert. Beide Methoden liefern wertvolle Ergebnisse, doch sie haben unterschiedliche Vor- und Nachteile. Eines aber gleich vorneweg, KI ist cool, es ist extrem flexibel, treibt aber mich persönlich an den Rand der Verzweiflung. Warum? Übergebe ich 5 mal die gleichen Daten an die KI, z.B. in meiner Anwendung in folgender Form:
testserver.de/php_calen…|Mario,52.5814763,13.3057333,9:30:00,11:00:00,24204|Mario,52.5346344,13.4224666,12:00:00,13:00:00,22664|Philipp,52.5346344,13.4224666,7:00:00,8:00:00,22664|Philipp,52.4702886,13.2930662,13:00:00,14:00:00,24500|David,52.4702886,13.2930662,10:00:00,11:00:00,24531|Jennifer,52.5035525,13.4176949,9:30:00,10:22:30,24542|Philipp,52.5035525,13.4176949,14:00:00,14:52:30,24543|André,52.5035525,13.4176949,7:30:00,8:22:30,24544|Jennifer,52.6053036,13.3540889,11:00:00,12:30:00,24495|Martin,52.5727963,13.4187507,15:00:00,16:00:00,24485
Dann habe ich zu mindesten zwei verschiedene Ergebnisse. Wenn es ganz schlecht läuft, habe ich 5 verschiedene Ergebnisse.
Code-Beispiel 1: Mitarbeiterzuordnung ohne KI Der folgende PHP-Code sortiert und bewertet Mitarbeiter basierend auf ihrer Entfernung, Verfügbarkeit und Priorität.
$fields[0], 'lat' => (float)$fields[1], 'lng' => (float)$fields[2], 'startTime' => $fields[3], 'endTime' => $fields[4], 'orderNumber' => $fields[5], ]; } } if (empty($parsedData)) { die("Keine gültigen Mitarbeiterdaten empfangen."); } $supportRequest = $parsedData[0]; $remainingEmployees = array_slice($parsedData, 1); // Entfernen des Anforderers aus der Liste $remainingEmployees = array_filter($remainingEmployees, function ($employee) use ($supportRequest) { return $employee['name'] !== $supportRequest['name'] || $employee['lat'] !== $supportRequest['lat'] || $employee['lng'] !== $supportRequest['lng']; }); // Entfernung und Priorität berechnen foreach ($remainingEmployees as &$employee) { $employee['distance'] = round(haversine( $supportRequest['lat'], $supportRequest['lng'], $employee['lat'], $employee['lng'] ), 2); $timeToReach = strtotime($supportRequest['startTime']) - strtotime($employee['endTime']); $timeToWait = strtotime($employee['startTime']) - strtotime($supportRequest['endTime']); if ($employee['distance'] <= 5 && $timeToReach >= 0 && $timeToReach <= 3600) { $employee['priority'] = "high"; } elseif ($employee['distance'] <= 10) { $employee['priority'] = "medium"; } else { $employee['priority'] = "low"; } $employee['time'] = "{$employee['startTime']} - {$employee['endTime']}"; } usort($remainingEmployees, function ($a, $b) { $priorityOrder = ['high' => 1, 'medium' => 2, 'low' => 3]; return $priorityOrder[$a['priority']] <=> $priorityOrder[$b['priority']]; }); $analysisResult = $remainingEmployees; ?>
Vor- und Nachteile
Vorteile ohne KI: 1. Kosten: Keine Abhängigkeit von KI-Diensten wie GPT-4 reduziert die laufenden Kosten erheblich. 2. Kontrolle: Logik und Priorisierungsregeln können exakt definiert und angepasst werden. 3. Performance: Direkte Verarbeitung auf dem Server führt zu schnelleren Ergebnissen.
Nachteile ohne KI: 1. Flexibilität: Änderungen in den Anforderungen erfordern Anpassungen im Code. 2. Komplexität: Erstellen neuer Algorithmen für komplexere Szenarien erfordert Zeit und Fachwissen.
Code-Beispiel 2: Mitarbeiterzuordnung mit KI Hier wird eine KI verwendet, um Mitarbeiter basierend auf Standort und Verfügbarkeit zu priorisieren. Der PHP-Code sendet die Daten an eine KI-API und verarbeitet die Antwort.
Mitarbeiterzuordnung: Ein PHP-basiertes Tool zur OptimierungVor- und Nachteile
Vorteile mit KI: 1. Flexibilität: Anpassungen an den Anforderungen können durch Änderungen der Anweisungen an die KI erfolgen. Dies kann sogar vom Kunden durchgeführt werden wenn z.B. die Anforderungen in FileMaker definiert werden und dann an die KI gesendet werden. 2. Intelligenz: Komplexere Logiken und Datenmuster können leichter berücksichtigt werden.
Nachteile mit KI: 1. Kosten: Jeder API-Aufruf verursacht Kosten, die sich bei häufiger Nutzung summieren können. 2. Abhängigkeit: Funktionalität hängt von der Verfügbarkeit des KI-Dienstes ab. 3. Performance: Zusätzliche Latenz durch API-Aufrufe.
Fazit
Beide Ansätze haben ihre Berechtigung. Der KI-gestützte Ansatz eignet sich für dynamische und komplexe Anforderungen, während die reine PHP-Lösung ideal für festgelegte und kostenbewusste Szenarien ist.
Welcher Ansatz für dich geeignet ist, hängt von deinen spezifischen Anforderungen und Prioritäten ab. Für mich ist nach dem Test klar, die KI werde ich integrieren, aber nur bei extrem komplexen Prozessen. Gedanklich gehe ich dann in die Richtung, Komplette Tagesplanung für ein Team aus z.B. 10-15 Mitarbeitern mit 30-40 Einsätzen in einem Stadtgebiet verstreut.

Mitarbeiterzuordnung: Ein PHP-basiertes Tool zur Visualisierung und Priorisierung
Dieses Projekt kombiniert PHP, die Haversine-Formel und die Google Maps API, um die besten Mitarbeiter für einen Unterstützungsantrag basierend auf Standort und Verfügbarkeit auszuwählen. Das Ergebnis wird auf einer interaktiven Karte dargestellt, und eine Mitarbeiterliste zeigt alle relevanten Details wie Entfernung und Priorität. Der Grund, viele Mitarbeiter sind bei unterschiedlichen Kunden im Einsatz. Es kann aber notwendig sein, das ein zweiter oder dritter Mitarbeiter für diesen Einsatz benötigt werden. Schwierig die Mitarbeiter aus dem Kopf heraus zuordnen zu wollen.
Überblick
Unser Ziel ist es, ein System zu erstellen, das: • Mitarbeiter basierend auf Entfernung und zeitlicher Verfügbarkeit priorisiert. • Standorte auf einer Karte visualisiert. • Überlappende Standorte automatisch verschiebt, um sie sichtbar zu machen. • Eine übersichtliche Mitarbeiterliste generiert.
PHP: Entfernung und Datenverarbeitung
Im folgenden PHP-Skript berechnen wir die Entfernung zwischen Standorten mit der Haversine-Formel, filtern die Mitarbeiter und senden die Daten an eine KI-Analyse.
$fields[0],
'lat' => (float)$fields[1],
'lng' => (float)$fields[2],
'startTime' => $fields[3],
'endTime' => $fields[4],
];
}
}
// Wenn keine gültigen Daten vorhanden sind
if (empty($parsedData)) {
die("Keine gültigen Mitarbeiterdaten empfangen.");
}
// Der Mitarbeiter, der Unterstützung benötigt
$supportRequest = $parsedData[0];
$remainingEmployees = array_slice($parsedData, 1);
// Entfernung berechnen und in das Array hinzufügen
foreach ($remainingEmployees as &$employee) {
$employee['distance'] = round(haversine(
$supportRequest['lat'], $supportRequest['lng'],
$employee['lat'], $employee['lng']
), 2);
}
// Anfrage an KI zur Analyse senden
$payload = [
'supportRequest' => $supportRequest,
'employees' => $remainingEmployees,
];
// Die Anfrage an die OpenAI-API bleibt unverändert und liefert die Ergebnisse
// JSON-Analyse bleibt wie im Skript
?>
Visualisierung mit Google Maps API
Hier zeigen wir, wie die Ergebnisse visualisiert werden, einschließlich der dynamischen Verschiebung überlappender Marker und einer dynamischen Mitarbeiterliste.
<script>
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: { lat: <?php echo $supportRequest['lat']; ?>, lng: <?php echo $supportRequest['lng']; ?> },
});
const employees = <?php echo json_encode($analysisResult); ?>;
// Unterstützungsmarker hinzufügen
new google.maps.Marker({
position: { lat: <?php echo $supportRequest['lat']; ?>, lng: <?php echo $supportRequest['lng']; ?> },
map: map,
title: "Unterstützungsantrag",
});
// Mitarbeiter hinzufügen
const OFFSET_DISTANCE = 0.001;
const seenLocations = new Map();
employees.forEach((employee) => {
let key = `${employee.lat},${employee.lng}`;
if (seenLocations.has(key)) {
const offset = seenLocations.get(key) + 1;
employee.lat += OFFSET_DISTANCE * offset;
employee.lng += OFFSET_DISTANCE * offset;
seenLocations.set(key, offset);
} else {
seenLocations.set(key, 0);
}
new google.maps.Marker({
position: { lat: employee.lat, lng: employee.lng },
map: map,
title: `${employee.name} - ${employee.distance} km`,
});
});
}
window.onload = initMap;
</script>
Was übergeben wir aus FileMaker
https://deine_url.de/ki_calendar.php?data=André,52.5035525,13.4176949,7:30:00,8:22:30|Mario,52.5814763,13.3057333,9:30:00,11:00:00|Mario,52.5346344,13.4224666,12:00:00,13:00:00|,52.5346344,13.4224666,7:00:00,8:00:00|Philipp,52.4702886,13.2930662,13:00:00,14:00:00|David,52.4702886,13.2930662,10:00:00,11:00:00|Jennifer,52.5035525,13.4176949,9:30:00,10:22:30|Philipp,52.5035525,13.4176949,14:00:00,14:52:30|André,52.5035525,13.4176949,7:30:00,8:22:30|Jennifer,52.6053036,13.3540889,11:00:00,12:30:00|Martin,52.5727963,13.4187507,15:00:00,16:00:00
Fazit
Die Rolle der KI in unserem Projekt
Bei diesem Projekt haben wir die GPT-4 API von OpenAI genutzt, eine hochmoderne KI, die auf natürliche Sprachverarbeitung spezialisiert ist. Die KI übernimmt dabei eine entscheidende Rolle:
Aufgaben der KI 1. Analyse der Daten: Die KI wertet die übermittelten Daten aus, einschließlich der Standorte und Verfügbarkeiten der Mitarbeiter, sowie der geforderten Zeitspanne. Sie trifft Entscheidungen, welche Mitarbeiter am besten geeignet sind, basierend auf: • Geografischer Nähe (Entfernung). • Zeitlicher Verfügbarkeit (Überlappung mit der Anforderungszeit). • Priorisierungskriterien. 2. Priorisierung der Mitarbeiter: Die KI sortiert die Mitarbeiter in Prioritätsklassen (hoch, mittel, niedrig), um Entscheidungsprozesse zu erleichtern. Dies hilft besonders bei komplexen Szenarien mit vielen Teilnehmern und unterschiedlichen Anforderungen. 3. Flexible Verarbeitung: Mit der eingebauten Sprachverarbeitungsfähigkeit kann die KI auf benutzerdefinierte Regeln und neue Anforderungen reagieren. Im Falle unseres Projekts wird sichergestellt, dass: • Der Supportanforderer (Mario) nicht als Unterstützer in die Liste aufgenommen wird. • Die Ergebnisse stets im JSON-Format zurückgegeben werden, damit sie direkt weiterverarbeitet werden können.
Warum GPT-4? • Komplexe Entscheidungen: GPT-4 kann nicht nur einfache Regeln anwenden, sondern auch inhaltlich komplexe Daten wie geografische Koordinaten, Zeitfenster und Prioritäten verknüpfen. • Flexibilität: Änderungen in den Anforderungen (z. B. neue Priorisierungsregeln) lassen sich einfach umsetzen, indem wir die KI-Prompts anpassen. • Effizienz: Im Gegensatz zu einer festen Programmierung ermöglicht die KI schnelle Analysen und Rückmeldungen, ohne den PHP-Code manuell anzupassen.
Das Projekt zeigt, wie sich PHP, eine KI-API und Google Maps zu einem leistungsstarken Logistik-Tool kombinieren lassen. Dies ist natürlich nur eine erste Version und verarbeitet nur wenige Daten.

Optimierung von Google Maps zur Verwaltung mehrerer Aufträge mit überlappenden Geodaten
In der modernen Logistik und Auftragsverwaltung sind präzise Karten mit mehreren Aufträgen ein unverzichtbares Werkzeug. Doch was passiert, wenn mehrere Aufträge dieselbe Geoposition teilen? Zu unterscheiden sind diese Marker nur über die Label. Diese zeigen in diesem Projekt die Auftragsnummer an. Ist diese unleserlich, dann ist klar, das liegen mehrere Marker übereinander. Unser jüngstes Projekt konzentrierte sich auf die Visualisierung solcher Szenarien, einschließlich der farblichen Hervorhebung und Gruppierung überlappender Marker sowie der optimierten Datenübertragung an externe Systeme wie FileMaker, in unserem Fall ausschließlich FileMaker.
Herausforderung
Das Ziel war es, eine dynamische Google Maps-Anwendung zu entwickeln, die: 1. Mehrere Aufträge darstellt, auch wenn sie die gleiche Position haben. Gleiche Positionen bedeuten, wir haben die gleichen Geo-Daten bzw. Hausnummer, Straße und PLZ sind identisch. 2. Farbcodierungen für solche „überlappenden“ Marker nutzt, um sie besser erkennbar zu machen. 3. Detaillierte Informationen über jeden Auftrag in einem Infowindow anzeigt. 4. Die optimierte Reihenfolge der Aufträge nahtlos an FileMaker überträgt.
Unsere Lösung
1 Marker-Anpassung für überlappende Geodaten
Wir haben eine Kartenfunktion implementiert, die doppelte Marker an derselben Position erkennt und diese farblich hervorhebt. Dabei haben wir: • Einen blauen Marker für Einzelaufträge verwendet. • Grüne Marker eingeführt, um überlappende Positionen zu kennzeichnen.
Zusätzlich zeigt ein Infowindow bei Klick auf einen Marker alle zugehörigen Auftragsnummern an. Über die angezeigten Auftragsnummer können per Klick, alle relevanten Daten innerhalb von FileMaker aufgerufen werden.
2 Optimierung der Datenübertragung
Wir haben die Datenübertragung so optimiert, dass nur relevante Informationen gesendet werden. Im Vorfeld wurde schon durch ein PHP-Script eine Vorauswahl getroffen. Dieses kann natürlich auch direkt über FileMaker erfolgen: • Origin: Die Startposition wurde durch die echte Auftrags-ID ersetzt. • Waypoints: Es wurden maximal neun Wegpunkte übergeben. • Destination: Das Ziel wurde als letzter Punkt hinzugefügt.
3 Dynamische Gruppierung
Durch die Implementierung einer Map-Datenstruktur wurden überlappende Marker gesammelt und in einem einzigen Marker angezeigt. Der Tooltip zeigt auf Wunsch alle zugehörigen Aufträge an.
Technische Highlights
Marker mit Farbcodierung
const markerColor = location.ids.length > 1 ? '#00FF00' : '#4285F4'; // Grün für mehrere IDs const markerLabel = location.ids.length > 1 ? 'Mehrere' : location.label; const marker = new google.maps.Marker({ position: location.position, map: map, label: { text: markerLabel, color: '#000', fontSize: '12px', fontWeight: 'bold', }, icon: { path: google.maps.SymbolPath.CIRCLE, scale: 8, fillColor: markerColor, fillOpacity: 1, strokeColor: '#FFF', strokeWeight: 2, labelOrigin: new google.maps.Point(0, -5), }, });
Unser InfoWindows mit mehreren Auftragsnummer:
const contentString = `Auftragsnummern:`; marker.addListener('click', () => { infowindow.setContent(contentString); infowindow.open(map, marker); });
${location.ids.map(id => ` Auftrag ${id} `).join('
')}
Anlagen:
${location.facilities}
Optimierte Reihenfolge an FileMaker senden, dort werden die Daten in Reihenfolge gebracht und können weiter verarbeitet werden.
function sendToFileMaker() { if (optimizedOrder.length > 0) { const originId = = json_encode($auftrag[0] ?? 'Unknown') ?>; const updatedOrder = [originId, ...optimizedOrder.slice(1)]; const filemakerUrl = `fmp://$/Deine Datei?script=DeinFMScript¶m=${encodeURIComponent(updatedOrder.join('\n'))}`; window.location.href = filemakerUrl; } else { alert('Die Route ist noch nicht optimiert. Bitte warten.'); } }
Die Datenübergabe erfolgt wie immer in der Art -deine_url_de/maps.php?json=true&origin=52.5212262,13.3340740 &destination=52.5070279,13.345529 &waypoints=52.4897075,13.3290075|52.4902317,13.355636|52.5037865,13.3271369|52.504083,13.3386575&auftrag=24501|24510|24547|24551|24573|24580&facilities=6039|1940|4540|5036|6634|6545 Zusammenstellen der Daten erfolgt über Schleifen, SQL oder Listen die per Referenzen die richtigen Daten enthalten.
Wie in den letzten Beiträgen schon beschrieben, nutzen wir ein WebViewer in FileMaker.
Fazit
Unsere Lösung zeigt, wie smarte Anpassungen in Visualisierungen und Datenübertragungen den Unterschied machen können. Mit farblich abgestimmten Markern, informativen Pop-ups und cleveren Datenstrukturen haben wir nicht nur die Benutzerfreundlichkeit auf ein neues Level gehoben, sondern auch die Effizienz der Prozesse ordentlich gepusht.

Ein schöner Tag....
Meine Frau weiß genau was ein Programmierer zum arbeiten benötigt. Viel Cola…. Danke für meinen schönen Geburtstag 🎂 ❤️

Vor- und Nachteile der Haverschen Formel im Vergleich zur Google Maps API für Entfernungsberechnungen
Die genaue Berechnung von Entfernungen ist ein zentraler Bestandteil vieler geografischer und logistischer Anwendungen. Dabei stehen oft zwei Methoden im Fokus: die Haversche Formel und die Entfernungsberechnung mithilfe der Google Maps API. Doch welche Methode eignet sich für welchen Anwendungsfall, und worin liegen ihre Vor- und Nachteile?
Was ist die Haversche Formel?
Die Haversche Formel ist eine mathematische Methode zur Berechnung der kürzesten Entfernung zwischen zwei Punkten auf der Oberfläche einer Kugel. Diese Methode berücksichtigt die Krümmung der Erde, geht jedoch von einer idealisierten kugelförmigen Erde aus.
Die Formel lautet:
a = sin²(Δφ / 2) + cos(φ₁) * cos(φ₂) * sin²(Δλ / 2) c = 2 * atan2(√a, √(1-a)) d = R * c Variablenbeschreibung: • φ₁, φ₂: Breitengrade der Punkte • λ₁, λ₂: Längengrade der Punkte • R: Erdradius (ca. 6.371 km) • d: Ergebnis der Entfernung
Vorteile der Haverschen Formel 1. Unabhängigkeit: Sie benötigt nur die geografischen Koordinaten der Punkte (Breitengrad und Längengrad). 2. Effizienz: Keine externe API-Abfrage notwendig; Berechnungen können lokal durchgeführt werden. 3. Einfache Implementierung: Kann in praktisch jeder Programmiersprache leicht umgesetzt werden.
Nachteile der Haverschen Formel 1. Nur Luftlinie: Die Berechnung liefert die kürzeste Entfernung auf einer Kugel, berücksichtigt jedoch keine tatsächlichen Straßen, Hindernisse oder andere geografische Gegebenheiten. 2. Genauigkeit: Die Annahme einer perfekten Kugel führt zu leichten Abweichungen, da die Erde keine Kugel ist. Das kann im allgemeinen vernachlässigt werden, wenn wir z.B. ein Stadtgebiet betrachten wie Berlin, dann interessiert uns die Krümmung der Erde wenig. Viel schlimmer ist die Vernachlässigung der Straßenführung.
Wie funktioniert die Entfernungsberechnung in der Google Maps API?
Die Google Maps API verwendet eine komplexere Methodik zur Berechnung von Entfernungen. Dabei werden reale Straßennetze, Verkehrsdaten und andere Faktoren berücksichtigt, um die tatsächlich benötigte Strecke und Zeit zu ermitteln. 1. Schritt 1: Übermittlung der Koordinaten und Route per API-Call. 2. Schritt 2: Berechnung der Entfernung und/oder Dauer anhand von Straßendaten, Verkehrsbedingungen und anderen Faktoren. 3. Schritt 3: Rückgabe detaillierter Daten, z. B. Entfernung, Dauer und Streckenführung.
Vorteile der Google Maps API 1. Reale Entfernungen: Liefert die tatsächliche Strecke basierend auf Straßen- und Verkehrsdaten. 2. Zusätzliche Informationen: Neben der Entfernung auch Zeitdauer, Wegbeschreibungen und Verkehrsinformationen verfügbar. 3. Hohe Genauigkeit: Ideal für Routenplanung und logistische Anwendungen.
Nachteile der Google Maps API 1. Kosten: Ab einer bestimmten Anzahl an Anfragen können Gebühren anfallen. 2. Abhängigkeit von externen Diensten: Funktioniert nur mit einer stabilen Internetverbindung. 3. Komplexität: Die Integration erfordert API-Schlüssel und ein grundlegendes Verständnis der API-Dokumentation.
Um die Google Maps API nutzen zu können, benötigen wir einen API-Schlüssel, den wir in der Google Cloud Console erstellen. Dieser Schlüssel ermöglicht den Zugriff auf verschiedene Dienste, darunter die Distance Matrix API oder die Directions API.
- Beispiel: Entfernungsmessung mit der Directions API
Die Directions API eignet sich hervorragend, um Entfernungen zwischen mehreren Punkten zu berechnen. Hier ein Beispiel:
API-Aufruf
Der Aufruf erfolgt durch eine URL wie diese: https://maps.googleapis.com/maps/api/directions/json?origin=52.515956,13.388512&destination=52.4981845,13.4610157&waypoints=52.520008,13.404954|52.529407,13.397634&key=DEIN_API_KEY
• origin: Startpunkt
• destination: Zielpunkt
• waypoints: Zwischenstationen, durch | getrennt
• key: Der API-Schlüssel
Der Rückgabewert der API ist ein JSON-Objekt. Dieses JSON enthält Details zur Route, darunter Entfernungen, Fahrzeiten und Wegbeschreibungen.
Aufruf:
<?php
// API-Schlüssel
$apiKey = 'DEIN_API_KEY';
// Parameter
$origin = '52.515956,13.388512';
$destination = '52.4981845,13.4610157';
$waypoints = '52.520008,13.404954|52.529407,13.397634';
// API-URL zusammenbauen
$url = "https://maps.googleapis.com/maps/api/directions/json?" .
"origin=" . urlencode($origin) .
"&destination=" . urlencode($destination) .
"&waypoints=" . urlencode($waypoints) .
"&key=" . $apiKey;
// API-Anfrage
$response = file_get_contents($url);
$data = json_decode($response, true);
// Ausgabe der Entfernungen
if ($data['status'] === 'OK') {
foreach ($data['routes'][0]['legs'] as $leg) {
echo "Start: " . $leg['start_address'] . "\\n";
echo "Ziel: " . $leg['end_address'] . "\\n";
echo "Entfernung: " . $leg['distance']['text'] . "\\n";
echo "Dauer: " . $leg['duration']['text'] . "\\n";
echo "-----------------\\n";
}
} else {
echo "Fehler: " . $data['status'];
}
?>
Die JSON Rückgabe würde so aussehen:
{ "status": "OK", "routes": [ { "legs": [ { "start_address": "Berlin Hauptbahnhof, Berlin, Germany", "end_address": "Alexanderplatz, Berlin, Germany", "distance": { "text": "3.4 km", "value": 3400 }, "duration": { "text": "10 mins", "value": 600 } }, { "start_address": "Alexanderplatz, Berlin, Germany", "end_address": "Brandenburger Tor, Berlin, Germany", "distance": { "text": "2.1 km", "value": 2100 }, "duration": { "text": "7 mins", "value": 420 } } ], "summary": "Route through Berlin", "bounds": { "northeast": { "lat": 52.531677, "lng": 13.40732 }, "southwest": { "lat": 52.49677, "lng": 13.37889 } } } ] }
Erklärung der JSON-Felder • status: Gibt den Status der API-Abfrage zurück. Hier ist der Wert “OK”, was bedeutet, dass die Anfrage erfolgreich war. • routes: Enthält eine Liste von möglichen Routen. Jede Route hat Details wie die legs, summary, und bounds. • legs: Repräsentiert einzelne Abschnitte einer Route (z. B. von einem Waypoint zum nächsten). • start_address: Startpunkt dieses Abschnitts. • end_address: Endpunkt dieses Abschnitts. • distance: Entfernung in Text (text) und in Metern (value). • duration: Geschätzte Fahrzeit in Text (text) und in Sekunden (value). • summary: Eine kurze Beschreibung der Route. • bounds: Geografische Grenzen der Route, definiert durch northeast und southwest Ecken.
In FileMaker kannst du JSON-Daten mit den eingebauten JSON-Funktionen verarbeiten. Über ein Insert from URL, so in der Art: Setzte Variable $$URL Wert https://maps.googleapis.com/maps/api/directions/json?origin=Berlin&destination=Munich&key=DEIN_API_KEY
Flexibler wird das ganze natürlich wenn ich die URL schon als Variable vorliegen habe, aber das ist abhängig von der Art und Weise des Entwicklers. Die Antwort (Ziel) speichern wir in einer Variablen z.B. $$JSON
Nun könnten wir diesen Bereich auslesen:
{ "routes": [ { "legs": [ { "start_address": "Berlin Hauptbahnhof, Berlin, Germany", "end_address": "Alexanderplatz, Berlin, Germany", "distance": { "text": "3.4 km", "value": 3400 }, "duration": { "text": "10 mins", "value": 600 } } ] } ] }
Variable setzen $start = Wert gleich JSONGetElement ( $json ; “routes[0].legs[0].start_address” )
Variable setzen $distance = Wert gleich JSONGetElement ( $json ; “routes[0].legs[0].distance.text” )
Das über eine Schleife durch alle Elemente der JSON Rückantwort. Die weitere Verarbeitung ist dann an den Zweck gebunden.
Set Variable [ $i ; Value: 0 ]
Set Variable [ $legsCount ; Value: JSONListKeys ( $json ; "routes[0].legs" ) ]
Loop
Exit Loop If [ $i ≥ $legsCount ]
Set Variable [ $startAddress ; JSONGetElement ( $json ; "routes[0].legs[" & $i & "].start_address" ) ]
Set Variable [ $endAddress ; JSONGetElement ( $json ; "routes[0].legs[" & $i & "].end_address" ) ]
Set Variable [ $distance ; JSONGetElement ( $json ; "routes[0].legs[" & $i & "].distance.text" ) ]
Set Variable [ $duration ; JSONGetElement ( $json ; "routes[0].legs[" & $i & "].duration.text" ) ]
New Record/Request
Set Field [ Tabelle::StartAdresse ; $startAddress ]
Set Field [ Tabelle::ZielAdresse ; $endAddress ]
Set Field [ Tabelle::Entfernung ; $distance ]
Set Field [ Tabelle::Dauer ; $duration ]
Set Variable [ $i ; $i + 1 ]
End Loop
Das Fazit. Die Google Maps API und die Haversche Formel sind zwei leistungsfähige Werkzeuge zur Entfernungsberechnung. Während die Haversche Formel durch ihre Einfachheit punktet, liefert die Google Maps API präzisere Ergebnisse für reale Anwendungsfälle. Die Wahl hängt letztlich von den Anforderungen an Genauigkeit und Komplexität ab. Ich persönlich nutze die Haversche Formel gern um schon eine Vorauswahl der zu übertragenden Werte zu treffen. Dann muss ich keine 100 Geo-Daten an die Maps API übertragen, es reichen auch 10.

Optimierte Routenplanung mit Google Maps und FileMaker
In der heutigen digitalen Arbeitswelt spielen nahtlose Integrationen zwischen verschiedenen Systemen eine entscheidende Rolle. Besonders bei der Optimierung von Routen für Außendienstmitarbeiter oder Logistikunternehmen ist es wichtig, sowohl Zeit als auch Kosten zu sparen. Dieser Blog-Post zeigt, wie man Google Maps für die dynamische Routenplanung nutzt und gleichzeitig Daten in FileMaker integriert.
Das Problem
Viele Unternehmen stehen vor der Herausforderung, eine Vielzahl von Wegpunkten (Aufträgen) optimal zu sortieren, um eine effiziente Route zu erstellen. Dies erfordert Tools, die nicht nur Kartenansichten bereitstellen, sondern auch die Reihenfolge der Stopps optimieren und diese Informationen zurück an das System (in diesem Fall FileMaker) senden können.
Die Lösung
Das folgende PHP-Script löst dieses Problem, indem es: 1. Google Maps API verwendet, um Wegpunkte zu optimieren. 2. Die optimierte Reihenfolge als Liste darstellt. 3. Ein FileMaker-Script auslöst, um diese Daten für weitere Prozesse zu nutzen.
Der erste Schritt steht darin die Daten die benötigt werden innerhalb von FileMaker zu sammeln. Dies ist möglich über die Erstellung einer Variable per Listenfunktion, SQL oder per Schleife. Wie auch immer die Umsetzung erfolgt, am Ende benötigen wir eine URL die wir an unser PHP-Script senden können. Enthalten sein müssen, die Geo-Daten und natürlich eindeutige IDs aus FileMaker damit diese Daten später auch wieder in FileMaker verarbeitet werden können. Eine URL sollte diesen Aufbau haben: https://dein-server.de/maps.php?json=true&origin=52.5212262,13.3340740 &destination=52.5070279,13.345529 &waypoints=52.4897075,13.3290075|52.4902317,13.355636|52.5037865,13.3271369|52.504083,13.3386575&auftrag=24501|24510|24547|24551|24573|24580&facilities=6039|1940|4540|5036|6634|6545 Diese werden dann über den FM-Befehl, -AUS URL einfügen- übergeben. Wie der Aufbau innerhalb von FileMaker funktioniert ist selbstredend. Ein WebViewer innerhalb von FileMaker muss natürlich vorhanden sein. Die Rückgabe muss an den WebViewer z.B. eine globale Variable erfolgen.
Das PHP Script:
<?php
// API-Schlüssel für Google Maps
$apiKey = 'Ihr_Google_Maps_API_Schlüssel';
// Werte aus GET-Parametern holen oder Standardwerte setzen
$origin = isset($_GET['origin']) ? $_GET['origin'] : '52.515956,13.388512';
$destination = isset($_GET['destination']) ? $_GET['destination'] : '52.4981845,13.4610157';
$waypoints = isset($_GET['waypoints']) ? $_GET['waypoints'] : '';
$auftrag = isset($_GET['auftrag']) ? explode('|', $_GET['auftrag']) : [];
$facilities = isset($_GET['facilities']) ? explode('|', $_GET['facilities']) : [];
// Wegpunkte in ein Array umwandeln
$waypointsArray = explode('|', $waypoints);
?>
- Origin (Startpunkt)
Der Origin ist der Ausgangspunkt der Route. • Es handelt sich um die Position, von der die Route beginnt. • In der Regel wird sie als geografische Koordinate angegeben (z. B. 52.515956,13.388512 für Berlin). • Beispiel: Eine zentrale Lagerhalle oder der Startpunkt eines Fahrers.
- Destination (Endpunkt)
Die Destination ist das Ziel der Route. • Es ist der Punkt, an dem die Route endet. • Ähnlich wie der Origin wird auch die Destination als geografische Koordinate angegeben. • Beispiel: Der Zielort eines Lieferauftrags.
- Waypoint (Zwischenstation)
Ein Waypoint ist eine oder mehrere Zwischenstationen entlang der Route. • Es handelt sich um Orte, die der Fahrer besuchen muss, bevor er sein Ziel (Destination) erreicht. • Waypoints können ebenfalls als Koordinaten (z. B. 52.5062585,13.2845797) angegeben werden. • Sie werden oft verwendet, um Stopps bei Kunden, Lieferstationen oder Servicepunkten darzustellen. • Google Maps erlaubt, mehrere Waypoints anzugeben, um komplexe Routen zu erstellen.
Das Script liest die Parameter origin, destination, waypoints, auftrag, und facilities, um sie für die Routenplanung vorzubereiten. Diese Parameter könnten direkt aus einer FileMaker-Datenbank oder einer anderen Quelle kommen.
Die Routenoptimierung
Das Herzstück des Scripts ist die Funktion initMap():
function initMap() { const map = new google.maps.Map(document.getElementById(‘map’), { zoom: 13, center: { lat: , lng: }, });
const directionsService = new google.maps.DirectionsService();
const directionsRenderer = new google.maps.DirectionsRenderer({ map: map });
directionsService.route(
{
origin: locations[0].position,
destination: locations[locations.length - 1].position,
waypoints: locations.slice(1, -1).map(location => ({
location: location.position,
stopover: true,
})),
travelMode: google.maps.TravelMode.DRIVING,
optimizeWaypoints: true,
},
(result, status) => {
if (status === 'OK') {
directionsRenderer.setDirections(result);
const waypointOrder = result.routes[0].waypoint_order.map(index => locations[index + 1].label);
optimizedOrder = [locations[0].label, ...waypointOrder, locations[locations.length - 1].label];
} else {
console.error('Error:', status);
}
}
);
}
Die Funktion ruft die Google Maps Directions API auf, um die optimale Reihenfolge der Wegpunkte zu berechnen. Diese Reihenfolge wird in optimizedOrder gespeichert.
Integration mit FileMaker
Das Script enthält einen Button, der die optimierte Reihenfolge an ein FileMaker-Script übergibt:
function sendToFileMaker() {
if (optimizedOrder.length > 0) {
const filemakerUrl = fmp://$/DEINE_DATEI?script=(0002)_Planung_Direkt_aus_Maps_holen¶m=${encodeURIComponent(optimizedOrder.join('\n'))}
;
window.location.href = filemakerUrl;
} else {
alert(‘Die Route ist noch nicht optimiert. Bitte warten.');
}
}
Dieser Button ermöglicht es, die optimierten Daten direkt in FileMaker zu laden, wo sie für weitere Aktionen wie die Gruppierung von Aufträgen oder Berichte verwendet werden können.
Vorteile der Lösung 1. Zeitersparnis: Die API optimiert die Route automatisch. 2. Nahtlose Integration: Die Übergabe an FileMaker erfolgt reibungslos. 3. Flexibilität: Die Darstellung in der Karte ermöglicht eine visuelle Kontrolle der Route. 4. Erweiterbarkeit: Zusätzliche Funktionen wie das Hinzufügen von z.B. Anlagen können leicht integriert werden.
Fazit
Dieses Script ist eine effektive Lösung für Unternehmen, die ihre Logistikprozesse optimieren möchten. Durch die Kombination von Google Maps und FileMaker wird eine leistungsstarke, flexible und leicht implementierbare Lösung geschaffen.
Als Anmerkung ist allerdings zu beachten, in diesem Fall können nur 10 Punkte verarbeitet werden. Dies erfordert eine Vorauswahl der an Google zu übergebenen Punkte. Dies kann direkt schon über die Google APIs erfolgen oder z.B. die Haversche Formel.
Zum Überblick das gesamte Script:
// Werte aus GET-Parametern holen oder Standardwerte setzen
$origin = isset($_GET['origin']) ? $_GET['origin'] : '52.515956,13.388512';
$destination = isset($_GET['destination']) ? $_GET['destination'] : '52.4981845,13.4610157';
$waypoints = isset($_GET['waypoints']) ? $_GET['waypoints'] : '';
$auftrag = isset($_GET['auftrag']) ? explode('|', $_GET['auftrag']) : [];
$facilities = isset($_GET['facilities']) ? explode('|', $_GET['facilities']) : [];
// Wegpunkte in ein Array umwandeln um Anzeige über PIN
$waypointsArray = explode('|', $waypoints);
?>
<!DOCTYPE html>
<html>
<head>
<title>Dynamische Route</title>
<script src="https://maps.googleapis.com/maps/api/js?key=DEIN_API_KEY"></script>
<script>
let optimizedOrder = []; // Variable für die optimierte Reihenfolge
function initMap() {
// Initialisiere die Karte
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 13,
center: { lat: <?php echo explode(',', $origin)[0]; ?>, lng: <?php echo explode(',', $origin)[1]; ?> },
});
const locations = [
{
position: { lat: <?php echo explode(',', $origin)[0]; ?>, lng: <?php echo explode(',', $origin)[1]; ?> },
label: '<?php echo $auftrag[0] ?? "Start"; ?>',
facilities: '<?php echo $facilities[0] ?? "Keine Anlagen"; ?>',
},
<?php foreach ($waypointsArray as $index => $point): ?>
{
position: { lat: <?php echo explode(',', $point)[0]; ?>, lng: <?php echo explode(',', $point)[1]; ?> },
label: '<?php echo $auftrag[$index + 1] ?? "Waypoint"; ?>',
facilities: `<?php echo str_replace(',', '<br>', $facilities[$index + 1] ?? "Keine Anlagen"); ?>`,
},
<?php endforeach; ?>
{
position: { lat: <?php echo explode(',', $destination)[0]; ?>, lng: <?php echo explode(',', $destination)[1]; ?> },
label: '<?php echo $auftrag[count($auftrag) - 1] ?? "Ziel"; ?>',
facilities: '<?php echo $facilities[count($facilities) - 1] ?? "Keine Anlagen"; ?>',
}
];
// Details zu Markern und Kreisen hinzufügen
locations.forEach((location) => {
const marker = new google.maps.Marker({
position: location.position,
map: map,
label: {
text: location.label,
color: '#000',
fontSize: '12px',
fontWeight: 'bold',
},
});
});
map.fitBounds(bounds);
}
function sendToFileMaker() {
if (optimizedOrder.length > 0) {
const filemakerUrl = `fmp://$/DEINE_DATEI?script=Planung¶m=${encodeURIComponent(optimizedOrder.join('\n'))}`;
window.location.href = filemakerUrl;
} else {
alert('Die Route ist noch nicht optimiert. Bitte warten.');
}
}
</script>
</head>
<body onload="initMap()">
<div id="map" style="width: 100%; height: 80%;"></div>
<button onclick="sendToFileMaker()">Optimierte Reihenfolge senden</button>
</body>
</html>
Erweiterte Portale in FileMaker: Liste mit PHP und Web Viewer
Erweiterte Portale in FileMaker: Anlagenliste mit PHP und Web Viewer
FileMaker ist ein vielseitiges Werkzeug für Datenbankanwendungen. Doch manchmal stoßen wir an technische Grenzen, wie die fehlende Möglichkeit, innerhalb eines Portals ein weiteres Portal zu platzieren. Diese Einschränkung können wir mit PHP und einem Web Viewer umgehen. In diesem Blog zeigen wir, wie du Anlagen zu einem Auftrag in einem Popover-Button darstellen kannst – inklusive interaktiver Buttons, die FileMaker-Scripts ausführen. Somit wirst Du in die Lage versetzt einen Datensatz auch in FileMaker anzusprechen und nicht nur den WebViewer zur Anzeige zu nutzen. In Meinem Beispiel, gehe ich von Aufträgen aus. Diese werden über ein Portal angezeigt. Jeder Auftrag kann mehrere Anlagen, bei meinem derzeitigen Kunden, sind es halt Anlagen im Bereich der Wasserversorgung.
Das Ziel 1. Anzeigen der Anlagen eines Auftrags: Eine dynamische Liste von Anlagen, dargestellt in einem Web Viewer.
2. Interaktive Aktionen:
Buttons, die ein FileMaker-Script starten, um die jeweilige Anlage anzuzeigen.
Lösung: FileMaker + PHP
- Datenübergabe von FileMaker an PHP
Der erste Schritt ist, die relevanten Daten – z. B. eine Liste von Anlagen – an ein PHP-Script zu übergeben. Dies geschieht mit einem FileMaker-Script, das die Web Viewer-URL dynamisch setzt:
Set Variable [ $auftragID ; Aufträge::ID ] Set Variable [ $anlagenListe ; Austauschen ( List ( Anlagen::AnlagenID ) ; “¶” ; “|” ) ] Set Variable [ $url ; “https://deine-webseite.de/anlagenanzeige.php?auftrag=” & $auftragID & “&anlagen=” & $anlagenListe ] Set Web Viewer [ Object Name: “WebViewer_Anlagen” ; URL: $url ]
Es ist natürlich auch möglich dem WebViewer Dynamisch eine Globale Variable zu übergeben. Dann sage ich einfach setze Variable $$WebViewer0 $url.
Nun benötigen wir natürlich noch ein PHP Script mit der beispielhaften Bezeichnung, -anlagenanzeige.php-. Dieses muss nach Erstellung auf den entsprechenden Server geladen werden.
<?php
// Daten aus der URL übernehmen
$auftragID = $_GET['auftrag'] ?? '';
$anlagenListe = $_GET['anlagen'] ?? '';
$anlagenArray = explode('|', $anlagenListe);
// Sicherheitsmaßnahmen
$auftragID = htmlspecialchars($auftragID, ENT_QUOTES, 'UTF-8');
$anlagenArray = array_map(function($anlage) {
return htmlspecialchars($anlage, ENT_QUOTES, 'UTF-8');
}, $anlagenArray);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Anlagen zu Auftrag <?= $auftragID ?></title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.anlage { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
.anlage-btn { padding: 5px 10px; background-color: #4285F4; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<h1>Anlagen zu Auftrag <?= $auftragID ?></h1>
<?php if (!empty($anlagenArray)): ?>
<?php foreach ($anlagenArray as $anlage): ?>
<div class="anlage">
<p>Anlage ID: <?= $anlage ?></p>
<button class="anlage-btn" onclick="openInFileMaker('<?= $anlage ?>')">Anlage öffnen</button>
</div>
<?php endforeach; ?>
<?php else: ?>
<p>Keine Anlagen gefunden.</p>
<?php endif; ?>
<script>
function openInFileMaker(anlageID) {
const fileMakerUrl = `fmp://$/DeineDatenbank?script=AnlageAnzeigen¶m=${encodeURIComponent(anlageID)}`;
window.location.href = fileMakerUrl;
}
</script>
</body>
</html>
Dieses Script: • Nimmt die Anlagenliste entgegen und zerlegt sie in ein Array. • Baut eine HTML-Darstellung mit Buttons für jede Anlage. • Bindet JavaScript ein, um bei einem Klick auf den Button ein FileMaker-Script zu starten.
- Web Viewer in FileMaker
Füge einen Web Viewer in dein Layout ein und setze dessen URL mit dem oben gezeigten FileMaker-Script. Die Daten werden beim Öffnen des Popovers automatisch geladen und angezeigt.
Ergebnis 1. Dynamische Anzeige: Die Anlagenliste wird aus der FileMaker-Datenbank geladen und dynamisch dargestellt. 2. Interaktive Aktionen: Ein Klick auf einen Button ruft die entsprechende Anlage direkt in FileMaker auf.
Vorteile dieses Ansatzes • Dynamik: Änderungen in den Datenbankeinträgen werden sofort berücksichtigt. • Erweiterbarkeit: PHP bietet dir volle Flexibilität in der Anzeige und Funktionalität. • Nutzerfreundlichkeit: Direkte Interaktion aus dem Web Viewer heraus.
Fazit
Die Kombination aus FileMaker und PHP eröffnet spannende Möglichkeiten, Einschränkungen des FileMaker-Portalsystems zu umgehen. Ob für die Darstellung von Listen, Diagrammen oder interaktiven Inhalten – mit Web Viewern kannst du deine FileMaker-Lösung erheblich erweitern. In meinem Beispielbild, wird aufgrund von übergebenen Aufträgen, per Maps API eine Reihe aller in Umgebung befindlichen Aufträge angezeigt. Ein Button öffnet noch die Maps Kartenansicht, ein Button ruft ein FileMaker Script auf, übergibt per Parameter die sortierten Aufträge an FileMaker. Weiterverarbeitung ist somit jederzeit möglich. Natürlich ist auch ein Klick auf die Auftrags ID möglich, es kann dann je nach Bedarf in FileMaker weiterverarbeitet werden.

Das neue Jahr läuft an.
Das neue Jahr läuft an.
Das neue Jahr hat begonnen und ich sitze wieder am Rechner. FileMaker über Google Maps API verbinden, Daten sortieren und anzeigen.

Scriptaufruf aus einem WebViewer
Wie starte ich ein FileMaker Script aus einer Webseite heraus.
Häufig haben wir das Problem, komplizierte Berechnungen ausführen zu müssen. Gerade wenn viele Datensätze betroffen sind, macht es Sinn, diese Berechnungen auszulagern. Wir übergeben einem PHP-Script alle benötigten Informationen und verarbeiten nur noch die Rückgabe. Das spart unübersichtliche FileMaker-Serverscripte die oft wie eine Blackbox sind. Kein Debugger, keine Zwischenmeldung über das Dialogfenster. Nur umständliche Rückgaben in Folge-Scripte die dann überprüft werden können. Es kann aber auch sein das wir ganz normale Funktionen z.B. Google Maps nutzen wollen. Wir können dann natürlich Daten anzeigen lassen. Adressdaten und andere Daten. Aber was wenn wir aus dem WebViewer dann den entsprechenden Datensatz aufrufen wollen? Dann müssen wir auf jeden Fall in der Lage sein, unsere Datenbank aufzurufen, ein Script und natürlich benötigen wir einen Parameter damit unser Script auch den entsprechenden Datensatz findet. So ein klassischer Aufruf läuft über die Angabe der Datenbank, auch wenn der WebViewer ja schon aus der Datenbank gestartet wurde, wir geben das Script und den Parameter an.

Gut zu sehen, der Datenbankname, der Scriptname und am Ende befindet sich der dynamische Parameter. In einem Folgepost werde ich einmal beschreiben wie wir Geo Daten übergeben, sortieren und am Ende eine Tourenplanung zurückbekommen die wir in FileMaker verarbeiten können.