Changelog - Version 7.0
Status: in development. This document tracks changes landing on the
7.0branch. Nothing here is released yet, and the contents may still change.
Breaking Changes
Schema::getInstance(), deprecated since 6.0, is removed. UseSchema::fromFile()for a file,Schema::fromJson()for a JSON string, orSchema::fromArray()for an array; they take the same second argument ($allowNullValues). See Migrating from Schema::getInstance().OpenApiValidation::assertRequest(), deprecated since 6.0, is removed. It was the former name ofsendRequest(), which takes the same requester.OpenApiValidation::setRequester(),getRequester()and the$requesterproperty are removed. They only fedmakeRequest()(removed, see below);sendRequest()never used them. Pass the requester tosendRequest()instead:$this->sendRequest($requester).ApiTestCase::makeRequest(), deprecated since 6.0, is removed. Build the request withApiRequester(withMethod(),withPath(),withQuery(),withRequestBody(),withRequestHeader(),expectStatus()) and pass it tosendRequest(). See Migrating from makeRequest().Body::match()takes an optional second argument,?string $contentType = null. A class that extendsBody(or one of its subclasses) and overridesmatch()has to add it to its signature.- A response is validated against the media type of its
Content-Typeheader (see Validation). A response whose header names a type the operation does not document now fails, also when its body would have matched the type guessed before. - A request body is validated against the media type of its
Content-Typeheader (see Validation). A request sent with a type the operation does not document now fails. On OpenAPI 3.0 a request with noContent-Typeis validated againstapplication/jsonwhen the operation documents it, like on 3.1, instead of against the first documented type. InvalidRequestException: Cannot handle Content Type '…'is no longer thrown: every request body is parsed now (see Validation). A request whose type the operation does not document throwsNotMatchedException: Content type '…' not found for <METHOD> <path>instead. Code that caughtInvalidRequestExceptionfor this case has to catchNotMatchedException.- On OpenAPI 3.1 a request without a body to an operation whose
requestBodyisrequired: truenow throwsRequiredArgumentNotFound, and a request with a body to an operation without arequestBodythrowsInvalidDefinitionException, as on OpenAPI 3.0 and Swagger 2.0 (see Bug Fixes). OpenApi31Schema::getSchemaDialect()returns the rootjsonSchemaDialectfield instead of a root$schemafield (see Bug Fixes).- When the specification's server URL has no host, the request keeps its own port (see Bug Fixes).
Validation
-
minLengthandmaxLengthare now enforced on string values, andminimumandmaximumon numeric values, for both request and response bodies. Before, these keywords were read from the specification but never checked, so a value outside the declared limits was accepted.Length is counted in characters, not bytes. A violation throws
NotMatchedException, for exampleValue 'abcdef' in 'name' is longer than maxLength 5.A request or response that was passing only because the limit was ignored now fails; the fix is in the data or in the specification.
exclusiveMinimum,exclusiveMaximumandmultipleOfare still not checked. -
The response body is matched against the media type of the response's
Content-Typeheader. The requester read the header but did not pass it on, so the type was guessed from the body (a string becametext/plain), and a response documented astext/html(or any non-JSON, non-XML type) could never be validated:Content type not found.The documented type is looked up as OpenAPI defines it: the type itself, then its range (
text/*), then*/*. A media type documented without a schema accepts any body. Without aContent-Typeheader the type is still guessed from the body. -
The request body is matched the same way, against the media type of the request's
Content-Type. OpenAPI 3.0 used the first documented media type and 3.1 preferred JSON, whatever the request was sent as, so an operation documentingapplication/jsonandmultipart/form-datavalidated a multipart upload against the JSON schema (or the reverse), and a request with an undocumented type was checked against another type's schema. Without aContent-Type(withRequestBody()sends arrays as JSON and sets none),application/jsonis used, thenapplication/xml,text/xml, then the first documented type, on both versions. -
The
multipart/form-dataparser only read a text field written with a;after its name, as byjg/webrequest writes it. A field as a browser or curl sends it (Content-Disposition: form-data; name="title") was not read: it raised "Undefined array key" warnings and was stored under anullkey, so a required field was reported missing. A file part of any type other thanapplication/octet-streamkept its ownContent-Typeheader lines in its value. The boundary was also used as a regular expression and a quoted boundary (boundary="...") was not recognised. The parser now splits the body on the boundary and reads each part's name and value, with CRLF or LF line endings. -
Request and response bodies are parsed by one rule, by their
Content-Type: any*/jsonor*+jsontype (RFC 6839, e.g.application/merge-patch+json) as JSON, any*/xmlor*+xmltype (e.g.application/problem+xml) as XML, multipart and urlencoded forms as fields, and anything else as the raw string. Without aContent-Type, a body that is not JSON is kept as a string. Before, a request of any other type raisedInvalidRequestException: Cannot handle Content Type, an XML request could not be validated, onlytext/xmlandapplication/xmlresponses were read as XML, and a non-JSON response without aContent-Typebecamenull("Body is expected"); it is now matched astext/plain.
Bug Fixes
OpenApi31RequestBody::match()did not enforcerequestBody.requiredand accepted a body sent to an operation that defines norequestBody. It now throwsRequiredArgumentNotFound(The body is required but it is empty) andInvalidDefinitionException(Request body provided for '…' but the OpenAPI 3.1 specification does not define a request body), like the OpenAPI 3.0 and Swagger 2.0 implementations.OpenApi31Schema::getSchemaDialect()read a root$schemafield, which OpenAPI 3.1 does not define; the dialect is declared by the rootjsonSchemaDialectfield.isJsonSchema202012()is now also true for the OAS 3.1 dialect (https://spec.openapis.org/oas/3.1/dialect/base), which is based on 2020-12.send()always took the port from the specification's server URL, so with a server URL without a host (/v2) the port of the request (http://localhost:8080/...) was dropped. The port now goes with the host: the server URL's when it has a host, the request's when it has not.expectHeaderContains()matched the header name case-sensitively, socontent-typedid not findContent-Type. HTTP header names are case-insensitive; the name is now looked up with PSR-7getHeader().
Requirements
- PHP 8.3, 8.4, 8.5 and 8.6 are now supported:
"php": ">=8.3 <8.7". The previous<8.6upper bound excluded PHP 8.6, since<8.6is exclusive.
ByJG dependencies
byjg/restserveris now^7.0.byjg/webrequestis now^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: true.
Toolchain
-
PHPUnit updated to
^12.5. -
Psalm is installed as
psalm/pharinstead ofvimeo/psalm.vimeo/psalmlists the PHP versions it supports and no published release includes 8.6, so as a dev dependency it madecomposer installfail on the 8.6 build job before any test ran.psalm/pharrequires onlyphp ^8.2and bundles its own dependencies, so it installs on every PHP version in the matrix and cannot conflict with the project's. Psalm itself still refuses to run on 8.6, which is why the Psalm job uses 8.5.composer psalmruns it. -
PHPUnit 13 is deliberately not used. It requires PHP
>=8.4.1, breaking the 8.3 floor.
Continuous Integration
- The build matrix now includes PHP 8.6.
- The Psalm job runs on PHP 8.5.
Housekeeping
phpunit.xml.distrenamed tophpunit.xml.