poli-page / laravel
Laravel package for the Poli Page PDF rendering API
Requires
- php: ^8.3
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- poli-page/sdk: ^0.13.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0
Requires (Dev)
- dg/bypass-finals: ^1.5
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.6
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0 || ^11.0
- php-http/guzzle7-adapter: ^1.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-mockery: ^2.0
- phpunit/phpunit: ^11.5.50 || ^12.5.8
Suggests
- php-http/guzzle7-adapter: Exposes Guzzle (already in every Laravel app) as a PSR-18 client for the SDK's discovery.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-02 09:42:37 UTC
README
Render Poli Page documents as Laravel HTTP responses.
About
This package wraps the Poli Page PHP SDK as a Laravel-native integration: a service container singleton, a PoliPage Facade, a PoliPageResponseFactory for controller responses with the right PDF headers, an Artisan smoke-test command, and event bridges that forward the SDK's retry and error hooks onto Laravel's event dispatcher. You configure it through config/poli-page.php and return rendered PDFs the same way you return JSON or a Blade view.
When to use this:
- You want to return a generated PDF from a controller action with correct
Content-Type,Content-Disposition, and cache headers. - You want the Poli Page client autowired from the container and configured through
.env. - You want SDK retry and error events to flow through Laravel listeners alongside the rest of your app's events.
When not to:
- You need to generate PDFs without a remote API — Poli Page is a hosted service.
- You're working outside Laravel — pick the matching Poli Page package for your stack (Symfony, Rails, Django, FastAPI, Next.js, NestJS, Rocket).
Requirements
- PHP 8.3+
- Laravel 12.x or 13.x
poli-page/sdk0.13.x (Composer installs it with the package)- A Poli Page API key from app.poli.page
Install
composer require poli-page/laravel
Package auto-discovery registers PoliPageServiceProvider and the PoliPage Facade alias. Add your API key to .env:
POLI_PAGE_API_KEY=pp_sandbox_x
Each API key belongs to one environment of your organization and has the form pp_{environment}_{32 lowercase hex characters}. Start with your sandbox key:
| Environment | Key | Watermark | Counted |
|---|---|---|---|
sandbox (every org) |
pp_sandbox_… |
yes | no |
live (every org, deletable on Pro+) |
pp_live_… |
no | yes |
created on Pro+ (e.g. staging) |
pp_staging_… |
no | yes |
"Counted" means the render counts toward your organization's monthly PDF quota. The key picks the environment, and each render uses the copy of the project published in that environment, so there is no template version to pass.
The package only requires the key to be set. A key that does not match ^pp_(?=[a-z][a-z0-9-]{0,19}_)[a-z][a-z0-9]*(?:-[a-z0-9]+)*_[0-9a-f]{32}\z logs one warning through Laravel's logger when the client is first resolved (the key itself is never logged), and the API answers 401 if it is invalid.
Publish the config file when you need to override defaults:
php artisan vendor:publish --tag=poli-page-config
Verify the integration end-to-end from the CLI with a sandbox key; every organization has the getting-started project's welcome template in its sandbox environment:
php artisan poli-page:render \
--project=getting-started \
--template=welcome \
--data='{"name":"World"}' \
-o welcome.pdf
Quick start
Inject the client and the response factory into a controller, then return the rendered bytes.
// app/Http/Controllers/InvoiceController.php namespace App\Http\Controllers; use PoliPage\Laravel\Http\PoliPageResponseFactory; use PoliPage\PoliPage; use PoliPage\RenderInput; class InvoiceController { public function __construct( private readonly PoliPage $poliPage, private readonly PoliPageResponseFactory $responses, ) {} public function show(string $id) { $pdf = $this->poliPage->render->pdf(new RenderInput( project: 'invoices', template: 'default', data: ['invoice_id' => $id], )); return $this->responses->bytes($pdf, "invoice-{$id}.pdf"); } }
Configuration
| Option | Default | Description |
|---|---|---|
api_key |
env('POLI_PAGE_API_KEY') |
Your Poli Page API key, pp_{environment}_{32 hex}. Required; a malformed value logs one warning and the API decides. |
base_url |
SDK default | API endpoint. Override for staging or self-hosted. |
timeout |
SDK default | Timeout in seconds of each API call and of the PDF download (for pdfStream(), the longest wait for the next chunk). Must be between 0 and 600. |
retries.max_attempts |
SDK default | Automatic retries on 429 / 5xx. Must be between 0 and 10. |
retries.delay_seconds |
SDK default | Base delay between retries. Must be between 0 and 30. |
http_client |
null |
Container key of a PSR-18 ClientInterface binding. |
request_factory |
null |
Container key of a PSR-17 RequestFactoryInterface binding. |
stream_factory |
null |
Container key of a PSR-17 StreamFactoryInterface binding. |
logger |
null |
Container key of a PSR-3 LoggerInterface binding. |
on_retry |
null |
Container key of a callable. When set, replaces the default PoliPageRetrying event bridge. |
on_error |
null |
Container key of a callable. When set, replaces the default PoliPageErrored event bridge. |
error_handler |
env('POLI_PAGE_ERROR_HANDLER', true) |
Answers an uncaught PoliPageException with its toPayload() JSON body and status (see Errors). Set to false to let your app handle it. |
Every optional key is null by default and the SDK applies its own defaults. Override individual values through env vars or by editing the published config:
// config/poli-page.php return [ 'api_key' => env('POLI_PAGE_API_KEY'), 'base_url' => env('POLI_PAGE_BASE_URL'), 'timeout' => env('POLI_PAGE_TIMEOUT'), 'retries' => [ 'max_attempts' => env('POLI_PAGE_RETRY_MAX_ATTEMPTS'), 'delay_seconds' => env('POLI_PAGE_RETRY_DELAY_SECONDS'), ], ];
API at a glance
| Symbol | Purpose |
|---|---|
PoliPage\PoliPage |
SDK client singleton. Inject by FQCN; exposes render and documents. |
PoliPage\Laravel\Facades\PoliPage |
Facade alias. Use PoliPage::render()->pdf(...) outside constructors. |
PoliPage\Laravel\Http\PoliPageResponseFactory |
Builds Response / StreamedResponse / RedirectResponse with PDF, stored-document HTML preview (documents->preview()), and document-redirect headers. |
PoliPage\Laravel\Events\PoliPageRetrying |
Laravel event dispatched for each SDK retry attempt. Wraps the SDK's RetryEvent. |
PoliPage\Laravel\Events\PoliPageErrored |
Laravel event dispatched on terminal SDK errors. Wraps the underlying PoliPageException. |
php artisan poli-page:render |
End-to-end smoke-test command. Renders a template to a PDF file. |
Full reference: docs.poli.page.
Errors
Error handler
The package registers an error handler on Laravel's exception handler, on by default. A PoliPage\PoliPageException that your code does not catch becomes a JSON answer built from the SDK's toPayload():
{ "code": "TEMPLATE_NOT_FOUND", "message": "Template 'does-not-exist' not found in project 'getting-started'.", "status": 404, "requestId": "req_…" }
- The HTTP status is the payload's
status: the API's status (404, 429, 500, …),503for anetwork_error,504for atimeoutand502for aninvalid_response(a 2xx whose body is not JSON, usually from a proxy between your app and the API). An SDK error with no status (for exampleinvalid_options, orio_errorfromrenderToFile()) answers500, with"status": nullin the body. requestIdis the API'sx-request-id, ornullwhen there is none.- The
Content-Typeisapplication/json. Laravel still reports the exception to your log as usual. - For a streamed PDF, call
render->pdfStream()in the controller and pass its stream toPoliPageResponseFactory::stream(), which reads the first chunk before it returns. A render or download error then reaches the handler before any PDF header is sent. Calling the SDK inside your ownStreamedResponsecallback would commit a200 application/pdffirst.
To handle SDK errors yourself, turn it off in .env:
POLI_PAGE_ERROR_HANDLER=false
or with 'error_handler' => false in config/poli-page.php. A render() callback you register for PoliPageException in bootstrap/app.php also takes precedence when it returns a response.
Exception types
The SDK throws five families of exceptions. They all extend PoliPage\PoliPageException. Catch them where you want a different answer than the error handler's:
- Auth —
PoliPage\Exception\AuthenticationException. Invalid or missing API key (HTTP 401). - Rate limit —
PoliPage\Exception\RateLimitException. Rate limit exceeded (HTTP 429); honorRetry-After. - Request rejected —
PoliPage\Exception\BadRequestException. Template or data rejected by the API (HTTP 400). - Network / transport —
PoliPage\Exception\ConnectionException. Network or transport failure after the SDK's retry budget is exhausted. - Invalid response —
PoliPage\Exception\InvalidResponseException. A 2xx whose body is not JSON, usually from a proxy or captive portal; never retried.
use PoliPage\Exception\AuthenticationException; use PoliPage\Exception\BadRequestException; use PoliPage\Exception\ConnectionException; use PoliPage\Exception\RateLimitException; use PoliPage\PoliPageException; try { $pdf = $this->poliPage->render->pdf($input); } catch (AuthenticationException $e) { abort(500, 'Poli Page credentials rejected.'); } catch (RateLimitException $e) { return response('Try again shortly.', 503); } catch (BadRequestException $e) { return back()->withErrors(['template' => $e->getMessage()]); } catch (ConnectionException $e) { report($e); abort(502); } catch (PoliPageException $e) { report($e); abort(500); }
Example app
A runnable Laravel 13 app at example-app/ demonstrates every public SDK method through routes (GET /render/pdf, GET /documents/{id}, GET /errors/not-found, …), an interactive single-page dashboard at GET /, and an app:demo:render-to-file command.
cd example-app
composer install
php artisan serve
Going further
- Event bridges — listening to
PoliPageRetryingandPoliPageErroredfrom Laravel listeners (forthcomingdocs/events.md). - Streaming large PDFs — using
PoliPageResponseFactory::stream()for multi-megabyte documents (forthcomingdocs/streaming.md). - Stored documents — persisting a
document_idand serving presigned URLs throughdocumentRedirect()(forthcomingdocs/storage.md). - Testing — swapping the singleton with
PoliPage::swap($fake)or$this->app->instance()(forthcomingdocs/testing.md).
Compatibility
| Package | poli-page/sdk |
Laravel | PHP |
|---|---|---|---|
| 0.13.x | 0.13.x | 12.x / 13.x | 8.3 / 8.4 / 8.5 |
The package shares its major.minor with the Poli Page API and requires the SDK of the same minor (see RELEASING.md). It follows Laravel's own supported-versions window and tracks the latest stable PHP minor.
Contributing
composer install
composer ci # Pint, PHPStan and the unit tests
Contract tests
tests/Contract/ImplContractTest.php runs the integration cases of the Poli Page API contract (impl.* in poli-page/api-contract) against the example app, with its SDK pointed at a local mock API. CI runs them against the contract version pinned in .github/workflows/ci.yml. Locally:
POLI_CONTRACT=/path/to/contract.json vendor/bin/phpunit --testsuite=contract
Without POLI_CONTRACT the suite is skipped (it fails when CI is set). A case that must fail for now goes in tests/contract-skip.json with its reason.
License
Released under the MIT License.