diff --git a/README.md b/README.md index 591bba7..440efd4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ while being easy to understand for people without knowledge of the jsonapi stand The JSON:API standard makes it easy for clients to fetch multiple resources in one call and understand the relations between them. Read more about it at [jsonapi.org](https://jsonapi.org/). +The library's code uses strict typing and is checked against a high standard in phpstan and rector. It is tested extensively with 99% coverage. + ## Installation @@ -19,9 +21,9 @@ composer require alsvanzelf/jsonapi The library requires php 8.2. Use the latest [v2.x release](/releases/tag/v2.5.0) for lower php versions. -#### Upgrading from v1 +#### Upgrading from v1 or v2 -If you used v1 of this library, see [UPGRADE_1_TO_2.md](/UPGRADE_1_TO_2.md) on how to upgrade. +If you used v1 or v2 of this library, see [UPGRADE_1_TO_2.md](/UPGRADE_1_TO_2.md) or [UPGRADE_2_TO_3.md](/UPGRADE_1_TO_2.md) on how to upgrade. diff --git a/UPGRADE_2_TO_3.md b/UPGRADE_2_TO_3.md index 8940a24..cc8e1e0 100644 --- a/UPGRADE_2_TO_3.md +++ b/UPGRADE_2_TO_3.md @@ -1,43 +1,70 @@ # Upgrade from library v2 to v3 -## Checking links, ... +## Enums -Objects more often use uninitialized properties. `has*()` helper methods have been added to support the new flow. +All constants have been replaced by enums. -- `$object->links` to `if ($object->hasLinks()) $object->links` +Content types: +- `Document::CONTENT_TYPE_OFFICIAL` (`'application/vnd.api+json'`) to `ContentTypeEnum::Official` +- `Document::CONTENT_TYPE_DEBUG` (`'application/json'`) to `ContentTypeEnum::Debug` +- `Document::CONTENT_TYPE_JSONP` (`'application/javascript'`) to `ContentTypeEnum::Jsonp` -## Interfaces +Jsonapi versions: +- `Document::JSONAPI_VERSION_1_0` (`'1.0'`) to `JsonapiVersionEnum::V_1_0` +- `Document::JSONAPI_VERSION_1_1` (`'1.1'`) to `JsonapiVersionEnum::V_1_1` +- `Document::JSONAPI_VERSION_LATEST` (`'1.1'`) to `JsonapiVersionEnum::latest()` (still refering to v1.1) -When extending interfaces, you'll have to add method argument types and return types. +Document levels: +- `Document::LEVEL_ROOT` (`'root'`) to `DocumentLevelEnum::Root` +- `Document::LEVEL_JSONAPI` (`'jsonapi'`) to `DocumentLevelEnum::Jsonapi` +- `Document::LEVEL_Resource` (`'resource'`) to `DocumentLevelEnum::Resource` -Check [all interfaces](/src/interfaces) for the correct typing. +Sorting orders (as return value in the `order` field from `RequestParser->getSortFields()`): +- `RequestParser::SORT_ASCENDING` (`'ascending'`) to `SortOrderEnum::Ascending` +- `RequestParser::SORT_DESCENDING` (`'descending'`) to `SortOrderEnum::Descending` -## Enums +Relationship types: +- `RelationshipObject::TO_ONE` (`'one'`) to `RelationshipTypeEnum::ToOne` +- `RelationshipObject::TO_MANY` (`'many'`) to `RelationshipTypeEnum::ToMany` -Content types: -- `Document::CONTENT_TYPE_OFFICIAL` to `ContentTypeEnum::Official` -- `Document::CONTENT_TYPE_DEBUG` to `ContentTypeEnum::Debug` -- `Document::CONTENT_TYPE_JSONP` to `ContentTypeEnum::Jsonp` +Validator object containers (only used internally): +- `Validator::OBJECT_CONTAINER_*` to `ObjectContainerEnum::*` -Jsonapi versions: -- `Document::JSONAPI_VERSION_1_0` to `JsonapiVersionEnum::V_1_0` -- `Document::JSONAPI_VERSION_1_1` to `JsonapiVersionEnum::V_1_1` -- `Document::JSONAPI_VERSION_LATEST` to `JsonapiVersionEnum::Latest` -Document levels: -- `Document::LEVEL_ROOT` to `DocumentLevelEnum::Root` -- `Document::LEVEL_JSONAPI` to `DocumentLevelEnum::Jsonapi` -- `Document::LEVEL_Resource` to `DocumentLevelEnum::Resource` +## Strict type checking -Sorting orders: -- `RequestParser->getSortFields()` returns a `SortOrderEnum` instead of `string` for the `order` field +When using the library you shouldn't notice this unless passing values of the wrong type. When extending the library you probably will notice this when overriding methods. -Relationship types: -- `RelationshipObject::TO_ONE` to `RelationshipTypeEnum::ToOne` -- `RelationshipObject::TO_MANY` to `RelationshipTypeEnum::ToMany` +In both cases the upgrade is relatively easy, you can just follow php's type errors. Using phpstan will also help you find typing mismatches. -## Internal +Some invalid types were already checked at run-time and threw library exceptions. This now changed into php type errors. -Enums: -- `RequestParser::SORT_*` to `SortOrderEnum::*` -- `Validator::OBJECT_CONTAINER_*` to `ObjectContainerEnum::*` + +## Object properties are uninitialized by default + +Objects more often use uninitialized properties. In some cases, `has*()` helper methods have been added to support the new flow. Otherwise, an `isset()` can be used if needed. + +_This should only affect you when you extend the libraries classes. Otherwise you can just use the library methods to handle these properties._ + +- `->links`, on `Document`, `ErrorObject`, `RelationshipObject`, `ResourceObject`, can be checked with `->hasLinks()` +- `Document`: `->httpStatusCode`, `->jsonapi`, `->meta` +- `ErrorObject`: `->id`, `->code`, `->title`, `->detail`, `->meta`, `->httpStatusCode` +- `JsonapiObject`: `->meta`, `->version` +- `LinkObject`: `->href`, `->rel`, `->describedby`, `->title`, `->type`, `->meta` +- `RelationshipObject`: `->meta`, `->resource`, `->type` +- `ResourceDocument`: `->resource` +- `ResourceIdentifierObject`: `->type`, `->id`, `->lid`, `->meta`, `->validator` +- `ResourceObject`: `->attributes`, `->relationships` + + +## Json encoding and decoding errors + +The default encoding options changed to also include `JSON_THROW_ON_ERROR`. Failure to encode already used to throw an exception, but now it will default to a `\JsonException`. + + +## Removed deprecations + +- Old v1-style classes (`base.php`, `collection.php`, `error.php`, `errors.php`, `exception.php`, `resource.php`, `response.php`) +- Array-style links (`->appendLink()`, `->addLinksArray()`, `->appendLinkObject()`, `ErrorObject->appendTypeLink()`, `LinksArray`, `LinksObject->append()`) +- `CursorPaginationProfile->getKeyword()` +- `Converter::mergeProfilesInContentType()`, use `Converter::prepareContentType()` instead diff --git a/composer.lock b/composer.lock index c3d435c..9371fd3 100644 --- a/composer.lock +++ b/composer.lock @@ -69,16 +69,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -121,9 +121,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -293,11 +293,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -342,7 +342,7 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -393,16 +393,16 @@ }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.8", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe" + "reference": "5e30669bef866eff70db8b58d72a5c185aa82414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/5e30669bef866eff70db8b58d72a5c185aa82414", + "reference": "5e30669bef866eff70db8b58d72a5c185aa82414", "shasum": "" }, "require": { @@ -440,9 +440,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.11" }, - "time": "2025-11-11T07:55:22+00:00" + "time": "2025-12-19T09:05:35+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -494,23 +494,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.0", + "version": "12.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a" + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bca180c050dd3ae15f87c26d25cabb34fe1a0a5a", - "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.6.2", + "nikic/php-parser": "^5.7.0", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", @@ -518,10 +518,10 @@ "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^1.3.1" + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.4.4" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -559,7 +559,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" }, "funding": [ { @@ -579,7 +579,7 @@ "type": "tidelift" } ], - "time": "2025-11-29T07:15:54+00:00" + "time": "2025-12-24T07:03:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -828,16 +828,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.4.4", + "version": "12.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7" + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9253ec75a672e39fcc9d85bdb61448215b8162c7", - "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", "shasum": "" }, "require": { @@ -851,7 +851,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-code-coverage": "^12.5.1", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", @@ -873,7 +873,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.4-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -905,7 +905,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" }, "funding": [ { @@ -929,7 +929,7 @@ "type": "tidelift" } ], - "time": "2025-11-21T07:39:11+00:00" + "time": "2025-12-15T06:05:34+00:00" }, { "name": "psr/http-message", @@ -986,21 +986,21 @@ }, { "name": "rector/rector", - "version": "2.2.9", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "0b8e49ec234877b83244d2ecd0df7a4c16471f05" + "reference": "f7166355dcf47482f27be59169b0825995f51c7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/0b8e49ec234877b83244d2ecd0df7a4c16471f05", - "reference": "0b8e49ec234877b83244d2ecd0df7a4c16471f05", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", + "reference": "f7166355dcf47482f27be59169b0825995f51c7d", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.32" + "phpstan/phpstan": "^2.1.33" }, "conflict": { "rector/rector-doctrine": "*", @@ -1034,7 +1034,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.9" + "source": "https://github.com/rectorphp/rector/tree/2.3.0" }, "funding": [ { @@ -1042,7 +1042,7 @@ "type": "github" } ], - "time": "2025-11-28T14:21:22+00:00" + "time": "2025-12-25T22:00:18+00:00" }, { "name": "rector/swiss-knife", @@ -2121,23 +2121,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -2159,7 +2159,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -2167,7 +2167,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" } ], "aliases": [], diff --git a/rector.php b/rector.php index 4d2b6b9..9502f76 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Rector\CodeQuality\Rector\FuncCall\SortNamedParamRector; +use Rector\CodeQuality\Rector\FuncCall\SortCallLikeNamedArgsRector; use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector; use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector; @@ -45,7 +45,7 @@ DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, // better readability using function declaration sorting - SortNamedParamRector::class, + SortCallLikeNamedArgsRector::class, // explicit testing private properties RemoveUnusedPrivatePropertyRector::class => [ diff --git a/src/Document.php b/src/Document.php index 3af54d9..d21df25 100644 --- a/src/Document.php +++ b/src/Document.php @@ -274,7 +274,7 @@ public function toJson(array $options=[]): string { // we can't use exceptions because $options['encodeOptions'] might be overridden to silence them if ($json === false) { - throw new Exception('failed to encode json: '.json_last_error_msg()); + throw new Exception('failed to generate json: '.json_last_error_msg()); } if ($options['jsonpCallback'] !== null) { @@ -309,7 +309,6 @@ public function sendResponse(array $options=[]): void { /** * @return array */ - #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); } diff --git a/src/enums/JsonapiVersionEnum.php b/src/enums/JsonapiVersionEnum.php index 1a7186a..3d1d5cd 100644 --- a/src/enums/JsonapiVersionEnum.php +++ b/src/enums/JsonapiVersionEnum.php @@ -7,5 +7,16 @@ enum JsonapiVersionEnum: string { case V_1_0 = '1.0'; case V_1_1 = '1.1'; - case Latest = self::V_1_1->value; + + /** + * @internal for public use {@see self::latest()} + * + * this value is used as a default value for JsonapiObject's constructor + * since (enum) functions can't be used as default argument values + */ + case Latest = 'latest'; + + public static function latest(): self { + return self::V_1_1; + } } diff --git a/src/enums/ObjectContainerEnum.php b/src/enums/ObjectContainerEnum.php index d2258c0..c5f61ae 100644 --- a/src/enums/ObjectContainerEnum.php +++ b/src/enums/ObjectContainerEnum.php @@ -7,10 +7,10 @@ /** * @internal */ -enum ObjectContainerEnum: string { - case Type = 'type'; - case Id = 'id'; - case Lid = 'lid'; - case Attributes = 'attributes'; - case Relationships = 'relationships'; +enum ObjectContainerEnum { + case Type; + case Id; + case Lid; + case Attributes; + case Relationships; } diff --git a/src/helpers/Validator.php b/src/helpers/Validator.php index 981168c..c9fef0f 100644 --- a/src/helpers/Validator.php +++ b/src/helpers/Validator.php @@ -56,7 +56,7 @@ public function claimUsedFields(array $fieldNames, ObjectContainerEnum $objectCo continue; } - throw new DuplicateException('field name "'.$fieldName.'" already in use at "data.'.$this->usedFields[$fieldName]->value.'"'); + throw new DuplicateException('field name "'.$fieldName.'" already in use at "data.'.$this->usedFields[$fieldName]->name.'"'); } } diff --git a/src/objects/JsonapiObject.php b/src/objects/JsonapiObject.php index 1ee5197..68d4671 100644 --- a/src/objects/JsonapiObject.php +++ b/src/objects/JsonapiObject.php @@ -95,6 +95,10 @@ public function toArray(): array { $array = [...$array, ...$this->getExtensionMembers()]; } if (isset($this->version)) { + if ($this->version === JsonapiVersionEnum::Latest) { + $this->version = JsonapiVersionEnum::latest(); + } + $array['version'] = $this->version->value; } if ($this->extensions !== []) { diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index c166f27..9a31efe 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -309,7 +309,7 @@ public function testToJson_InvalidUtf8CustomException(): void { $options = ['array' => ['foo' => "\xB1\x31"], 'encodeOptions' => JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE]; $this->expectException(Exception::class); - $this->expectExceptionMessage('failed to encode json: Malformed UTF-8 characters, possibly incorrectly encoded'); + $this->expectExceptionMessage('failed to generate json: Malformed UTF-8 characters, possibly incorrectly encoded'); $this->document->toJson($options); }