Skip to content

PDF-12283 Support PHP 8.3–8.5 and Laravel 13 apps; fix client for league/oauth2-client 2.x - #72

Merged
websviewer merged 5 commits into
masterfrom
PDF-12283-php-8.5-laravel-13-compat
Sep 26, 2026
Merged

websviewer merged 5 commits into
masterfrom
PDF-12283-php-8.5-laravel-13-compat

Conversation

@websviewer

@websviewer websviewer commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Jira: PDF-12283

TL;DR

  • The package does not depend on Laravel; its dependencies do not conflict with Laravel 13.
  • The client has been broken since v3.1.0 (2022). Every request, including the token request, fails with league/oauth2-client 2.x (Call to undefined method PDFfiller::sendRequest()), and with guzzlehttp/psr7 2.x (Uri::resolve() removed). This PR fixes it.
  • Found while fixing it: the Bearer token was sent to any host passed as an absolute URL to apiCall(). Now refused.
  • PHP 8.1–8.5 deprecations fixed; first test suite (28 tests) and CI added.

Why it was broken

v3.1.0 moved league/oauth2-client from ^1.0 to 2.6.1 without adapting PDFfiller.php:

  1. getResponse() calls sendRequest() — league 1.x only.
  2. league 2.x getAccessToken() → getParsedResponse() → getResponse() expects a ResponseInterface; ours returns a parsed array.
  3. psr7 2.x removed Uri::resolve(), stream_for(), modify_request().

The new tests run against master code: 18/20 provider tests and 5/5 model tests fail (plus a fatal in FillableFieldsList, see below).

Internal usage: pdffiller/pdffiller and pdffiller/supervisor lock v3.2.0 but no code references PDFfiller\OAuth2\... (dead dependency). The package is public (~2.4k downloads/month on Packagist).

Changes (5 commits)

1. PHP 8.1–8.5 deprecations + fatal (no public signature changes for subclasses)

  • DTO\FillableFieldsList::offsetExists(): add : bool like the parent ListObject. Without it the class fails to load on any PHP 7+ since v3.0.0.
  • Core\ListObject, DTO\FillableFieldsList: #[\ReturnTypeWillChange] on ArrayAccess/Iterator methods (real return types would break subclasses in consumer code).
  • Core\AbstractObject: #[\AllowDynamicProperties] (PHP 8.2) — DTOs store attributes as properties.
  • 6 implicitly nullable parameters → ?Type (PHP 8.4).

2. HTTP layer for league 2.x / psr7 2.x (PDFfiller.php)

  • sendRequest() restored with the league 1.x behaviour: return 4xx/5xx responses instead of throwing, so checkResponse() handles them.
  • getParsedResponse() overridden to use our getResponse() (parse + checkResponse()).
  • UriResolver::resolve(), Utils::streamFor(), Utils::modifyRequest() (all available since psr7 1.7).
  • http_build_query(..., '', ...) instead of null.

3. Security: token stays on urlApiDomain

  • getAuthenticatedRequest() now refuses a resolved URL whose scheme, host or port differs from urlApiDomain (InvalidRequestException), before anything is sent.

4. Dependencies, tests, README

Package Before After
php >=7.0 >=8.1
league/oauth2-client ^2.6 ^2.8 (PHP 8.3/8.4 support, nullable fixes, Guzzle ≥ 7.4.5 for security fixes)
guzzlehttp/guzzle ^7.4 unchanged
oodle/inflect ^0.2.0 unchanged — dormant since 2015 but works on PHP 8.5 without notices (one call, pluralize())
phpunit/phpunit (dev) — ^10.5 || ^11.5 || ^12.5 || ^13.0
nesbot/carbon (dev) ^2.31 ^2.31 || ^3.0
  • 28 tests with Guzzle MockHandler (no network); phpunit.xml.dist fails on warnings, notices and deprecations.

5. CI: .github/workflows/tests.yml — PHP 8.3 / 8.4 / 8.5 (highest) + 8.1 (lowest). ubuntu-latest, not the self-hosted runner, because the repo is public (fork PRs must not run on self-hosted runners). publish-on-tag.yml unchanged.

Test results (local, official php:*-cli Docker images)

PHP deps league psr7 tests PHP notices when loading all src/
8.1 lowest 2.8.0 1.9.0 28 ✅ 0
8.1 highest 2.9.1 2.13 28 ✅ 0
8.2 highest 2.9.1 2.13 28 ✅ 0
8.3 highest 2.9.1 2.13 28 ✅ 0
8.3 psr7 1.x 2.9.1 1.9.1 28 ✅ 0
8.4 highest 2.9.1 2.13 28 ✅ 0
8.5 highest 2.9.1 2.13 28 ✅ 0
  • composer require next to laravel/framework 13.33 on PHP 8.5: resolves; token → list → get → create → delete → error flow works against a mock, 0 PHP notices.

Auth review

What the credential is and who reads it. AccessToken (access_token, refresh_token, expires) from urlAccessToken, created by league's GenericProvider from the token response. It is only read by the PDFfiller REST API through Authorization: Bearer. No token format or storage change in this PR.

Guard vs. our overrides. PDFfiller::getResponse() returns an array, not a ResponseInterface. In league 2.8/2.9 the only caller that expects a response object is getParsedResponse(), now overridden; getAccessToken() and fetchResourceOwnerDetails() both go through it, so checkResponse() always runs. Swallowing BadResponseException matches league's own getParsedResponse() and league 1.x sendRequest().

