Skip to content

EE-293: Declare the optional parameters as explicit nullable types - #19

Merged
mSprunskas merged 11 commits into
paysera:masterfrom
vinayak-iyer-paysera:EE-293-explicit-nullable-types
Sep 30, 2026
Merged

mSprunskas merged 11 commits into
paysera:masterfrom
vinayak-iyer-paysera:EE-293-explicit-nullable-types

Conversation

@vinayak-iyer-paysera

@vinayak-iyer-paysera vinayak-iyer-paysera commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On PHP 8.4, loading this library prints eight "Implicitly marking parameter … as nullable is deprecated" notices. This declares the eight parameters as explicit nullable types, which removes them. Nothing a caller passes or receives changes. I propose releasing it as 0.4.2.

Why

  • PHP 8.4 deprecates a typed parameter that becomes nullable only because its default is null.
  • The eight parameters are $default of getBool(), getFloat(), getInt() and getString(), and $message / $previous of the constructors of InvalidItemException, InvalidItemTypeException and MissingItemException.
  • A deprecation is not an error. But an application whose tests fail on deprecations, or that logs them, sees eight notices from this library on every PHP 8.4 process.
  • Update dependencies and fix deprecation notices #13 (2023) fixes four of these parameters too, but it cannot be merged as it is. It also raises the minimum to PHP 8.0 and adds return types such as getIterator(): ArrayIterator, which would break subclasses. This change does only the parameters and keeps PHP 7.1.

Changes

  • src/: bool $default = null becomes ?bool $default = null, and the same for the other seven. The exception classes also import Exception instead of naming it \Exception. Nothing else in src/ changes.
  • phpunit.xml.dist: convertDeprecationsToExceptions="true". PHPUnit 9 then fails a test that triggers a deprecation, and the tests load every class under src/, so the PHP 8.4 jobs fail if an implicitly nullable parameter comes back. PHPUnit 6, used on PHP 7.1 and 7.2, converts deprecations already.
  • Tests for explicit null on all eight parameters, for a previous exception on each exception class, and for a custom message. The suite goes from 47 to 58 tests, and line coverage from 90 of 96 lines to 93 of 96.
  • CHANGELOG.md: 0.4.2.

Backward compatibility

?bool $default = null and bool $default = null declare the same type. They accept the same values, and PHP treats them as identical for subclasses. A backward-compatibility check from 0.4.1 finds no change in the library's API. ?type needs PHP 7.1, which composer.json already requires. So this is a patch release.

Not in this change: on PHP 8.5, getIterator() passes the wrapped object itself to ArrayIterator, which 8.5 deprecates. The obvious fix changes numeric iteration keys from strings to integers, so it needs its own change.

Test plan

  • The suite on PHP 7.1 to 8.4, with the lowest and the highest dependencies, through the workflow's own commands: 58 tests each.
  • With the eight parameters put back as they were, both PHP 8.4 jobs fail: 54 of 58 tests error with "Implicitly marking parameter … as nullable is deprecated". The same code passes on PHP 8.3.
  • Each new test fails when the code it covers is broken (single mutations of the parameters, of the previous exception each constructor passes on, and of the custom message).
  • A Symfony 4.4 and a Symfony 7.4 application built for this check, on PHP 7.4, 8.3 and 8.4: every public method and exception, with the kernel in debug mode and Symfony's DebugClassLoader active. On PHP 8.4 it saw the eight notices before this change and none after.
  • A Symfony 6.4 production application's full suite, with the eight parameters changed: the same result for every test before and after.
  • CI on this pull request: 18 of 18 jobs pass with 58 tests each (https://github.com/paysera/lib-object-wrapper/actions/runs/36682896946).

🤖 Generated with Claude Code

PHP 8.4 deprecates a typed parameter that is nullable only because
its default is null. Loading the library printed eight such notices:
the $default of getBool(), getFloat(), getInt() and getString(), and
$message and $previous in the three exception constructors.

The parameters now say ?type. The accepted values are the same, so no
caller or subclass changes, and PHP 7.1 still parses it. A new test
loads every class in a separate process and fails if PHP reports a
deprecation for the library.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The suite never called getKey(), setMessage(), getExpectedType() or
getGivenType(), and never converted a list of lists to an array.
Line coverage was 90 of 96.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PHPUnit runs data providers before it starts measuring coverage, so
setMessage(), called while the provider built its case, was counted
as never run although the test asserts its result. Each case now
passes a function that builds the exception during the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test listed four classes by hand, so a class added later would
escape it, and it ignored whether a class loaded at all. It now walks
src/ and reports any file whose class does not load.

The child process also starts with -n. It used to read the machine's
php.ini: a startup notice from an old setting failed the test on a
clean library, and with opcache enabled for the command line the
deprecations bypassed the error handler, so the test passed on the
code it exists to catch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only InvalidItemException's two parameters were passed an explicit
null in a test. The four getters' $default and the $previous of the
other two exceptions now are as well, which is the one thing the
nullable types must keep accepting. The new test methods declare
their void return type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep the loading test and the explicit-null cases for the eight parameters.
Remove the cases for code the release does not change (the exceptions'
setMessage() and non-null previous exceptions, getDataAsArray() over nested
lists), which were added only to raise line coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread tests/ClassLoadingTest.php Outdated
Comment thread src/Exception/InvalidItemException.php Outdated
Comment thread tests/Exception/InvalidItemExceptionTest.php
The three exception classes now import Exception, the way ObjectWrapper
imports RuntimeException. Nothing a caller sees changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every case passed null for the previous exception, and none passed a
message to InvalidItemException. Each exception now gets a case with a
previous exception, compared as the same object, and
InvalidItemException gets one with a custom message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
convertDeprecationsToExceptions makes PHPUnit 9 fail a test that
triggers a deprecation, and the tests load every class under src/, so
the PHP 8.4 jobs now fail on an implicitly nullable parameter without a
test of their own. PHPUnit 6 converts deprecations already.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mSprunskas
mSprunskas merged commit 34690b0 into paysera:master Sep 30, 2026
18 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.

2 participants