Skip to content

Prepare 7.0: PHP 8.3-8.6, PHPUnit 12.5, Psalm as a tool - #92

Merged
byjg merged 17 commits into
masterfrom
7.0
Oct 20, 2026
Merged

byjg merged 17 commits into
masterfrom
7.0

Conversation

@byjg

@byjg byjg commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Prepares this component for the 7.0 line (dependency level L1).

Requirements

"php": ">=8.3 <8.7" — PHP 8.3, 8.4, 8.5 and 8.6.

The previous <8.6 is exclusive, so it stopped at 8.5 and excluded PHP 8.6 entirely.

Toolchain

Before After
PHPUnit ^10.5|^11.5 ^12.5
Psalm ^5.9|^6.13 in require-dev ^6.16 in tools/psalm/composer.json

PHPUnit 13 is deliberately not used. It requires PHP >=8.4.1, which would break the 8.3 floor, and it pulls sebastian/diff ^9.0 — which the newest stable Psalm (6.16.1) does not accept. Because minimum-stability: dev is set, that combination does not fail; it silently resolves Psalm to an unreleased 6.x-dev branch head. Pinning PHPUnit to ^12.5 keeps one stable PHPUnit and one stable Psalm across the whole matrix.

Resolved consistently as PHPUnit 12.5.33 + Psalm 6.16.1 + sebastian/diff 7.0.0.

ByJG dependencies

Bumped to ^7.0. While 7.0 is unreleased these resolve to 7.0.x-dev from each component's 7.0 branch (minimum-stability: dev + prefer-stable: true), so the L0 branches must be pushed first — verified resolving on PHP 8.6.

Psalm moved out of require-dev

Psalm enumerates the PHP versions it supports and no published release lists 8.6, so as a dev dependency it made composer install fail on the 8.6 build job before any test ran. It now lives in tools/psalm/composer.json and is installed only by the Psalm job (PHP 8.5). composer psalm bootstraps the tool, so local usage is unchanged.

Continuous integration

  • Build matrix now includes PHP 8.6.
  • Psalm job moved to PHP 8.5. Psalm 6.16.1 declares ~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0 and cannot be installed on 8.6 at all.

Housekeeping

  • phpunit.xml.dist → phpunit.xml
  • 6.x changelogs carried onto the 7.0 branch
  • CHANGELOG-7.0.md added/updated

Verification

Tests run locally on PHP 8.5.9 and PHP 8.6.0beta1 — both green.

byjg added 4 commits August 2, 2026 22:41
Requires byjg/webrequest ^7.0, which narrows the PSR-7 wither return
types to static. Nothing here overrides a wither, so the library source
is unchanged.

The dev dependency on byjg/restserver has to move to ^7.0 as well.
Leaving it at ^6.0 makes the constraint unsatisfiable: prefer-stable
picks restserver 6.0.0, which itself requires webrequest ^6.0 and
conflicts with the root requirement.

RestServer 7.0 reworked its API, so the test fixtures follow:

  HttpRequestHandler  -> Server
  $request->param()   -> $request->attribute()   (route params now live
                         in the attribute bag; Server.php merges them
                         there via addAttributes)
  $request->post()    -> $request->body()

The RestServer snippet in docs/contract-tests.md is updated the same way,
with a note recording the 6.x name.

Both resolve to 7.0.x-dev until those packages are tagged.
The body was parsed as JSON or multipart, and anything else threw
InvalidRequestException. That rejected urlencoded forms, which OpenAPI
declares as a first-class request content type -- both as
`content."application/x-www-form-urlencoded"` in 3.x and as `in: formData`
with `consumes` in Swagger 2.0.

Parse them with parse_str(). The result is an associative array of
strings, which is the shape the matcher already receives from multipart
bodies; numeric types are checked with is_numeric(), so a form field
still matches an integer schema.

Purely additive -- that branch previously threw, so nothing can regress.
Content types other than these three still raise InvalidRequestException.
Holds working notes and scratch files that belong on the machine, not in
the repository.
Widen the PHP constraint to ">=8.3 <8.7"; the previous "<8.6" is
exclusive and excluded PHP 8.6.

