Skip to content

Align declared PHP support with what is tested, and fix PHP 9 fatals - #23

Open
Matt (matt-evervault) wants to merge 4 commits into
masterfrom
feature/nix-and-modern-php-versions
Open

Align declared PHP support with what is tested, and fix PHP 9 fatals#23
Matt (matt-evervault) wants to merge 4 commits into
masterfrom
feature/nix-and-modern-php-versions

Conversation

@matt-evervault

@matt-evervault Matt (matt-evervault) commented Aug 12, 2026

Copy link
Copy Markdown

Migrates the SDK to currently-supported PHP versions.

Why each change

composer.json — the declared floor was inaccurate

"php": "^7.1|^8.0" had not been true since str_ends_with() (PHP 8.0+) was introduced into EvervaultUtils::isDecryptionDomain(). A PHP 7 consumer could install the package and then hit a fatal at runtime — the worst failure mode, because Composer is the layer that is supposed to prevent exactly that.

Raised to ^8.4 to match the CI matrix. This is a breaking change for consumers on 8.1–8.3, so a changeset is included (minor, i.e. 0.2.0 under semver-0 convention).

ext-gmp is now declared directly. The SDK's entire crypto path runs through GMP via paragonie/ecc, but it was only ever satisfied as a transitive requirement — meaning a consumer without GMP would get a confusing error attributed to a dependency instead of a clear platform requirement up front.

phpunit/phpunit ^9.0^13.0 — the config and the tool disagreed

This one was actively costing us signal. phpunit.xml had been written against the PHPUnit 10+ schema (all four displayDetailsOnTestsThatTrigger* attributes), but the lockfile pinned 9.6.34, which doesn't understand them. Every CI run printed:

Warning - The configuration file did not pass validation!
- Element 'phpunit', attribute 'displayDetailsOnTestsThatTriggerDeprecations': The attribute 'displayDetailsOnTestsThatTriggerDeprecations' is not allowed.
  … and three more
  Test results may not be as expected.

So the settings intended to surface deprecations were being silently discarded, in a green build, for however long the mismatch existed. PHPUnit 9 is also EOL and unsupported on 8.4+.

phpunit.xml is rebuilt on the PHPUnit 13 schema and now also has a bootstrap, a named EndToEnd testsuite, and a <source> block. The <source> block matters: it is what lets PHPUnit tell a deprecation in our code from one in a dependency, which is what makes failOnDirectDeprecation="true" safe to enable — our own deprecations fail the build, a dependency's are only displayed.

No test changes were needed (no data providers, and setUpBeforeClass(): void was already correct).

CI was configured to hide the very thing we needed to see

shivammathur/setup-php defaults to ini-file: production, which sets error_reporting = E_ALL & ~E_DEPRECATED. That is the reason the EvervaultError deprecation below never appeared in a passing build despite running on 8.4 and 8.5. Now pinned to error_reporting=E_ALL.

Also in the workflow:

  • ::set-output$GITHUB_OUTPUT. GitHub deprecated the workflow-command form and it warns on every run.
  • Added gmp to extensions, matching the new manifest requirement rather than relying on it being present by default.
  • Dropped the phpunit-versions: ['latest'] matrix axis. It fed into nothing — dependencies come from the lockfile — so it only put a misleading "latest" in job names while actually running whatever the lock pinned.
  • Test step now uses --testsuite EndToEnd instead of a bare path, so the suite definition lives in one place.

Two latent PHP 9 fatals

Both are deprecations on 8.4 and hard errors in PHP 9:

  1. EvervaultError::__construct()\Exception $previous = null is an implicitly-nullable parameter. Now ?\Throwable $previous = null, which additionally fixes a real narrowing bug: the signature rejected \Error and other non-Exception throwables that its own parent \Exception::__construct() accepts, so new EvervaultError($msg, 0, $someError) would TypeError.

  2. EvervaultUtils::isDecryptionDomain()parse_url($domain)['host'] emitted Undefined array key "host" for a URL without a host and then passed null into str_ends_with(), which is deprecated now and a TypeError in PHP 9. Replaced with parse_url($domain, PHP_URL_HOST) plus an explicit early return. Wildcard matching, exact matching and non-matching all verified unchanged.

Three PHP 8 warning-severity reads

These were notices in PHP 7 and are warnings in PHP 8; with failOnWarning now on, they would become test failures, so they are fixed rather than left to bite later.

  • Evervault::run() read $options['async'] unconditionally. The parameter default is ['version' => null, 'async' => false], but a caller passing ['version' => 2] replaces the default array wholesale, so async goes missing and the read warns. Now !empty().
  • EvervaultHttp::_handleApiResponse() read ->code and ->detail off json_decode() without checking it succeeded — a non-JSON or empty 403 body (a gateway error page, say) produced Attempt to read property on null. Now guarded on both fields, so a malformed body degrades to the generic permissions message instead of warning. Verified against a local server returning real 403s: a well-formed body still yields the exact detail string that testEncrytWithDataRoleForbiddingDecryption asserts.
  • EvervaultCrypto::_createV2Aad() threw an unqualified Exception from inside namespace Evervault, which resolves to the non-existent Evervault\Exception — so if that branch were ever reached it would raise Error: Class "Evervault\Exception" not found instead of the intended error. Now EvervaultError. (The branch is currently unreachable, since $versionNumber is a literal 1; left in place rather than deleted.)