Refused cases (tests): OAuth error 401 (IdentityProviderException), errors payload 400 (ResponseException), 500 without access_token (InvalidArgumentException), 502 HTML (UnexpectedValueException), API call without a token (not sent). After a refused token request no token is stored. Token not sent to: other host, protocol-relative host, http:// downgrade, other port, host suffix (api.test.evil.test). Allowed: relative paths, absolute URLs on urlApiDomain (case-insensitive host). Model ids like //evil.test/x stay on the API host.

Exposure window.

  • v3.1.0–v3.2.0: no token could be issued or used through this library, so there is nothing to revoke from these builds.
  • v3.0.x (league 1.x, 2018–2022): an absolute URL passed to apiCall()/queryApiCall()/… would receive the Bearer token. The library itself never passes absolute URLs (all models use relative path segments), so this only matters for consumer code that passed URLs from untrusted input.

Upstream notes checked: league 2.8 "send scopes with access token request" — scopes are only sent when configured; none by default here.

Not changed / follow-ups

  • examples/bootstrap/init.php uses the phpdotenv v3 API (new Dotenv\Dotenv(...)) while ^5.4 is allowed — examples only.
  • oodle/inflect is unmaintained; it can be replaced by a small internal pluralizer later.
  • Release: minimum PHP moves from 7.0 to 8.1 and the new URL check refuses cross-host URLs, so a major version (v4.0.0) is suggested.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a parsed-response method for API requests.
    • API requests now reject URLs outside the configured API domain before sending credentials.
  • Bug Fixes
    • API error responses are returned for response handling instead of being propagated as transport exceptions.
    • Form parameters are encoded consistently.
  • Compatibility
    • Updated the minimum supported PHP version to 8.1 and broadened compatibility with current PHP and development dependency versions.
  • Tests
    • Added automated test coverage for API requests, models, and collection behavior, with CI checks across multiple PHP versions.

websviewer and others added 5 commits September 26, 2026 21:47
…st fatal

- FillableFieldsList::offsetExists(): add `: bool` like ListObject. Without
  it the class fails to load on any PHP 7+ ("must be compatible").
- ListObject / FillableFieldsList: #[\ReturnTypeWillChange] on ArrayAccess
  and Iterator methods (PHP 8.1). No signature change for subclasses.
- AbstractObject: #[\AllowDynamicProperties] (PHP 8.2), DTOs store
  attributes as properties.
- explicit ?Type for 6 implicitly nullable parameters (PHP 8.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d guzzlehttp/psr7 2.x

Since v3.1.0 (league/oauth2-client 1.x -> 2.x) every request failed:
- getResponse() called sendRequest(), which only exists in league 1.x.
  Add a protected sendRequest() with the same behaviour (return 4xx/5xx
  responses instead of throwing, so checkResponse() handles them).
- league 2.x getAccessToken() goes through getParsedResponse(), which
  expects getResponse() to return a ResponseInterface. Override
  getParsedResponse() to use our getResponse() (parse + checkResponse).
- psr7 2.x removed Uri::resolve(), stream_for() and modify_request():
  use UriResolver::resolve(), Utils::streamFor(), Utils::modifyRequest()
  (available since psr7 1.7).
- http_build_query(): '' instead of null (PHP 8.1 deprecation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getAuthenticatedRequest() resolves the URL against urlApiDomain. An
absolute or protocol-relative URL passed to apiCall() replaced the host,
so the Bearer token was sent to that host (also on an http:// downgrade
or another port). Refuse such requests with InvalidRequestException.
Relative paths and absolute URLs on urlApiDomain still work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- php >=7.0 -> >=8.1
- league/oauth2-client ^2.6 -> ^2.8 (PHP 8.3/8.4 support, nullable fixes,
  Guzzle >= 7.4.5 for security fixes)
- dev: phpunit ^10.5 || ^11.5 || ^12.5 || ^13.0; carbon ^2.31 || ^3.0
- add PHPUnit tests (Guzzle MockHandler, no network): token grants and
  refusals, API calls, request options, models, lists, enums, and the
  urlApiDomain guard; fail on warnings, notices and deprecations
- README: PHP >= 8.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub-hosted runner, because the repository is public.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: cd40bff8-b058-43fe-b7e1-19f7b04c1323

📥 Commits

Reviewing files that changed from the base of the PR and between bae5503 and 7687e0f.

📒 Files selected for processing (18)
  • .github/workflows/tests.yml
  • .gitignore
  • README.md
  • composer.json
  • phpunit.xml.dist
  • src/Core/AbstractObject.php
  • src/Core/Exception.php
  • src/Core/ListObject.php
  • src/DTO/FillableFieldsList.php
  • src/Exceptions/ErrorsException.php
  • src/Exceptions/OptionsMissingException.php
  • src/Exceptions/TypeException.php
  • src/PDFfiller.php
  • src/SignatureRequestRecipient.php
  • tests/ListObjectTest.php
  • tests/MockedProviderTrait.php
  • tests/ModelTest.php
  • tests/PDFfillerTest.php
 ________________________________________________________________________________________________________________________________________
< It is a truth universally acknowledged, that a single developer in possession of a good feature, must be in want of an AI code review. >
 ----------------------------------------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@websviewer
websviewer merged commit 20ce276 into master Sep 26, 2026
4 of 5 checks passed
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