Require eventjet/json ^0.2 and PHP 8.3 - #3
Open
MidnightDesign wants to merge 2 commits into
Open
Conversation
eventjet/incert 0.1.1 requires eventjet/json ^0.2.0. As long as this
package pins ^0.1.2, the two are mutually exclusive and the eventjet-2014
app cannot install both, so the constraint has to move.
eventjet/json 0.2.0 contains a single change: "Reject values that don't
match the declared type". The public API is untouched -- Json::encode(),
Json::decode() and JsonError are byte-identical in signature. What
changed is that decode() now validates a value against the declared type
before constructing the target object.
Previously, scalar constructor arguments were bound through
ReflectionClass::newInstanceArgs(), whose parameter binding runs in weak
mode regardless of declare(strict_types=1), so a payload could be
silently coerced: "not a boolean" became true, 42 became '42', 50.9
became 50. 0.2.0 mirrors strict-mode binding instead and throws
JsonError on a mismatch. int -> float widening still works, as it does
in strict mode. The equivalent check for plain, non-promoted properties
only converts a TypeError into a JsonError; that path was already
strict. So the only genuinely new rejections are on scalar-typed
promoted constructor parameters of a decode target.
Audit
-----
The package has exactly one decode call site: OpenAi::createChatCompletion()
decodes the response body into CreateChatCompletionResponse. Every
scalar-typed promoted parameter reachable from that class was checked
against what the OpenAI chat-completions endpoint actually sends:
CreateChatCompletionResponse: id (string), created (int), model
(string), object (string), choices (array)
Choice: finish_reason (string), index (int)
ChatCompletionResponseMessage: content (?string), refusal (?string),
role (string)
All of them match the wire format. finish_reason is non-null on
non-streaming completions, and the package does not decode streaming
chunks or error envelopes -- a non-200 response is turned into a
RuntimeException before any decoding happens. content and refusal, the
two fields OpenAI documents as nullable, are already declared nullable.
There are no float-typed parameters, and usage/token counts are not
decoded at all, since the response DTO has no usage field. Unknown keys
(usage, system_fingerprint, service_tier, annotations, tool_calls) are
skipped by the decoder rather than assigned. No source change is
required.
Verified by decoding verbatim gpt-4o-mini response bodies (plain
completion, refusal, multiple choices with finish_reason length and
content_filter, and a tool_calls response) under both 0.1.2 and 0.2.0
and confirming identical results. The payloads 0.2.0 newly rejects --
created or index as a numeric string, id as a number -- are shapes the
API does not produce.
Two pre-existing decode failures were found and confirmed to predate
this bump by running the same payloads against 0.1.2. Neither is a
regression and neither is reachable through this client:
- ChatCompletionTokenLogprob::$logprob and TopLogprob::$logprob are
declared float|int, and the decoder rejects union-typed constructor
parameters outright. Unreachable, because CreateChatCompletionRequest
exposes no logprobs flag, so choices[].logprobs is always null.
- A message without a refusal key fails with "Missing required
constructor argument". The current API always sends the key.
Verification
------------
check-deps, cs-check, phpstan, psalm and phpunit all pass. They were run
on PHP 8.3 because the dev toolchain this package pins (psalm ^5.10,
which caps at 5.26.1) does not run on PHP 8.4. infection was skipped:
no source code changed.
The eventjet/json bump moves this package's minimum PHP version, so the
root constraint has to move with it.
eventjet/json v0.2.0 requires php >=8.3, as does v0.1.3. Only v0.1.2 and
earlier required >=8.1. Under the previous "^0.1.2" constraint, the root
"php": ">=8.2" was therefore honest: on PHP 8.2 composer simply resolved
eventjet/json to v0.1.2. Under "^0.2" there is exactly one candidate,
v0.2.0, and it needs 8.3 -- so the root constraint was promising support
this package can no longer deliver, and "composer require eventjet/openai"
on PHP 8.2 would fail to resolve rather than fail to install:
- Root composer.json requires eventjet/json ^0.2 -> satisfiable by
eventjet/json[v0.2.0].
- eventjet/json v0.2.0 requires php >=8.3 -> your php version (8.2.0)
does not satisfy that requirement.
Verified by resolving the package against config.platform.php 8.2.0
(fails as above) and 8.3.0 (resolves cleanly to eventjet/json v0.2.0).
check-deps, cs-check, phpstan, psalm and phpunit were re-run on PHP 8.3
after the change and all pass.
This only touches the root php constraint. The dev toolchain is stale
enough that it cannot run on PHP 8.4 at all -- psalm ^5.10 caps at
5.26.1, which crashes while scanning -- but that upgrade is a separate
concern and is deliberately left out of this change.
MidnightDesign
force-pushed
the
bump-eventjet-json-0.2
branch
from
August 5, 2026 11:34
2805a3a to
fe1ddf2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
eventjet/incert0.1.1 requireseventjet/json^0.2.0. As long as this package pins^0.1.2, the two are mutually exclusive and the eventjet-2014 app cannot install both, so the constraint has to move.What changed in eventjet/json 0.2.0
One commit: "Reject values that don't match the declared type". The public API is untouched —
Json::encode(),Json::decode()andJsonErrorare byte-identical in signature. What changed is thatdecode()now validates a value against the declared type before constructing the target object.Previously, scalar constructor arguments were bound through
ReflectionClass::newInstanceArgs(), whose parameter binding runs in weak mode regardless ofdeclare(strict_types=1), so a payload could be silently coerced —"not a boolean"becametrue,42became'42',50.9became50. Nothing was thrown and nothing was logged. 0.2.0 mirrors strict-mode binding instead and throwsJsonErroron a mismatch.booltrue,false"true","false",0,1intfloatstringint→floatwidening still works; it is the one widening strict mode itself performs. The equivalent check for plain, non-promoted properties only converts aTypeErrorinto aJsonError— that path was already strict. So the only genuinely new rejections are on scalar-typed promoted constructor parameters of a decode target.The platform requirement moves too
The root
phpconstraint goes from>=8.2to>=8.3in the same change, because the json bump moves this package's real minimum PHP version.eventjet/jsonv0.2.0 requiresphp >=8.3, as does v0.1.3. Only v0.1.2 and earlier required>=8.1. Under the old^0.1.2constraint the root>=8.2was therefore honest — on PHP 8.2, composer simply resolved json to v0.1.2. Under^0.2there is exactly one candidate, v0.2.0, and it needs 8.3, so leaving the root at>=8.2would promise support the package cannot deliver, andcomposer require eventjet/openaion PHP 8.2 would fail to resolve:Verified by resolving the package against
config.platform.php8.2.0(fails as above) and8.3.0(resolves cleanly to v0.2.0).eventjet/klarnamade the identical change for the same reason.The audit
The package has exactly one decode call site:
OpenAi::createChatCompletion()(src/OpenAi.php:49) decodes the response body intoCreateChatCompletionResponse. Every scalar-typed promoted parameter reachable from that class was checked against what the OpenAI chat-completions endpoint actually sends.CreateChatCompletionResponseidstringcreatedintmodelstringobjectstring"chat.completion"choicesarrayChoicefinish_reasonstringindexintmessagelogprobsLogprobs|nullnullChatCompletionResponseMessagecontentstring|nullrefusalstring|nullrolestring"assistant"Specific hazards checked and cleared:
contentandrefusalare the two fields OpenAI documents as nullable, and both are already declared nullable.finish_reasonis non-null on non-streaming completions, which is the only mode this client supports.temperature/top_p-style float parameter exists on any decode target, so the1vs"1"hazard does not arise.CreateChatCompletionResponsehas nousagefield, sousageand its nested counts never reach the type check.RuntimeExceptionbefore any decoding happens, and there is no streaming/delta path.usage,system_fingerprint,service_tier,annotationsandtool_callsare skipped by the decoder rather than assigned, so they cannot trip the new check.No source change is required.
This was verified empirically, not only by reading. Verbatim gpt-4o-mini response bodies — a plain completion, a refusal instead of content, multiple choices with
finish_reasonlengthandcontent_filter, and atool_callsresponse withcontent: null— were decoded under both 0.1.2 and 0.2.0 with identical results. The payloads 0.2.0 newly rejects are shapes the API does not produce:Two pre-existing issues (not regressions, not fixed here)
Both were confirmed to fail identically under 0.1.2, so neither is caused by this bump, and neither is reachable through this client:
ChatCompletionTokenLogprob::$logprobandTopLogprob::$logprobare declaredfloat|int, and the decoder rejects union-typed constructor parameters outright (Union types are not supported— present in 0.1.2 too). Unreachable, becauseCreateChatCompletionRequestexposes nologprobsflag, sochoices[].logprobsis alwaysnull.refusalkey fails withMissing required constructor argument "refusal". The current API always sends the key.Both are worth fixing eventually, but out of scope for a dependency bump.
Verification
composer check-depscomposer cs-checkcomposer phpstancomposer psalmcomposer phpunitOPENAI_API_KEY)composer infectionAll five were re-run after the
php: >=8.3change and still pass.composer show eventjet/jsonresolves to v0.2.0, and no transitive dependency blocks the upgrade.Stack
This PR is stacked on top of #4, the dev-toolchain upgrade, which merges first.
That upgrade is what makes the gates above meaningful. Originally they had to be run on PHP 8.3 rather than the host's 8.4, because the toolchain this package pinned (
vimeo/psalm ^5.10, capped at 5.26.1) does not support 8.4 and crashes during scanning withInvalidArgumentException: $value must be a scalar. With #4 underneath, all five gates were re-run natively on the host's PHP 8.4 and pass, with no platform override.Note that the root
phpconstraint stays at>=8.3here: #4 modernizesrequire-devonly and does not move the package's own minimum.