curl_close() in the tests — found by this PR's own change

Worth calling out, because it is the justification for the whole exercise. The moment error_reporting=E_ALL and failOnDirectDeprecation went in, CI went red on its first run with something nobody had seen before:

4 tests triggered 3 PHP deprecations:
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0

curl_close() has been a no-op since PHP 8.0, when curl handles became objects freed by the garbage collector, and 8.5 deprecates it outright. Three test helpers were still calling it. The 8.4 job passed and only 8.5 flagged it — this is exactly the class of 8.5-and-later problem the previous configuration was structurally incapable of reporting, since it ran on 8.5 with E_DEPRECATED masked and a config file the test runner rejected. Removing the calls is a runtime no-op.

Housekeeping

flake.nix gains gmp explicitly — it worked only because nixpkgs enables it in the default extension set. .gitignore gains .phpunit.cache, the cache directory PHPUnit 13 is now configured to use.

Reviewer notes

  • The ^8.4 floor is the one judgement call here. It is a breaking change for consumers on 8.1–8.3. The alternative was ^8.2 (everything still receiving security fixes as of Aug 2026) with a dual phpunit ^11 || ^13 constraint and a four-version matrix. ^8.4 was chosen to match the matrix this branch already committed to; say the word and I'll widen it.
  • isDecryptionDomain and the EvervaultCrypto exception class were not in the original triage list — I picked them up because enabling failOnWarning/failOnDirectDeprecation while leaving known warning paths in the code would be inconsistent. Easy to drop if you'd rather keep this PR narrower.
  • failOnNotice/failOnWarning do raise the strictness of a suite that talks to a live API. If that turns out to be flaky in practice, those two are the dials to turn down — failOnDirectDeprecation is the one carrying the PHP 9 value.

🤖 Generated with Claude Code

@matt-evervault Matt (matt-evervault) self-assigned this Aug 12, 2026
@ev-vaultkeeper

Copy link
Copy Markdown

Vaultkeeper Commands

Mention @ev-vaultkeeper <command> in a PR review thread:

  • review — Review this PR and leave a review.
  • address-comments — Push commits that address the review feedback on this PR.
  • fix-ci — Investigate the failing CI on this PR and push a fix.

You can also request evervault-dependencies as a reviewer to trigger a review.

@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fae1347

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
evervault-php Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

The previous commit narrowed CI to PHP 8.4/8.5 but left the manifest,
test toolchain and source claiming support for PHP 7. This closes that
gap and fixes the deprecations CI was hiding.

- composer.json: php ^7.1|^8.0 -> ^8.4, declare ext-gmp directly, and
  bump phpunit to ^13 (9.6 is EOL and untested on 8.4+).
- phpunit.xml: was written against the PHPUnit 10+ schema while the lock
  pinned 9.6, so every CI run printed a validation warning and silently
  ignored all four displayDetails* flags. Rebuilt on the 13 schema with
  bootstrap, a named testsuite, and failOnDirectDeprecation.
- CI: setup-php's production ini sets error_reporting to E_ALL &
  ~E_DEPRECATED, which is why the EvervaultError deprecation never
  appeared in a green build. Force E_ALL, add gmp, and replace the
  deprecated ::set-output with $GITHUB_OUTPUT.
- EvervaultError: implicitly-nullable $previous is a PHP 9 fatal. Now
  ?\Throwable, which also matches \Exception::__construct.
- EvervaultUtils: parse_url()['host'] warned and passed null into
  str_ends_with() (a TypeError in PHP 9) for hostless URLs.
- Evervault/EvervaultHttp: guard the unconditional $options['async'] and
  json_decode property reads that warn under PHP 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matt-evervault Matt (matt-evervault) changed the title Migrate to modern php versions Align declared PHP support with what is tested, and fix PHP 9 fatals Aug 12, 2026
Matt (matt-evervault) and others added 2 commits August 12, 2026 15:38
The new error_reporting=E_ALL plus failOnDirectDeprecation caught this on
the first run: curl_close() is deprecated as of 8.5 because it has had no
effect since 8.0, when curl handles became objects freed by the GC. The
8.4 job passed and 8.5 reported 14 passing tests with 3 deprecations.

Removing the calls is a no-op at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matt-evervault
Matt (matt-evervault) requested a review from a team August 13, 2026 09:15

@matt-evervault Matt (matt-evervault) left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed this output from Claude and cleaned up the PR description a bit

@matt-evervault
Matt (matt-evervault) marked this pull request as ready for review August 13, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant