goetas/apache-fop
PHP wrapper for Apache FOP that renders XSL-FO documents into PDF and other output formats. It offers a simple API to run FOP transformations and integrate FO-based PDF generation into Laravel or any PHP application.
This bundle is a thin Symfony wrapper around the Apache FOP command-line tool: you give it XSL-FO (either a file or a string), it shells out to fop/java -jar fop.jar, and you get a PDF back. Before writing any PHP, confirm the hard prerequisite is in place — a JVM and a working FOP install:
java -version
fop -version # or: java -jar /opt/fop/fop.jar -version
Then install and register the bundle. Note it targets Symfony 2/3 (last release May 2017), so on Symfony 4+ you will register it manually in config/bundles.php rather than AppKernel:
composer require goetas/apache-fop
// config/bundles.php
Goetas\ApacheFopBundle\ApacheFopBundle::class => ['all' => true],
Minimal configuration (check the bundle's DependencyInjection/Configuration.php for the exact key names in your version — it wires the FOP executable path, the Java binary, and a temp directory):
# config/packages/apache_fop.yaml
apache_fop:
fop_path: '%kernel.project_dir%/bin/fop'
# java_path: /usr/bin/java
# temp_dir: '%kernel.cache_dir%/fop'
First use case — render a hand-written .fo file to PDF from a controller or console command:
use Goetas\ApacheFopBundle\Fop\Fop;
public function render(Fop $fop): Response
{
$fop->generatePdf(
$this->getParameter('kernel.project_dir').'/var/invoice.fo',
'/tmp/invoice.pdf'
);
return $this->file('/tmp/invoice.pdf');
}
Where to look first: the bundle's Fop service class (it is small and shows exactly which process arguments are built), Resources/config/services.xml for the service ID to inject, and the Tests/ directory for a fixture .fo file you can render immediately to prove the toolchain works end-to-end.
XSLT → FO → PDF pipeline. The bundle does not transform your data into XSL-FO; that is your job. The standard workflow is: serialize your domain object to XML (Symfony Serializer or Twig-rendered XML), run it through an XSLT stylesheet (XSLTProcessor in ext-xsl), then hand the resulting .fo to FOP. Keep the three steps as separate services so each is testable without a JVM.
$xml = $this->serializer->serialize($invoice, 'xml');
$xsl = new \XSLTProcessor();
$xsl->importStylesheet(simplexml_load_file($template.'.xsl'));
$fo = $xsl->transformToXml(simplexml_load_string($xml));
$fop->generatePdf($this->writeTemp($fo), $pdfPath);
Stream vs. file. For anything beyond toy documents, generate to a temp file, then either return $this->file(...) or copy it into your storage layer. Streaming FOP's stdout directly is possible but makes error reporting much worse (exit code and stderr get tangled with binary output).
Cache aggressively. FOP is slow (JVM startup alone is 0.5–2s). Key a cache entry on hash(xml + xsl + config) and store the PDF in var/cache or your object store. In practice this turns a 2s request into a 5ms one and is the single biggest day-to-day win.
Move generation off the request thread. Wire a console command that consumes a queue (or a simple doctrine jobs table on old Symfony versions) and have controllers enqueue a job. Return a download link once the PDF exists.
Wrap it if you need control. The service is thin; decorating it to add timing, logging, and retry logic is cleaner than editing vendor code:
services:
App\Fop\LoggingFop:
decorates: 'goetas_apache_fop'
arguments: ['@App\Fop\LoggingFop.inner', '@logger']
Testing. In unit tests, mock/replace the Fop service so no JVM is needed. Keep one slow integration test (marked @group integration) that renders a known .fo file and asserts the output starts with %PDF-.
Process. It is essentially argument-building plus config. If it fights your Symfony version, dropping it and calling new Process(['fop', '-fo', $in, '-pdf', $out]) yourself is a legitimate, low-risk move — you lose nothing but a YAML block.$fop = $container->get('goetas_apache_fop')) or add an alias to Fop::class in your own services.yaml. The bundle's XML service definitions predate autoconfigure.fop.xconf, not your system font cache. Ship an explicit fop.xconf (with <fonts> pointing at bundled TTFs) and pass it via -c. Otherwise production renders boxes/tofu where dev rendered text.temp_dir is writable by the web user and is not a tmpfs so small it fills during large renders.-Xmx via JAVA_OPTS (or FOP's -X style options if supported by your wrapper) for big documents; set a Process timeout, because a malformed FO can hang FOP indefinitely..fo in dev catches unclosed blocks before they reach the JVM..xconf files (fonts, hyphenation, image resolution) and in your XSLT, both of which are plain files you control outside the bundle.How can I help you explore Laravel packages today?