Skip to content

feat(core): return result objects instead of by-reference output parameters - #708

Open
Spomky wants to merge 1 commit into
4.3.xfrom
feature/result-objects
Open

feat(core): return result objects instead of by-reference output parameters#708
Spomky wants to merge 1 commit into
4.3.xfrom
feature/result-objects

Conversation

@Spomky

@Spomky Spomky commented Aug 28, 2026

Copy link
Copy Markdown
Member

Closes #685. Rebased on 4.3.x after #709 (service interfaces) and #707 (exception hierarchy).

What changes

Readonly result objects replace the by-reference output parameters listed in the issue. The new methods sit next to the old ones, and the old ones are implemented on top of them, so nothing breaks.

Before After
JWSVerifier::verifyWithKeySet($jws, $jwkset, 0, null, $jwk) verify($jws, $keys, 0)VerificationResult
JWEDecrypter::decryptUsingKey/KeySet($jwe, …) decrypt($jwe, $keys, 0)DecryptionResult
JWSLoader::loadAndVerifyWithKey/KeySet($token, …, $signature) loadAndVerify($token, $keys)LoadingResult
JWELoader::loadAndDecryptWithKey/KeySet($token, …, $recipient) loadAndDecrypt($token, $keys)LoadingResult
JWSSerializerManager::unserialize($input, $name) unserializeToken($input)UnserializationResult
JWESerializerManager::unserialize($input, $name) unserializeToken($input)UnserializationResult
NestedTokenLoader::load($token, …, $signature) loadAndVerify($token, …)LoadingResult
$result = $jwsVerifier->verify($jws, $jwkset, 0);
if ($result->isVerified()) {
    $key = $result->getKey();
}

$result = $jweDecrypter->decrypt($jwe, $jwkset, 0);
if ($result->isDecrypted()) {
    $jwe = $result->getJwe();   // the JWE given to the decrypter is left untouched
}

Two other things the new methods gain:

  • they accept a JWK as well as a JWKSet, so the …WithKey() / …WithKeySet() pairs collapse into one method;
  • the callable that observes the keys discarded along the way — added in fix(encryption): read the recipient header from the builder state #698 behind func_get_arg() because a signature cannot change in a minor — is a declared argument of verify() and decrypt().

The $signature / $signatureIndex docblock mismatch of verifyWithKeySet() is fixed on the docblock side: renaming the parameter would break callers already using named arguments.

No return type changes

No existing method changed its signature. verifyWithKeySet() still returns bool, loadAndVerifyWithKeySet() still returns JWS, unserialize() still returns JWS/JWE, load() still returns JWS. The result objects are only returned by the new methods.

The new methods are declared on the interfaces of #709

JWSVerifierInterface, JWEDecrypterInterface, JWSLoaderInterface, JWELoaderInterface and NestedTokenLoaderInterface gain the new method, and the deprecated ones are tagged @deprecated there too. Adding a method to a published interface is a hard BC break, so this is only free because those interfaces have not shipped yet — worth a conscious ack before merging.

It is also what makes the design hold together: JWSLoader receives a JWSVerifierInterface, so without the declaration it could not call verify() and would have to keep calling the deprecated method internally. It also keeps decoration — the extension mechanism #709 established — working for the new API.

Deprecations

verifyWithKeySet(), decryptUsingKey(), decryptUsingKeySet(), loadAndVerifyWithKey(), loadAndVerifyWithKeySet(), loadAndDecryptWithKey() and loadAndDecryptWithKeySet() trigger a deprecation and will be removed in 5.0.0. Their output parameter is required, so the methods cannot survive without it.

JWSSerializerManager::unserialize(), JWESerializerManager::unserialize() and NestedTokenLoader::load() keep working as they are: their output parameter is optional, so only passing it is deprecated, and only the argument goes away in 5.0.0. Calling unserialize($input) or load($token, $encryptionKeySet, $signatureKeySet) triggers nothing.

Algorithm interfaces

As in #654, changing encryptContent(), encryptKey(), wrapKey() and getAgreementKey() cannot be done additively without breaking every third-party implementation. 4.3.0 only documents the signature they will have in 5.0.0 and ships the objects they will return: EncryptedContent (ciphertext + tag) and WrappedKey (key + additional header parameters).

Bundle

Both families of services implement the new methods:

  • the EventDispatching* decorators dispatch from the new method and implement the deprecated ones on top of it, so an event is dispatched exactly once whichever API the application uses;
  • the deprecated inheritance-based services do the same, so they stay behaviourally identical to what they were.

Two decisions worth a look

  • decrypt() does not throw on failure. The issue's example has it throwing, but decryptUsingKeySet() already throws for an empty key set or a JWE without recipients, and those must keep propagating. A dedicated exception would be needed to tell "no key worked" apart from them; isDecrypted() keeps the old semantics, mirrors isVerified() and makes the deprecated method a two-line wrapper.
  • NestedTokenLoader is included although it is not in the issue's table — its load() has the same ?int &$signature idiom.

Checks

  • phpunit: 1033 tests green, including 23 new ones covering the result objects, the deprecations and the unchanged behaviour of the deprecated methods.
  • ecs, rector --dry-run: clean on the touched files.
  • phpstan: no new error, and the set of unmatched baseline entries is identical to the one on a pristine 4.3.x. Nineteen baseline entries are dropped — the by-ref, int|null and arguments.count types they described are gone, JWEDecrypter now iterates JWKSet::all() instead of the @internal iterator, and the deprecated loader methods declare @param-out int.

@Spomky Spomky self-assigned this Aug 28, 2026
@Spomky Spomky added the feature label Aug 28, 2026
@Spomky Spomky added this to the 4.3.0 milestone Aug 28, 2026
…meters

The verifiers, the decrypters, the loaders and the serializer managers used to
write their secondary results - the key that verified a signature, the index of
the recipient that could be decrypted, the name of the serializer - into
variables of the caller. Those methods cannot be called with named arguments,
cannot be composed and are hard to type for static analysers.

Readonly result objects carry those values instead, and the methods that
populate the references are implemented on top of the new ones:

- JWSVerifier::verify() returns a VerificationResult
- JWEDecrypter::decrypt() returns a DecryptionResult, and leaves the JWE it is
  given untouched
- JWSLoader::loadAndVerify(), JWELoader::loadAndDecrypt() and
  NestedTokenLoader::loadAndVerify() return a LoadingResult
- JWSSerializerManager::unserializeToken() and
  JWESerializerManager::unserializeToken() return an UnserializationResult

The new methods accept a JWK as well as a JWKSet, and the callable that observes
the keys discarded along the way is now a declared argument. They are declared
on the service interfaces, so that a decorator can be plugged into them the same
way as into the methods they replace.

The old methods keep their exact behaviour and trigger a deprecation. The two
methods whose output parameter is optional - the serializer managers'
unserialize() and NestedTokenLoader::load() - only deprecate that argument.

The algorithm interfaces cannot be changed without breaking every third-party
implementation, so 4.3.0 only documents the signature they will have in 5.0.0
and ships the objects they will return: EncryptedContent and WrappedKey.
@Spomky
Spomky force-pushed the feature/result-objects branch from eab5cae to 341c3be Compare August 29, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace the by-reference output parameters with result objects

1 participant