From deef0d9bdb014840dfe03ed82fc5c89f72632770 Mon Sep 17 00:00:00 2001 From: Andrei Ghinea Date: Tue, 5 May 2026 22:28:57 +0300 Subject: [PATCH] Move from document/{docId}/pdf to /document/{docId}/content endpoint --- CHANGELOG.md | 10 +++++ README.md | 4 +- docs/document.md | 36 ++++++++++++++--- examples/casefile-e2e-demo.php | 10 +++++ src/Document.php | 74 ++++++++++++++++++++++++++++++++-- src/Entity.php | 29 +++++++++++++ 6 files changed, 153 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c826a2..5b8342f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Removed +## 3.2.0 - 2026-05-05 +### Added +- `Document::getContent(bool $signed = true)` — downloads document content as raw binary via the `/content` endpoint (no base64 JSON round-trip). Decrypted content only; the SDK does not expose `decrypt=false`. +- `Document::setFile(string $filePath)` — preferred setter; sends the API `file` field (PDF uploads today; naming aligns with upcoming non-PDF support). +- `Document::getFormat()` — returns the document format as reported by the API (typically `"pdf"` for current use). +- `Entity::getBinaryContent()` — low-level helper for fetching raw binary responses from asset endpoints. +### Changed +- `Document::getPdf()` now delegates to `getContent()` and is deprecated. The underlying endpoint changed from the deprecated `/pdf` (base64) to `/content` (binary). Return value is unchanged (raw binary string). +- `Document::setPdfFile()` is deprecated in favour of `setFile()`. + ## 3.1.1 - 2026-05-05 ### Added - Removed nesbot/carbon dependency; OAuth timestamps use native `DateTimeImmutable` / `time()`. diff --git a/README.md b/README.md index 94e5386..388aa25 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ relevant `requestIds` you find in the logs. The case file object is a container used to bundle documents and signers. Every signing process starts with a case file. * [Documents][document-docs] - The document object represents (and contains) the actual PDF document. + The document object represents the document file. Use `setFile()` for the upload path (PDF supported today; same API field as future formats). `setPdfFile()` remains for compatibility. * [Signature lines][signature-line-docs] Every signable document must have at least one signature line. Think of it as the dashed line that people used to sign using a pen. @@ -233,7 +233,7 @@ CaseFile::persist($myCaseFile); // Create a new signable document in this case file $myDocument = new Document($myCaseFile); $myDocument->setTitle('Demo document'); -$myDocument->setPdfFile('/path/to/pdfFile'); +$myDocument->setFile('/path/to/document.pdf'); $myDocument->makeSignable(); Document::persist($myDocument); diff --git a/docs/document.md b/docs/document.md index f025b37..367fd91 100644 --- a/docs/document.md +++ b/docs/document.md @@ -1,5 +1,7 @@ # Documents -The document object represents (and contains) the actual PDF document. A document can either be a signable document or an unsignable _annex_. Note that document are always linked to a case file and can't exist on their own. +The document object represents (and contains) the document file. **Creating documents via the SDK currently supports PDF only** (signable document or annex). The API uses a generic `file` field; `setFile()` is the preferred way to supply the path, while `setPdfFile()` remains for compatibility. + +A document can either be a signable document or an unsignable _annex_. Documents are always linked to a case file and can't exist on their own. ## Creating a document Creating a document requires that you have a case file first since a document can't exist on its own. The case file must be passed to the _Document_ constructor and the document will be linked to the case file. @@ -13,8 +15,8 @@ $myDocument = new Document($myCaseFile); // Set the document title $myDocument->setTitle('My brand new document'); -// Add the actual PDF document -$myDocument->setPdfFile('/path/to/pdfFile'); +// Add the PDF file (generic `file` field on the API) +$myDocument->setFile('/path/to/document.pdf'); // Make the document signable $myDocument->makeSignable(); @@ -23,6 +25,8 @@ $myDocument->makeSignable(); Document::persist($myDocument); ``` +> **Note:** `setPdfFile()` still works but is deprecated. Use `setFile()` for new code — same behaviour today (PDF), aligned with the API’s `file` parameter for future formats. + ## Retrieve existing documents There is several ways to retrieve document from Penneo. Available methods for retrieving documents are: @@ -68,8 +72,28 @@ $myDocuments = Document::findBy( ); ``` -## Retrieving the signed document -When the signing process is completed (when __getStatus()__ returns "completed"), the signed PDF document can be retrieved by calling the __getPdf()__ method on the document object in question. +## Downloading the document content +When the signing process is completed (when __getStatus()__ returns "completed"), the signed document can be downloaded by calling __getContent()__: + +```php +// Download the signed document (binary) +$binary = $myDocument->getContent(); +file_put_contents('signed-document.pdf', $binary); + +// Download the unsigned version +$unsigned = $myDocument->getContent(false); + +// Document format as returned by the API (typically "pdf" today) +$format = $myDocument->getFormat(); +``` + +The `getContent()` method accepts one optional parameter: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `$signed` | bool | `true` | Return the signed version when available | + +> **Note:** `getPdf()` still works but is deprecated. Use `getContent()` for new code — raw binary from `/content` without base64 JSON. ## Retrieving linked objects A signable document contains signature lines. These objects can be retrieved using the following methods: @@ -98,5 +122,7 @@ Returns the date and time when the document was last modified as a _DateTime_ ob Returns the date and time when the document signing process was finalized as a _DateTime_ object. * __getDocumentId()__ Returns the unique ID that is stamped on every page in the document for identification purposes. +* __getFormat()__ +Returns the document format as a string (typically `"pdf"`). Returns `null` for locally created documents that have not been persisted yet. * __getOptions()__ Returns the option values assigned to the document. diff --git a/examples/casefile-e2e-demo.php b/examples/casefile-e2e-demo.php index 72f4584..d4bb8c9 100644 --- a/examples/casefile-e2e-demo.php +++ b/examples/casefile-e2e-demo.php @@ -259,6 +259,16 @@ function printStep(string $label): void $link = $signingRequest->getLink(); echo "\nSigning link:\n{$link}\n"; + printStep('Download document content (Document::getContent)'); + try { + $binary = $document->getContent(); + echo 'Downloaded document binary: ' . strlen($binary) . " bytes\n"; + $format = $document->getFormat(); + echo 'Document format: ' . ($format ?? 'n/a') . PHP_EOL; + } catch (Throwable $e) { + echo '(skip content download: ' . $e->getMessage() . ')' . PHP_EOL; + } + try { $log = $signer->getEventLog(); echo 'Signer event log entries (LogEntry): ' . count($log) . PHP_EOL; diff --git a/src/Document.php b/src/Document.php index 29af8d3..c7edb85 100644 --- a/src/Document.php +++ b/src/Document.php @@ -12,6 +12,7 @@ class Document extends Entity 'options', 'type', '@pdfFile', + '@file', 'documentTypeId' => 'documentType->getId' ), 'update' => array( @@ -31,7 +32,9 @@ class Document extends Entity protected $modified; protected $completed; protected $status; + protected $format; protected $pdfFile; + protected $file; protected $caseFile; protected $type = 'attachment'; @@ -85,10 +88,36 @@ public function findSignatureLine($id) return parent::findLinkedEntity($this, SignatureLine::class, $id); } - public function getPdf() + /** + * Download the document content as raw binary via GET .../content. + * + * Upload is PDF-only in the API today; this returns whatever bytes the API stores for the document. + * Content is always returned decrypted (API default); the encrypted storage blob is not exposed via the SDK. + * + * @param bool $signed Get the signed version when available (default: true). Pass false for the original document. + * + * @return string Raw binary content + */ + public function getContent(bool $signed = true): string { - $data = parent::getAssets($this, 'pdf'); - return base64_decode($data[0]); + $params = []; + if (!$signed) { + $params['signed'] = 'false'; + } + + return parent::getBinaryContent($this, 'content', $params); + } + + /** + * @deprecated Use getContent() instead. + * + * @param bool $signed Get the signed version when available (default: true). Pass false for the original document. + * + * @return string Raw binary PDF content + */ + public function getPdf(bool $signed = true): string + { + return $this->getContent($signed); } public function makeSignable() @@ -96,11 +125,50 @@ public function makeSignable() $this->type = 'signable'; } + /** + * Set the document file from a local path. The file is base64-encoded and sent as the API `file` + * field (alongside the legacy `pdfFile` from setPdfFile()). Only PDF uploads are supported by + * the API today; this naming prepares for additional formats without breaking compatibility. + * + * @param string $filePath Path to the local PDF file + */ + public function setFile(string $filePath): void + { + $this->file = $filePath; + } + + /** + * @deprecated Use setFile() instead. + */ public function setPdfFile($pdfFile) { $this->pdfFile = $pdfFile; } + /** + * Return the document format as reported by the API. + * + * In practice this is `"pdf"` for current integrations. Other numeric format codes from the API + * are mapped when present; additional format names may apply as the API evolves. + * + * @return string|null + */ + public function getFormat(): ?string + { + if ($this->format === null) { + return null; + } + + $formats = [ + 1 => 'pdf', + 2 => 'xml', + 3 => 'xhtml', + 4 => 'zip', + ]; + + return $formats[$this->format] ?? (string) $this->format; + } + public function getDocumentId() { return $this->documentId; diff --git a/src/Entity.php b/src/Entity.php index 88aa700..b400c62 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -299,6 +299,35 @@ public static function getAssets(Entity $parent, $assetName) return $result; } + /** + * Fetch raw binary content from an asset endpoint. + * + * Unlike getAssets(), the response body is returned as-is without JSON decoding. + * This requires that the server returns binary (i.e. the request must NOT include + * an Accept: application/json header, which is already the SDK default). + * + * @param Entity $parent + * @param string $assetPath Sub-path appended after the entity URL (e.g. "content") + * @param array $queryParams Associative array of query-string parameters + * + * @return string Raw binary content + * @throws Exception + */ + public static function getBinaryContent(Entity $parent, string $assetPath, array $queryParams = []): string + { + $url = $parent->getRelativeUrl() . '/' . $parent->getId() . '/' . $assetPath; + if ($queryParams) { + $url .= '?' . http_build_query($queryParams); + } + + $response = ApiConnector::callServer($url); + if (!$response) { + throw new Exception('Penneo: Internal problem encountered fetching content: ' . $assetPath); + } + + return $response->getBody()->getContents(); + } + public static function callAction(Entity $parent, string $actionName, string $method = 'patch', $data = null): bool { $url = $parent->getRelativeUrl() . '/' . $parent->getId() . '/' . $actionName;