Tutorials / PHP

PHP Factur-X Library Alternative (API)

The robust alternative to fragile PHP Factur-X libraries. Handle strict E-Rechnung and Factur-X compliance in Symfony, Laravel, or WordPress without complex PDF or XML dependencies.

Prerequisites

  • PHP 8.1+
  • guzzlehttp/guzzle client

Full Implementation

post('http://localhost:8000/v1/convert', [
        'multipart' => [
            [
                'name'     => 'pdf',
                'contents' => fopen('input.pdf', 'r'),
                'filename' => 'input.pdf'
            ],
            [
                'name'     => 'metadata',
                'contents' => json_encode([
                    'invoice_number' => 'PHP-999',
                    'seller' => ['name' => 'PHP Solutions'],
                    'totals' => ['net_amount' => 120.50]
                ])
            ]
        ]
    ]);

    file_put_contents('factur-x-final.pdf', $response->getBody());
    echo "Success: File generated.";

} catch (\Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>

PHP Libraries vs Docker API

Libraries like horstoeko/zugferd generate CII XML but skip the Schematron validation required for legal compliance. Libraries wrapping tcpdf or Ghostscript for PDF/A-3 embedding consume large amounts of RAM and break on edge cases. The Docker API approach isolates all complexity:

Approach Schematron Composer deps PDF/A-3
horstoeko/zugferd None fpdf, dompdf, setasign Partial
atgp/factur-x None smalot/pdfparser Partial
Factur-X Engine API Yes (Saxon-HE) guzzlehttp/guzzle only Yes (VeraPDF)

Use Case: Symfony Service with HttpClient

Use Symfony's native HttpClientInterface — no Guzzle required. Inject the service and call the engine from a Symfony command or event listener:

<?php
namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class InvoiceService
{
    public function __construct(
        private HttpClientInterface $http,
        private string $engineUrl = 'http://facturx-engine:8000'
    ) {}

    public function generateFacturX(string $pdfPath, array $metadata): string
    {
        $response = $this->http->request('POST', $this->engineUrl . '/v1/convert', [
            'body' => [
                'pdf'      => fopen($pdfPath, 'r'),
                'metadata' => json_encode($metadata),
            ],
        ]);

        if ($response->getStatusCode() !== 200) {
            throw new \RuntimeException('Engine error: ' . $response->getContent(false));
        }

        return $response->getContent(); // binary PDF string
    }
}

Use Case: Laravel Queued Job for Batch Generation

Dispatch invoice generation as a Laravel queued job after payment confirmation. Store the resulting PDF in S3 via Laravel's filesystem abstraction:

<?php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class GenerateFacturXInvoice implements ShouldQueue
{
    use Queueable;

    public function __construct(
        private int $orderId,
        private string $pdfPath,
        private array $metadata
    ) {}

    public function handle(): void
    {
        $response = Http::attach('pdf', file_get_contents($this->pdfPath), 'invoice.pdf')
            ->post(config('services.facturx.url') . '/v1/convert', [
                'metadata' => json_encode($this->metadata),
            ]);

        $response->throw(); // throws on 4xx/5xx

        Storage::disk('s3')->put(
            "invoices/{$this->orderId}.pdf",
            $response->body()
        );
    }
}

Use Case: Validate Supplier Invoices Before Archiving

When receiving e-invoices from suppliers via EDI or email attachment, validate compliance before injecting into your accounting system. Non-compliant invoices (broken VAT math, missing IBAN, wrong profile) are rejected with a structured error report:

<?php
use GuzzleHttp\Client;

function validateInvoice(string $filePath): array
{
    $client = new Client();
    $response = $client->post('http://localhost:8000/v1/validate', [
        'multipart' => [[
            'name'     => 'file',
            'contents' => fopen($filePath, 'r'),
            'filename' => basename($filePath),
        ]],
    ]);

    $result = json_decode($response->getBody(), true);

    if (!$result['is_valid']) {
        foreach ($result['errors'] as $error) {
            error_log("[{$error['code']}] {$error['message']}");
        }
        throw new \RuntimeException('Invoice failed EN 16931 validation');
    }

    return $result;
}

Frequently Asked Questions

Can I use plain curl instead of Guzzle?

Yes. PHP's native curl_setopt with CURLOPT_POSTFIELDS set to a multipart array works perfectly. The Guzzle example is shown for readability but any HTTP client that supports multipart/form-data works — including Symfony HttpClient and Laravel's Http facade.

How do I stream the PDF response to the browser in PHP?

Set the appropriate headers and echo the binary content: header('Content-Type: application/pdf'), header('Content-Disposition: attachment; filename="invoice.pdf"'), then echo $response->getBody(). In Laravel use return response($response->body(), 200, ['Content-Type' => 'application/pdf']).

Does the engine work with PHP running on shared hosting?

The Factur-X Engine Docker container runs separately from your PHP environment — it can be hosted on any VPS, cloud VM, or local server. Your PHP app on shared hosting simply makes HTTP requests to it. The engine requires at minimum 0.5 vCPU and 512 MB RAM on the host where it runs.

See Also