Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
/.gitignore export-ignore
/Makefile export-ignore
/phpcs.xml.dist export-ignore
/phpstan.src.neon export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
4 changes: 2 additions & 2 deletions .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
php-version: 8.3
tools: composer:v2
coverage: none
- name: Install PHP dependencies
Expand All @@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
php: [ '8.3', '8.4', '8.5', '8.6' ]
name: Tests on PHP ${{ matrix.php }}
steps:
- name: Checkout source code
Expand Down
34 changes: 21 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Official PHP client for [Gotenberg](https://gotenberg.dev), a Docker-based API for converting documents to PDF. Two rules override everything else: **backward compatibility** (never rename or remove public methods or change method signatures without discussion) and **PSR compliance** (build on PSR-7, PSR-17, PSR-18, never introduce hard dependencies on specific HTTP implementations).

- PHP 8.1+ (see `composer.json`)
- PHP 8.3+ (see `composer.json`)
- Composer

## Quick start
Expand All @@ -21,7 +21,7 @@ Run all Composer commands inside this Docker container:
docker run --rm -it \
-e PHP_EXTENSION_XDEBUG=1 \
-v $(PWD):/usr/src/app/ \
thecodingmachine/php:8.1-v4-cli \
thecodingmachine/php:8.3-v4-cli \
bash -c "composer run all"
```

Expand All @@ -31,18 +31,20 @@ docker run --rm -it \
src/
Gotenberg.php Static factory (entry point)
ApiModule.php Base trait: URL, endpoint, headers, request building
MultipartFormDataModule.php Trait: multipart form data, webhooks, downloads
MultipartFormDataModule.php Trait: multipart form data, webhooks, downloads, archives
StagesModule.php Trait: stages, merge ordering (Index)
Stage.php Stage value object: one named constructor per Gotenberg stage
Profile.php, PaperSize.php… Backed enums for the parameters with a fixed set of values
Modules/
Chromium.php Chromium entry point (pdf/screenshot)
ChromiumPdf.php Chromium PDF conversion builder
ChromiumScreenshot.php Chromium screenshot builder
ChromiumMultipartFormDataModule.php Shared Chromium trait
ChromiumCookie.php Cookie value object
LibreOffice.php LibreOffice conversion builder
PdfEngines.php PDF engines builder (merge, split, convert)
Pdf.php PDF builder: pipe, single-stage routes, read routes
Exceptions/ Custom exceptions
Stream.php File stream wrapper
SplitMode.php Split mode enum
DownloadFrom.php Download-from value object
tests/
TestCase.php Base class with custom assertions
Expand All @@ -55,15 +57,19 @@ Start with `src/Gotenberg.php` for the public API surface, then follow into `src

### Fluent API

All configuration methods return `$this`. The builder pattern is the backbone of the public API. Keep it uniform.
All configuration methods return `$this`, typed `static`. The builder pattern is the backbone of the public API. Keep it uniform.

### Strict types

Every PHP file declares `strict_types=1`.

### Multipart form data

Configuration is stored in `$this->multipartFormData` (array of `[name, value]` or `[name, value, filename]` entries). Use `$this->formValue()` and `$this->formFile()` helpers.
Configuration is stored in `$this->multipartFormData` (array of `[name, value]` or `[name, value, filename]` entries). Use `$this->formValue()` and `$this->formFile()` helpers. `formValue()` sends booleans as `true`/`false`; encode lists and objects with `json_encode(..., JSON_THROW_ON_ERROR)`.

### Known values

A parameter with a fixed set of values takes a backed string enum, one file per enum in `src/` (`Profile`, `PaperSize`, `Magnification`…), with PascalCase cases. Send `$enum->value`; `json_encode` serializes enums inside lists and stage options on its own. A parameter that also takes free text, such as a page selection, takes a union (`string|Pages`).

### Coding standard

Expand All @@ -73,16 +79,18 @@ Doctrine coding standard enforced via phpcs/phpcbf. Follow it strictly. Use expl

For Chromium: add the method to `ChromiumPdf.php`, `ChromiumScreenshot.php`, or the shared trait `ChromiumMultipartFormDataModule.php` depending on scope. Call `$this->formValue('formFieldName', $value)` or `$this->formFile('formFieldName', $stream)`. Add a corresponding unit test with a data provider.

For LibreOffice or PdfEngines: add the method to the relevant module class. Same `formValue`/`formFile` pattern, same test requirement.
For LibreOffice or Pdf: add the method to the relevant module class. Same `formValue`/`formFile` pattern, same test requirement.

For a new stage: add a named constructor to `Stage.php`, its options keyed by their Gotenberg names, `null` for "keep the default", and a `Stream` for a file option. `stages()` and `Pdf::run()` pick it up; add a case to `StageTest`.

## Testing

Unit tests only (no integration tests, this is a client library). PHPUnit 10.5+. Base class `Gotenberg\Test\TestCase` provides custom assertions:
Unit tests only (no integration tests, this is a client library). PHPUnit 12.4+. Base class `Gotenberg\Test\TestCase` provides custom assertions:

- `assertContainsFormValue(string $name, string $expectedValue, RequestInterface $request)`: asserts a multipart form field has the expected value.
- `assertContainsFormFile(string $name, RequestInterface $request)`: asserts a multipart form file exists.
- `assertRequest(RequestInterface $request, string $endpoint, array $values, array $files)`: asserts the endpoint, the form values (`[name, value]`) and the form files (`[field, filename, content, content type]`).
- `assertContainsFormValue()` and `assertContainsFormFile()`: the single checks `assertRequest()` runs.

Tests follow a data provider pattern: a static method returns an array of test cases (description, `RequestInterface` built via the fluent API, expected form values/files), and the test method runs assertions against each.
Tests follow a data provider pattern: a static method yields test cases (a closure that builds the request or stage with the fluent API, then the expected form values and files), and the test method calls the closure and runs assertions against the result. Never build the request in the provider itself: PHPUnit runs providers before it starts recording coverage, so that code counts as untested. Wrap the closure in parentheses, `(static fn (): RequestInterface => ...)`, or phpcs misreads the array.

Form field names must match Gotenberg's API exactly. Cross-reference with the [Gotenberg documentation](https://gotenberg.dev).

Expand All @@ -106,6 +114,6 @@ Plan non-trivial changes before coding. Present what needs to change, why, which

### Commits

[Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`. Scope should match the module (e.g., `chromium`, `libreoffice`, `pdfengines`).
[Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`. Scope should match the module (e.g., `chromium`, `libreoffice`, `pdf`, `stage`).

Stage specific files. Never `git add -A` or `git add .`.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ help: ## Show the help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: bash
bash: ## Start a bash process in a PHP 8.1 Docker container
bash: ## Start a bash process in a PHP 8.3 Docker container
docker run --rm -it \
-e PHP_EXTENSION_XDEBUG=1 \
-v $(PWD):/usr/src/app/ \
thecodingmachine/php:8.1-v4-cli \
thecodingmachine/php:8.3-v4-cli \
bash
123 changes: 113 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ PDF files, and more!

| Gotenberg version | Client |
|---------------------|---------------------------------------------------------------------------------------------------|
| `8.x` **(current)** | `v2.x` **(current)** |
| `9.x` **(current)** | `v3.x` **(current)** |
| `8.x` | `v2.x` |
| `7.x` | `v1.x` |
| `6.x` | [thecodingmachine/gotenberg-php-client](https://github.com/thecodingmachine/gotenberg-php-client) |

Expand Down Expand Up @@ -58,7 +59,7 @@ $response = Gotenberg::send(

## Requirement

This packages requires [Gotenberg](https://gotenberg.dev), a containerized API for seamless PDF conversion.
This package requires PHP 8.3 or later, and [Gotenberg](https://gotenberg.dev) 9, a containerized API for seamless PDF conversion.

See the [installation guide](https://gotenberg.dev/docs/getting-started/installation) for more information.

Expand Down Expand Up @@ -92,7 +93,7 @@ use Gotenberg\Gotenberg;

Gotenberg::chromium($apiUrl);
Gotenberg::libreOffice($apiUrl);
Gotenberg::pdfEngines($apiUrl);
Gotenberg::pdf($apiUrl);
```

Each of these modules offers a variety of methods to populate a *multipart/form-data* request.
Expand All @@ -106,7 +107,7 @@ use Gotenberg\Gotenberg;
Gotenberg::chromium($apiUrl)
->pdf() // Or screenshot().
->singlePage() // Optional.
->url('https://my.url'));
->url('https://my.url');
```

> [!TIP]
Expand All @@ -132,14 +133,81 @@ Gotenberg::chromium($apiUrl)
Gotenberg::libreOffice($apiUrl)
->convert(new Stream('document.docx', $stream));

// Or upload a ZIP archive that Gotenberg unpacks.
Gotenberg::chromium($apiUrl)
->pdf()
->archives(Stream::path($pathToSiteZip))
->html(null);

// Or even tell Gotenberg to download the files for you.
Gotenberg::libreOffice($apiUrl)
->downloadFrom([
->downloadFrom(
new DownloadFrom('https://url.to.document.docx', ['MyHeader' => 'MyValue'])
])
)
->convert();
```

## Stages

A stage is one operation on a PDF: merge, split, stamp, redact, sign, make PDF/A, and so on. Gotenberg runs them in
order on the PDFs a Chromium or LibreOffice conversion makes:

```php
use Gotenberg\Gotenberg;
use Gotenberg\Profile;
use Gotenberg\Stage;
use Gotenberg\Stream;

Gotenberg::libreOffice($apiUrl)
->stages(
Stage::merge(),
Stage::stampText('DRAFT', x: 72, y: 72, fontSize: 24),
Stage::remediate(Profile::PdfA2b),
)
->convert(
Stream::path($pathToDocx),
Stream::path($pathToXlsx)
);
```

A stage that reads a file, such as an image to stamp or a certificate, takes a `Stream`: the client uploads it for you.
Options with a fixed set of values take an enum: `Profile`, `Font`, `Pages`, `PaperSize`, and so on.
With a `merge` stage, the documents are merged in the order of the arguments. See [Run stages](https://gotenberg.dev/docs/pdf/pipe)
for every stage and its order rules.

The `pdf` module runs stages on the PDFs you upload, reads them, and validates them:

```php
use Gotenberg\Gotenberg;
use Gotenberg\Profile;
use Gotenberg\Stage;
use Gotenberg\Stream;

// Runs a list of stages, through /forms/pdf/pipe.
Gotenberg::pdf($apiUrl)
->stages(
Stage::select('1-2'),
Stage::stampImage(Stream::path($pathToLogo), x: 72, y: 72, width: 100),
Stage::optimize(),
)
->pipe(Stream::path($pathToPdf));

// Runs one stage, through its own route: /forms/pdf/merge, /forms/pdf/sign, and so on.
Gotenberg::pdf($apiUrl)
->run(Stage::merge(), Stream::path($pathToPdf1), Stream::path($pathToPdf2));

Gotenberg::pdf($apiUrl)
->run(Stage::sign(Stream::path($pathToP12), certificatePassword: 'secret'), Stream::path($pathToPdf));

// Reads the PDFs; Gotenberg answers JSON, one entry per PDF.
Gotenberg::pdf($apiUrl)->info(Stream::path($pathToPdf));
Gotenberg::pdf($apiUrl)->metadata(Stream::path($pathToPdf));
Gotenberg::pdf($apiUrl)->bookmarks(Stream::path($pathToPdf));
Gotenberg::pdf($apiUrl)->attachments(Stream::path($pathToPdf));
Gotenberg::pdf($apiUrl)->form(null, Stream::path($pathToPdf));
Gotenberg::pdf($apiUrl)->validate([Profile::PdfA2b, Profile::PdfUa1], Stream::path($pathToPdf));
```

## Send a request to the API

After having created the HTTP request, you have two options:
Expand Down Expand Up @@ -203,8 +271,9 @@ $request = Gotenberg::chromium($apiUrl)
$filename = Gotenberg::save($request, '/path/to/saving/directory');
```

It returns the filename of the resulting file. By default, Gotenberg creates a *UUID* filename (i.e.,
`95cd9945-484f-4f89-8bdb-23dbdd0bdea9`) with either a `.zip` or a `.pdf` file extension (or image formats for screenshots).
It returns the filename of the resulting file, as the `Content-Disposition` header names it. Without
`outputFilename()`, Gotenberg names the result after its source (`report.docx` gives `report.docx.pdf`), or picks a
*UUID* for a web page or a ZIP archive.

You may also explicitly set the HTTP client:

Expand Down Expand Up @@ -277,5 +346,39 @@ try {
}
```

> [!NOTE]
> The `trace()` and `getGotenbergTrace()` methods are deprecated but still available for backward compatibility.
### Webhook

You may tell Gotenberg to upload the result to a webhook instead of answering with it. Gotenberg then sends a
`webhook.success` or `webhook.error` event to the events URL:

```php
use Gotenberg\Gotenberg;
use Gotenberg\WebhookMethod;

$request = Gotenberg::chromium($apiUrl)
->pdf()
->webhook('https://my.webhook.url', 'https://my.webhook.url/events')
->webhookMethod(WebhookMethod::Put) // Optional, default POST.
->webhookExtraHttpHeaders(['Authorization' => 'Bearer …']) // Optional.
->url('https://my.url');
```

## Upgrade from v2

The client follows the Gotenberg 9 API:

* `Gotenberg::pdfEngines()` becomes `Gotenberg::pdf()`: each route is a stage, run with `run()` or `stages()->pipe()`.
* The post-processing methods of the conversions (`merge()`, `split()`, `pdfa()`, `pdfua()`, `metadata()`, `flatten()`,
`encrypt()`, `allow*()`, `facturX()`, `embeds()`, `optimizeImages()`, `watermarking()`, `stamping()`, `rotating()`)
become stages. `SplitMode`, `FacturX` and `EmbedMetadata` give way to `Stage`.
* Fields with an engine twin take the `native` prefix: `nativePages()`, `nativeBookmarks()`, `nativeTaggedPdf()`,
`nativePdfa()`, `nativePdfua()`, `nativeQuality()`, `nativeWatermark*()`, and so on. `paperSize()` takes a
`PaperSize`, `paperDimensions()` the width and height.
* `webhook()` takes the events URL, which Gotenberg requires; the error webhook is gone.
* `downloadFrom()`, `cookies()` and the `failOn*()` methods are variadic, `emulatedMediaFeatures()` takes an array.
* `Stream` exposes `filename` and `stream` as readonly properties.
* Parameters with a fixed set of values take backed enums instead of strings (`Profile`, `PaperSize`, `SameSite`,
`WebhookMethod`…). `FacturX::CONFORMANCE_*` becomes `FacturXLevel`, `EmbedMetadata::RELATIONSHIP_*` becomes
`Relationship`. `nativePdfua()` takes no argument: LibreOffice only writes PDF/UA-1.
* JSON encoding errors throw `JsonException`.
* The deprecated `trace()` and `getGotenbergTrace()` are removed: use `correlationId()` and `getCorrelationId()`.
24 changes: 10 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
"xlsx",
"docx",
"pptx",
"pdftk",
"libreoffice",
"unoconv",
"wkhtmltopdf",
"unoconv",
"chrome",
"chromium",
"puppeteer"
"pdf/a",
"pdf/ua",
"factur-x"
],
"license": "MIT",
"authors": [
Expand All @@ -33,18 +31,17 @@
}
],
"require": {
"php": "^8.1|^8.2|^8.3|^8.4|^8.5",
"php": "^8.3",
"ext-json": "*",
"ext-mbstring": "*",
"guzzlehttp/psr7": "^1 || ^2.1 || ^3",
"php-http/discovery": "^1.14",
"guzzlehttp/psr7": "^2.7 || ^3.0",
"php-http/discovery": "^1.20",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0|^2.0"
"psr/http-message": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"doctrine/coding-standard": "^14.0",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.4",
"squizlabs/php_codesniffer": "^4.0"
},
"autoload": {
Expand All @@ -60,7 +57,7 @@
"scripts": {
"lint": [
"phpcs",
"phpstan analyse -l max src tests"
"phpstan analyse --no-progress"
],
"lint:fix": "phpcbf",
"tests": "XDEBUG_MODE=coverage phpunit --coverage-html coverage_html --coverage-clover coverage.xml",
Expand All @@ -74,7 +71,6 @@
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
}
Expand Down
Loading
Loading