Bump byjg/* dependencies to ^7.0. While 7.0 is unreleased these resolve
to 7.0.x-dev from each component's 7.0 branch, via minimum-stability dev
with prefer-stable.

Pin PHPUnit to ^12.5 and move Psalm to tools/psalm/composer.json. Psalm
enumerates supported PHP versions and no release lists 8.6, so as a
require-dev it made "composer install" fail on the 8.6 build before any
test ran. "composer psalm" bootstraps the tool and runs it.

CI: add PHP 8.6 to the matrix; the Psalm job runs on 8.5.

Housekeeping: rename phpunit.xml.dist to phpunit.xml, carry the 6.x
changelogs onto this branch, and update CHANGELOG-7.0.md.
byjg added 13 commits September 14, 2026 12:08
licensing.html returns 404; the page is /license/. The Dependencies
mermaid drifted from composer.json, which is now what the site's graph
is built from (collect-deps.py in add-doc).
This copy came from a template shared by eleven repositories: its Issues
link was the placeholder github.com/your-repo/issues, and nothing in it
was specific to this repository. GitHub now serves the account-wide
CONTRIBUTING.md from byjg/.github, which links the guide on
opensource.byjg.com.
- Install Psalm as psalm/phar instead of the tools/psalm project
- phpunit.xml: PHPUnit 12.5 schema
- Workflow: actions/checkout@v7, no workflow_dispatch
- README: Installation and Running Tests sections, https links
- Remove FUNDING.yml (comes from byjg/.github) and IDE files
- CHANGELOG-7.0.md updated
These keywords were read from the specification but never checked, so a value
outside the declared limits was accepted in request and response bodies.

Length is counted in characters, not bytes. exclusiveMinimum, exclusiveMaximum
and multipleOf are still not checked.
The requester read the response Content-Type but did not pass it to the
body matcher, so the type was guessed from the body (a string became
text/plain) and a response documented as text/html, or any non-JSON,
non-XML type, could never be validated: "Content type not found".

Body::match() takes an optional content type. OpenApiResponseBody looks it
up as OpenAPI defines it (the type, then its range, then */*), accepts any
body for a media type documented without a schema, and still guesses the
type when there is no header.
The requester knew the request Content-Type but did not pass it to the
body matcher. OpenAPI 3.0 then used the first documented media type and
3.1 preferred JSON, whatever the request was sent as, so a multipart
upload to an operation that also documents JSON was checked against the
JSON schema, and a request with an undocumented type passed against
another type's schema.

The request body is now matched like the response: the media type that
covers its Content-Type (the type, its range, then */*), or JSON, XML,
then the first documented type when there is no header. The lookup lives
in Body and is shared by the 3.0 and 3.1 request bodies.
parseMultiPartForm() only read a text field written with a ";" after its
name, as byjg/webrequest writes it: a field from a browser or curl
(name="title" and nothing after) raised "Undefined array key" and was
stored under a null key. A file part of any type but
application/octet-stream kept its own header lines in its value, the
boundary was used as a regular expression, and a quoted boundary was not
recognised.

The body is now split on the literal boundary, and each part into its
headers and value at the first blank line, with CRLF or LF line endings.
Any */json or +json type (RFC 6839) is decoded as JSON, any */xml or +xml
type is read as XML, multipart and urlencoded forms become their fields,
and anything else is kept as the raw string. Without a Content-Type, a
body that is not JSON is kept as a string.

Before, a request of any other type raised InvalidRequestException, an
XML request could not be validated, only text/xml and application/xml
responses were read as XML, and a non-JSON response without a
Content-Type became null ("Body is expected").

The new content type tests build their schema with Schema::fromArray()
instead of the deprecated Schema::getInstance().
Schema::getInstance(), OpenApiValidation::assertRequest() and
ApiTestCase::makeRequest() were deprecated in 6.0 for removal in 7.0. Use
Schema::fromFile(), fromJson() or fromArray(), sendRequest(), and an
ApiRequester passed to sendRequest(). The tests are migrated, and the
migration guide and changelog say they are gone.
- 3.1 enforces requestBody.required and rejects a body with no definition.
- getSchemaDialect() reads jsonSchemaDialect.
- The request port is kept for a server URL without a host.
- expectHeaderContains() ignores the header name case.
- Remove the unused setRequester()/getRequester().
@byjg
byjg merged commit 8dd75e7 into master Oct 20, 2026
6 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