Skip to content

feat(api)!: pdf()/pdfContent(), typed sub-resource filters, parent id as create() argument - #71

Merged
justpilot merged 1 commit into
4.xfrom
feat/4x-api-ergonomics
Oct 25, 2026
Merged

justpilot merged 1 commit into
4.xfrom
feat/4x-api-ergonomics

Conversation

@justpilot

Copy link
Copy Markdown
Owner

Fixes the API rough edges found while rewriting the examples against 4.x.

1. pdf() / pdfContent() instead of pdf(..., rawPdf: true)

All seven document APIs (invoices, offers, confirmations, credit notes, delivery notes, letters, reminders):

  • pdf(int $id, ?PdfType $type = null): XPdf: JSON, metadata plus base64
  • pdfContent(int $id, ?PdfType $type = null): string: raw bytes (format=pdf, checked via getPdf())

Both carry #[NoDiscard]. Why two methods instead of a conditional return type: the flag picks a different wire format and a different return type. A boolean flag argument is a smell, and a PHPStan conditional type only helps callers that run PHPStan; plain PHP and IDEs would still see XPdf|string. Shared query building is in AbstractApi::pdfQuery().

2. Typed sorting and pagination for sub-resources

The spec documents filter fields only for payments (from, to, type, user_id) and property values ({parent}_property_id, value). So:

  • Generated typed filters (definition key filter, parent field omitted because it's an argument): InvoicePaymentFilter, CreditNotePaymentFilter, IncomingPaymentFilter (type as list<PaymentType>), and Client/Article/Supplier/IncomingPropertyValueFilter. PaymentsApi/PropertyValuesApi get a TFilter template and reject a filter of another resource at runtime (InvalidArgumentException), like they already do for options.
  • One handwritten SubResourceFilter (orderBy, page, perPage, extra) for items, comments, tags, reminder items, recurring e-mail receivers and property definitions. I didn't generate ~35 near-identical per-resource classes that would only hold sort and paging fields.
  • Every sub-resource kind now has list(), listPage() and iterateAll(). In the sandbox, /invoice-items and /invoice-comments return @page/@per_page/@total and honour per_page/order_by.
  • Comments: list(int $parentId, ?array $actionKeys = null, SubResourceFilter|array $filters = []). actionKeys stays second so existing positional calls don't silently land in $filters.
  • The parent ID argument wins over a filter key with the same name. Plain arrays keep working everywhere.

3. Parent ID as the first argument of create()

Convention: the parent ID is always an argument, never an options field. create(int $parentId, $options) now applies to payments, comments, tags, property values and recurring e-mail receivers, like ItemsApi already did.

Why this one and not "always in the options":

  • It mirrors list($parentId), listPage($parentId) and iterateAll($parentId). You address the parent the same way for reads and writes.
  • It matches the access path ($billomat->invoices->payments->create($invoiceId, …)), and the options only describe the sub-resource itself, so one options object can be reused for several parents.
  • Items, the most used sub-resource, already worked this way, so there are fewer breaks in total than moving item IDs into options.

The parent fields are readOnly in the definitions (still in the read models) and AbstractSubResourceApi::createFor() adds them to the payload. ItemsApi changes are limited to list/listPage/iterateAll and create (now via createFor()); update() and *ItemUpdateOptions from #70 are untouched.

4. delete() on documents

The docs claimed delete() only works in DRAFT. Sandbox check (9 requests, throwaway client + invoice, cleaned up): I created a draft invoice with two items and called complete(), which set the status to OVERDUE. delete() returned 200, and get() afterwards returned null. Together with the earlier runs (completed, cancelled, won and lost documents of every type), the doc pages now say delete() works in any status. Where cancel() exists (invoices, offers, confirmations, credit notes, reminders), they recommend it for completed documents (bookkeeping/GoBD, gaps in the number sequence). Letters and delivery notes have no cancel() since #66, so their pages don't suggest one.

5. Rector set

Two new rules in rector/upgrade-4.0.php, with fixture tests and the end-to-end fixture extended:

  • PdfContentRector: pdf($id, $type, rawPdf: true) / pdf($id, $type, true) → pdfContent($id, $type). rawPdf: false is dropped, and a runtime flag is left alone.
  • ParentIdToCreateArgumentRector: create(new InvoicePaymentCreateOptions(invoiceId: $id, …)) (named or first positional) → create($id, new InvoicePaymentCreateOptions(…)) for all payment/comment/tag/property-value/e-mail-receiver options. Options built in a variable stay manual and are listed in the UPGRADE "What stays manual" section.

Also updated

Unit tests (new sub-resource list/page/iterate, filter mismatch and create payload tests, pdf() JSON and pdfContent() type tests), examples (04-payments.php uses InvoicePaymentFilter + OrderBy), the docs on all affected resource pages plus pagination/filtering/http-layer/error-handling, README quickstart, AGENTS.md, CHANGELOG [Unreleased] and UPGRADE-4.0.md (before/after sections for all three BC breaks, plus the Rector table).

Checks

composer lint, analyse, refactor:dry, generate:check and test (3596 tests) pass. composer mutate -- --git-diff-lines --git-diff-base=origin/4.x … gives a covered MSI of 98 %.

…parent id as create() argument

- pdf() always returns the *Pdf model, pdfContent() the raw bytes; the bool $rawPdf flag is gone.
- Sub-resource lists take SubResourceFilter (sorting, pagination) or the generated *PaymentFilter/*PropertyValueFilter, and all have listPage()/iterateAll().
- Payments, comments, tags, property values and recurring e-mail receivers take the parent id as the first argument of create(), like items; the parent id field is removed from their create options.
- Rector set: PdfContentRector and ParentIdToCreateArgumentRector.
- Docs: delete() works on documents in any status (checked in the sandbox).
@justpilot
justpilot merged commit 8687fd4 into 4.x Oct 25, 2026
8 checks passed
@justpilot
justpilot deleted the feat/4x-api-ergonomics branch October 25, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant