yoh/symfony-jasper-report-bundle
Symfony bundle integrating JasperReports so you can compile .jrxml templates and generate PDF, HTML, XLS, CSV and other reports from PHP through simple Symfony services and configuration, with support for report parameters, database connections and output formats.
Know what you're actually getting. The bundle is thin glue: a Symfony DI extension, a config tree, and a service definition that instantiates Jaspersoft\Client\Client from jaspersoft/jrs-rest-php-client. It does not render reports, ship a .jrxml compiler, or talk to a local JVM. Every call is an HTTP round-trip to a running JasperReports Server (v6/v7-era REST v2 endpoint). If you don't have a JRS instance with an HTTP API exposed at /jasperserver, nothing in this package is useful to you.
If you're in Laravel, do not install the bundle. A Symfony 3 bundle needs Symfony\Component\HttpKernel\Bundle\Bundle, a kernel, and AppKernel-style registration. You will not get it to boot cleanly under Laravel's container. Instead pull the underlying client only:
composer require jaspersoft/jrs-rest-php-client
and bind it in AppServiceProvider:
// config/services.php
'jasper' => [
'url' => env('JASPER_URL', 'https://reports.internal:8443/jasperserver'),
'username' => env('JASPER_USER'),
'password' => env('JASPER_PASSWORD'),
'org' => env('JASPER_ORG', 'organization_1'),
],
// AppServiceProvider::register()
$this->app->singleton(\Jaspersoft\Client\Client::class, fn () => new \Jaspersoft\Client\Client(
config('services.jasper.url'),
config('services.jasper.username'),
config('services.jasper.password'),
config('services.jasper.org'),
));
If you are genuinely on Symfony 3, register Yoh\JasperReportBundle\JasperReportBundle in AppKernel::registerBundles() and add the jasper_report: block to config.yml. Don't guess the keys — open DependencyInjection/Configuration.php in vendor/yoh/symfony-jasper-report-bundle/ first, and Resources/config/services.yml second to find the service ID you'll inject. The docs are one line and the config keys are the only real API surface.
First use case — the 90% path, fetch a parameterised report as PDF bytes:
$pdf = $this->client->reportService()->runReport(
'/reports/operations/monthly_summary',
'pdf',
null,
null,
['month' => '2024-06', 'region' => ['EMEA', 'APAC']]
);
return response($pdf)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline; filename="monthly.pdf"');
Validate connectivity with a single reportService()->getReport('/reports/…') before wiring any UI — that isolates auth/URI problems from rendering problems.
Always resolve the client as a singleton. jrs-rest-php-client performs a login handshake and retains a session cookie; constructing a Client per call adds a full auth round-trip to every report and increases the chance of hitting session limits on the server. The bundle already registers it as a shared service; in Laravel use singleton(), and in tests swap it behind an interface.
Wrap it, don't sprinkle it. The client's API is wide (reportService, resourceService, jobService, userService, permissionService) and its exceptions are terse. Put a narrow interface of your own in front:
interface JasperGateway
{
public function pdf(string $uri, array $params = []): string;
public function inputControls(string $uri): array;
}
This is also the extension point: the bundle's service can be decorated in Symfony (decorates:) or rebound in Laravel, so you keep one place to add retries, timing, and logging.
Discover parameters before calling them. Hard-coded parameter names rot. reportService()->getInputControls($uri) returns the control descriptors (name, type, mandatory, allowed values); use it to drive select boxes and to validate before you POST. Passing an unknown parameter name silently yields a default-valued report rather than an error on many server builds.
Multi-value vs single-value parameters have different shapes. Single-select takes a scalar; multi-select takes an indexed array. Mandatory controls must be present at all times. ['region' => ['EMEA','APAC']] is correct; ['region' => 'EMEA,APAC'] is not.
Formats are server-side strings. pdf, xlsx, docx, csv, html, rtf, xml, odt. The server, not the client, decides what it can produce — an unsupported format surfaces as a 4xx or empty body, not a client-side exception.
Stream large reports rather than buffering. runReport returns the whole body as a PHP string. For anything over a few MB, hand it straight to a streamed response or write to disk:
return response()->streamDownload(function () use ($uri, $params) {
echo $this->jasper->pdf($uri, $params);
}, 'report.pdf');
Push slow reports to the queue. A cold runReport with joins across a warehouse can take tens of seconds; that's a request timeout waiting to happen. Queue the generation, store the artifact, and hand the user a signed URL. For scheduled/recurring output use jobService (create a report job with a trigger and output destination) rather than a cron that re-runs runReport.
Cache by content hash, not by URI. sha1($uri . json_encode($params)) is a safe key; parameter order matters in JSON but not semantically, so ksort the params first. Reports are usually deterministic for a given data snapshot — a short TTL (minutes) removes most load.
It is abandoned and pinned to a dead ecosystem. Last release 2017-05-19, Symfony 3 only, effectively zero dependents. Symfony 3 bundles do not boot on Symfony 4+, and the bundled client predates PHP 7.4/8 typed-property and deprecation churn. Budget for a fork or for pinning jaspersoft/jrs-rest-php-client at its last compatible tag and patching around each(), dynamic properties, and PHP 8.1+ deprecations yourself.
The organisation suffix is the number-one auth failure. JasperReports Server users are addressed as username|org_id; if the server has organisations enabled and you pass a bare jasperadmin, you get a 401 that looks like a bad password. Pass organization_1 (or your org ID) as the org parameter — the client appends the | — and confirm the account is actually in that org, not the root.
URL shape matters. The base URL should be scheme + host + port + context path, with no trailing slash and no /rest suffix: https://host:8443/jasperserver. Report URIs are absolute server paths beginning with /, e.g. /reports/samples/AllAccounts, and are not file paths — they're the resource tree inside JRS.
SSL verification bites internal deployments. Self-signed certificates behind a corporate CA will fail Guzzle's verify step. Fix it with a proper CA bundle (curl.cainfo / Guzzle verify option); disabling verification is the tempting shortcut and quietly makes credentials interceptable.
The client hides the HTTP detail. When a call fails you get RESTRequestException with a status code and little else. Temporarily log at the transport level (Guzzle middleware or a debug handler) to see the actual request body and the server's HTML error page — JRS often returns a stack trace in HTML with a 500, which is far more informative than the client's message.
Sessions and CSRF-ish state. REST v2 keeps a session; long-lived queue workers that hold a client for hours can hit an expired session and fail mid-batch. Re-instantiate (or re-login) inside long-running workers rather than reusing the singleton across the whole process lifetime. For php artisan queue:work processes, resolving the client per job is safer than injecting it into the worker.
Test against your actual JRS version. REST v2 was deprecated and removed in some JRS 7.x/8.x builds in favour of v2 with revised auth, and the older client's assumptions about response headers and login endpoints may not hold. A one-hour spike against the target instance — login, one getReport, one runReport with two parameters — will tell you in minutes whether this package is viable or whether you should write a small Guzzle wrapper against the documented REST endpoints yourself. For a Laravel codebase that's often the cheaper answer: 150 lines of HTTP client you control beats a nine-year-old Symfony bundle you can't upgrade.
How can I help you explore Laravel packages today?