From aeead1a07dba3b9f21e103b215375952d5a64947 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 29 Dec 2025 15:14:38 +0100 Subject: [PATCH 01/20] fixes for phpcs bonus levels --- examples/bootstrap_examples.php | 2 +- examples/collection.php | 2 +- phpcs.bonus.xml | 4 +++- phpstan.bonus.neon | 4 ++-- src/objects/LinkObject.php | 14 ++++++++------ tests/example_output/collection/collection.php | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index acf810cc..92348d3b 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -59,7 +59,7 @@ class ExampleDataset { ]; public static function getRecord($type, $id) { - if (!isset(self::$records[$type][$id])) { + if (isset(self::$records[$type][$id]) === false) { throw new \Exception('sorry, we have a limited dataset'); } diff --git a/examples/collection.php b/examples/collection.php index cdd5a38c..ab11fc8a 100644 --- a/examples/collection.php +++ b/examples/collection.php @@ -18,7 +18,7 @@ foreach ($users as $user) { $resource = ResourceObject::fromObject($user, type: 'user', id: $user->id); - if ($user->id == 42) { + if ($user->id === 42) { $ship = new ResourceObject('ship', 5); $ship->add('name', 'Heart of Gold'); $resource->addRelationship('ship', $ship); diff --git a/phpcs.bonus.xml b/phpcs.bonus.xml index ee7fe9e0..5cb3deca 100644 --- a/phpcs.bonus.xml +++ b/phpcs.bonus.xml @@ -21,5 +21,7 @@ - + + + diff --git a/phpstan.bonus.neon b/phpstan.bonus.neon index ae9db4e9..169fed18 100644 --- a/phpstan.bonus.neon +++ b/phpstan.bonus.neon @@ -4,9 +4,9 @@ includes: parameters: level: 10 - + treatPhpDocTypesAsCertain: true - + # @see https://github.com/phpstan/phpstan-strict-rules strictRules: allRules: true diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index 5a13d2fb..7fbb4fdb 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -152,12 +152,7 @@ public function toArray(): array { $array['type'] = $this->type; } if ($this->hreflang !== []) { - if (count($this->hreflang) === 1) { - $array['hreflang'] = $this->hreflang[0]; - } - else { - $array['hreflang'] = $this->hreflang; - } + $array['hreflang'] = $this->getHrefLanguages(); } if (isset($this->describedby) && $this->describedby->isEmpty() === false) { $array['describedby'] = $this->describedby->toArray(); @@ -168,4 +163,11 @@ public function toArray(): array { return $array; } + + /** + * @return string|string[] + */ + private function getHrefLanguages(): string|array { + return (count($this->hreflang) === 1) ? $this->hreflang[0] : $this->hreflang; + } } diff --git a/tests/example_output/collection/collection.php b/tests/example_output/collection/collection.php index 991fbaee..a3a02659 100644 --- a/tests/example_output/collection/collection.php +++ b/tests/example_output/collection/collection.php @@ -29,7 +29,7 @@ public static function createJsonapiDocument() { foreach ($users as $user) { $resource = ResourceObject::fromObject($user, 'user', $user->id); - if ($user->id == 42) { + if ($user->id === 42) { $ship = new ResourceObject('ship', 5); $ship->add('name', 'Heart of Gold'); $resource->addRelationship('ship', $ship); From 7452fffadd739ba400ee532b53d199e14c92f754 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 29 Dec 2025 15:14:52 +0100 Subject: [PATCH 02/20] fix phpstan performance issues --- .../profiles/CursorPaginationProfileTest.php | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 42250a6d..9d67e331 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -76,21 +76,30 @@ public function test_WithRelationship(): void { parent::assertArrayHasKey('data', $array); parent::assertArrayHasKey('relationships', $array['data']); parent::assertArrayHasKey('people', $array['data']['relationships']); - parent::assertArrayHasKey('links', $array['data']['relationships']['people']); - parent::assertArrayHasKey('data', $array['data']['relationships']['people']); - parent::assertArrayHasKey('meta', $array['data']['relationships']['people']); - parent::assertArrayHasKey('prev', $array['data']['relationships']['people']['links']); - parent::assertArrayHasKey('next', $array['data']['relationships']['people']['links']); - parent::assertArrayHasKey('page', $array['data']['relationships']['people']['meta']); - parent::assertArrayHasKey('href', $array['data']['relationships']['people']['links']['prev']); - parent::assertArrayHasKey('href', $array['data']['relationships']['people']['links']['next']); - parent::assertArrayHasKey('total', $array['data']['relationships']['people']['meta']['page']); - parent::assertArrayHasKey('estimatedTotal', $array['data']['relationships']['people']['meta']['page']); - parent::assertArrayHasKey('bestGuess', $array['data']['relationships']['people']['meta']['page']['estimatedTotal']); - parent::assertCount(3, $array['data']['relationships']['people']['data']); - parent::assertArrayHasKey('meta', $array['data']['relationships']['people']['data'][0]); - parent::assertArrayHasKey('page', $array['data']['relationships']['people']['data'][0]['meta']); - parent::assertArrayHasKey('cursor', $array['data']['relationships']['people']['data'][0]['meta']['page']); + + // re-map nested arrays to variables to speed up phpstan + // without it, this file takes 10 seconds (!) more to process + + $people = $array['data']['relationships']['people']; + parent::assertArrayHasKey('links', $people); + parent::assertArrayHasKey('data', $people); + parent::assertArrayHasKey('meta', $people); + parent::assertArrayHasKey('prev', $people['links']); + parent::assertArrayHasKey('next', $people['links']); + parent::assertArrayHasKey('page', $people['meta']); + parent::assertArrayHasKey('href', $people['links']['prev']); + parent::assertArrayHasKey('href', $people['links']['next']); + + $peopleMeta = $people['meta']; + parent::assertArrayHasKey('total', $peopleMeta['page']); + parent::assertArrayHasKey('estimatedTotal', $peopleMeta['page']); + parent::assertArrayHasKey('bestGuess', $peopleMeta['page']['estimatedTotal']); + parent::assertCount(3, $people['data']); + + $firstPerson = $people['data'][0]; + parent::assertArrayHasKey('meta', $firstPerson); + parent::assertArrayHasKey('page', $firstPerson['meta']); + parent::assertArrayHasKey('cursor', $firstPerson['meta']['page']); } public function testSetLinksFirstPage_HappyPath(): void { From 0a998dc0b6ef10465104fd489b3f507569859c7a Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 29 Dec 2025 15:19:27 +0100 Subject: [PATCH 03/20] cleanup relaxed type checking by phpdoc --- examples/bootstrap_examples.php | 13 +++++-------- phpstan.bonus.neon | 2 -- phpstan.neon | 4 ++-- src/helpers/RequestParser.php | 2 +- src/objects/RelationshipObject.php | 1 + tests/example_output/ExampleTimestampsProfile.php | 13 +++++-------- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index 92348d3b..c18cf55c 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -151,14 +151,11 @@ public function getOfficialLink(): string { * optionally helpers for the specific profile */ - /** - * @param ResourceInterface&HasAttributesInterface $resource - */ - public function setTimestamps(ResourceInterface $resource, ?\DateTimeInterface $created=null, ?\DateTimeInterface $updated=null) { - if ($resource instanceof HasAttributesInterface === false) { - throw new \Exception('cannot add attributes to identifier objects'); - } - + public function setTimestamps( + ResourceInterface & HasAttributesInterface $resource, + ?\DateTimeInterface $created=null, + ?\DateTimeInterface $updated=null, + ) { $timestamps = []; if ($created !== null) { $timestamps['created'] = $created->format(\DateTime::ISO8601); diff --git a/phpstan.bonus.neon b/phpstan.bonus.neon index 169fed18..ddc4b487 100644 --- a/phpstan.bonus.neon +++ b/phpstan.bonus.neon @@ -5,8 +5,6 @@ includes: parameters: level: 10 - treatPhpDocTypesAsCertain: true - # @see https://github.com/phpstan/phpstan-strict-rules strictRules: allRules: true diff --git a/phpstan.neon b/phpstan.neon index a5a5f410..d0cf43ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,11 +5,11 @@ parameters: - src/ - tests/ - examples/ - + typeAliases: PHPStanTypeAlias_InternalOptions: 'array' - treatPhpDocTypesAsCertain: false + treatPhpDocTypesAsCertain: true strictRules: allRules: false diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index 0863c306..24918ca8 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -112,7 +112,7 @@ public function hasIncludePaths(): bool { * the raw format allows for custom processing * * @param PHPStanTypeAlias_InternalOptions $options {@see RequestParser::$defaults} - * @return string[]|array + * @return array|array */ public function getIncludePaths(array $options=[]): array { if ($this->queryParameters['include'] === '') { diff --git a/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php index e44ece9a..51668997 100644 --- a/src/objects/RelationshipObject.php +++ b/src/objects/RelationshipObject.php @@ -316,6 +316,7 @@ public function getNestedContainedResourceObjects(): array { $resourceObjects = []; foreach ($resources as $resource) { + // @phpstan-ignore instanceof.alwaysTrue, identical.alwaysFalse (we _can_ have both ResourceObject and ResourceIdentifierObject here) if ($resource->getResource() instanceof ResourceObject === false) { continue; } diff --git a/tests/example_output/ExampleTimestampsProfile.php b/tests/example_output/ExampleTimestampsProfile.php index 210af099..4129a35f 100644 --- a/tests/example_output/ExampleTimestampsProfile.php +++ b/tests/example_output/ExampleTimestampsProfile.php @@ -14,14 +14,11 @@ public function getOfficialLink(): string { return 'https://jsonapi.org/recommendations/#authoring-profiles'; } - /** - * @param ResourceInterface&HasAttributesInterface $resource - */ - public function setTimestamps(ResourceInterface $resource, ?\DateTimeInterface $created=null, ?\DateTimeInterface $updated=null) { - if ($resource instanceof HasAttributesInterface === false) { - throw new InputException('cannot add attributes to identifier objects'); - } - + public function setTimestamps( + ResourceInterface & HasAttributesInterface $resource, + ?\DateTimeInterface $created=null, + ?\DateTimeInterface $updated=null, + ) { $timestamps = []; if ($created !== null) { $timestamps['created'] = $created->format(\DateTime::ISO8601); From 7d4b8515a566df31bfca36cddc5a3ab33c200707 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 29 Dec 2025 15:53:12 +0100 Subject: [PATCH 04/20] improve test coverage --- src/Document.php | 4 -- src/objects/RelationshipObject.php | 5 -- src/objects/ResourceIdentifierObject.php | 2 +- tests/ResourceDocumentTest.php | 61 +++++++++++++++++++ tests/helpers/RequestParserTest.php | 7 +-- .../objects/ResourceIdentifierObjectTest.php | 48 +++++++++++++++ 6 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/Document.php b/src/Document.php index 0c2dfcdd..f1d599bb 100644 --- a/src/Document.php +++ b/src/Document.php @@ -140,7 +140,6 @@ public function setDescribedByLink(string $href, array $meta=[]): void { } /** - * @throws InputException if the $level is unknown * @throws InputException if the $level is DocumentLevelEnum::Resource */ public function addMeta(string $key, mixed $value, DocumentLevelEnum $level=DocumentLevelEnum::Root): void { @@ -163,9 +162,6 @@ public function addMeta(string $key, mixed $value, DocumentLevelEnum $level=Docu case DocumentLevelEnum::Resource: throw new InputException('level "resource" can only be set on a ResourceDocument'); - - default: - throw new InputException('unknown level "'.$level->value.'"'); } } diff --git a/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php index 51668997..6d72df4b 100644 --- a/src/objects/RelationshipObject.php +++ b/src/objects/RelationshipObject.php @@ -45,8 +45,6 @@ public function __construct( * @param CollectionDocument|ResourceInterface|ResourceInterface[]|null $relation * @param array $links * @param array $meta - * - * @throws InputException if $relation is not one of the supported formats */ public static function fromAnything( array|CollectionDocument|ResourceInterface|null $relation, @@ -66,9 +64,6 @@ public static function fromAnything( elseif ($relation === null) { $relationshipObject = new RelationshipObject(RelationshipTypeEnum::ToOne); } - else { - throw new InputException('unknown format of relation "'.get_debug_type($relation).'"'); - } return $relationshipObject; } diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index 5f09cd4d..b45be053 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -112,7 +112,7 @@ public function setMetaObject(MetaObject $metaObject): void { */ public static function fromResourceObject(ResourceObject $resourceObject): static { if ($resourceObject->hasIdentification() === false) { - throw new InputException('resource has no identification yet<'); + throw new InputException('resource has no identification yet'); } $resourceIdentifierObject = new static($resourceObject->type, $resourceObject->primaryId()); diff --git a/tests/ResourceDocumentTest.php b/tests/ResourceDocumentTest.php index 144d574c..f9a2276c 100644 --- a/tests/ResourceDocumentTest.php +++ b/tests/ResourceDocumentTest.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\ResourceDocument; use alsvanzelf\jsonapi\enums\DocumentLevelEnum; +use alsvanzelf\jsonapi\enums\RelationshipTypeEnum; use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; @@ -100,6 +101,66 @@ public function testAddRelationship_DoNotIncludeContainedResources(): void { parent::assertArrayNotHasKey('included', $array); } + public function testAddRelationship_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->addRelationship('foo', null); + } + + public function testAddLink_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->addLink('foo', null); + } + + public function testSetSelfLink_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->setSelfLink('https://jsonapi.org'); + } + + public function testSetAttributesObject_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->setAttributesObject(new AttributesObject()); + } + + public function testAddRelationshipObject_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->addRelationshipObject('foo', new RelationshipObject(RelationshipTypeEnum::ToOne)); + } + + public function testSetRelationshipsObject_IdentifierOnlyObject(): void { + $document = new ResourceDocument(); + $document->setPrimaryResource(new ResourceIdentifierObject('user', 42)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('the resource is an identifier-only object'); + + $document->setRelationshipsObject(new RelationshipsObject()); + } + public function testAddMeta_HappyPath(): void { $document = new ResourceDocument(); $document->addMeta('foo', 'root', DocumentLevelEnum::Root); diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php index 2f03689c..fa2120f9 100644 --- a/tests/helpers/RequestParserTest.php +++ b/tests/helpers/RequestParserTest.php @@ -87,12 +87,9 @@ public function testFromSuperglobals_WithPhpInputStream(): void { $_SERVER['REQUEST_URI'] = '/'; $_SERVER['CONTENT_TYPE'] = ContentTypeEnum::Official->value; + // empty $_POST so we get a bit more test coverage for input stream processing $_GET = []; - $_POST = [ - 'meta' => [ - 'foo' => 'bar', - ], - ]; + $_POST = []; $requestParser = RequestParser::fromSuperglobals(); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index 64e65ff2..89539196 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -6,8 +6,10 @@ use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\exceptions\Exception; +use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; +use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; class ResourceIdentifierObjectTest extends TestCase { @@ -55,6 +57,35 @@ public function testSetLocalId_WithIdAlreadySet(): void { $resourceIdentifierObject->setLocalId('uuid-1'); } + public function testFromResourceObject_HappyPath(): void { + $resource = new ResourceObject('test', 1); + $resource->addAttribute('foo', 'bar'); + + $array = $resource->toArray(); + + parent::assertSame('test', $array['type']); + parent::assertSame('1', $array['id']); + parent::assertArrayHasKey('attributes', $array); + + $resourceIdentifierObject = ResourceIdentifierObject::fromResourceObject($resource); + + $array = $resourceIdentifierObject->toArray(); + + parent::assertSame('test', $array['type']); + parent::assertSame('1', $array['id']); + parent::assertArrayNotHasKey('attributes', $array); + } + + public function testFromResourceObject_NoFullIdentification(): void { + $resource = new ResourceObject(); + $array = $resource->toArray(); + + $this->expectException(InputException::class); + $this->expectExceptionMessage('resource has no identification yet'); + + ResourceIdentifierObject::fromResourceObject($resource); + } + public function testEquals_HappyPath(): void { $one = new ResourceIdentifierObject('test', 1); $two = new ResourceIdentifierObject('test', 2); @@ -168,6 +199,13 @@ public function testGetIdentificationKey_NoFullIdentification(): void { $resourceIdentifierObject->getIdentificationKey(); } + public function testIsEmpty_IdWithoutType(): void { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $resourceIdentifierObject->setId(42); + + parent::assertFalse($resourceIdentifierObject->isEmpty()); + } + public function testIsEmpty_WithAtMembers(): void { $resourceIdentifierObject = new ResourceIdentifierObject(); @@ -190,4 +228,14 @@ public function testIsEmpty_WithExtensionMembers(): void { parent::assertFalse($resourceIdentifierObject->isEmpty()); } + + public function testPrimaryId_NoFullIdentification(): void { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $primaryIdMethod = new \ReflectionMethod($resourceIdentifierObject, 'primaryId'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('resource has no identification yet'); + + $primaryIdMethod->invoke($resourceIdentifierObject); + } } From c99963d50dea0cda33e66b336e207f2fc265b7c8 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 2 Jan 2026 23:18:48 +0100 Subject: [PATCH 05/20] fix issues for phpstan level 5 --- examples/output.php | 6 +----- examples/relationships.php | 9 --------- phpstan.neon | 5 +++-- src/profiles/CursorPaginationProfile.php | 24 ++++++++++++------------ 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/examples/output.php b/examples/output.php index 1cb9d4ab..a0ac0592 100644 --- a/examples/output.php +++ b/examples/output.php @@ -69,10 +69,6 @@ * send json response */ -$options = ['prettyPrint' => true, 'contentType' => 'text/html']; echo '

Send json response

'; echo '
$document->sendResponse();
'; -echo '
';
-$document->sendResponse($options);
-echo '
'; -echo '

Also sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.ContentTypeEnum::Official->value.']

'; +echo '

Echo\'s the result of $document->json() and sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.ContentTypeEnum::Official->value.']

'; diff --git a/examples/relationships.php b/examples/relationships.php index 73db5dc8..5f0cb5e3 100644 --- a/examples/relationships.php +++ b/examples/relationships.php @@ -74,15 +74,6 @@ $document->addRelationshipObject('one-by-one-neighbours', $relationshipObject); -/** - * custom - */ -$jsonapi = new ResourceDocument('user', 1); -$customRelation = [ - 'data' => ['cus' => 'tom'], -]; -$jsonapi->addRelationship('custom', $customRelation); - /** * sending the response */ diff --git a/phpstan.neon b/phpstan.neon index d0cf43ae..303d0a25 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,13 +1,14 @@ parameters: # slowly increase - level: 4 + level: 5 paths: - src/ - tests/ - examples/ typeAliases: - PHPStanTypeAlias_InternalOptions: 'array' + PHPStanTypeAlias_InternalOptions: 'array' + PHPStanTypeAlias_Document: 'array' treatPhpDocTypesAsCertain: true diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 75825210..399b5f86 100644 --- a/src/profiles/CursorPaginationProfile.php +++ b/src/profiles/CursorPaginationProfile.php @@ -56,10 +56,10 @@ class CursorPaginationProfile implements ProfileInterface { /** * set links to paginate the data using cursors of the paginated data * - * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param PaginableInterface & HasLinksInterface $paginable a CollectionDocument or RelationshipObject */ public function setLinks( - PaginableInterface $paginable, + PaginableInterface & HasLinksInterface $paginable, string $baseOrCurrentUrl, string $firstCursor, string $lastCursor, @@ -71,32 +71,32 @@ public function setLinks( } /** - * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param PaginableInterface & HasLinksInterface $paginable a CollectionDocument or RelationshipObject */ - public function setLinksFirstPage(PaginableInterface $paginable, string $baseOrCurrentUrl, string $lastCursor): void { + public function setLinksFirstPage(PaginableInterface & HasLinksInterface $paginable, string $baseOrCurrentUrl, string $lastCursor): void { $this->setPaginationLinkObjectsWithoutPrevious($paginable, $baseOrCurrentUrl, $lastCursor); } /** - * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param PaginableInterface & HasLinksInterface $paginable a CollectionDocument or RelationshipObject */ - public function setLinksLastPage(PaginableInterface $paginable, string $baseOrCurrentUrl, string $firstCursor): void { + public function setLinksLastPage(PaginableInterface & HasLinksInterface $paginable, string $baseOrCurrentUrl, string $firstCursor): void { $this->setPaginationLinkObjectsWithoutNext($paginable, $baseOrCurrentUrl, $firstCursor); } /** * set the cursor of a specific resource to allow pagination after or before this resource */ - public function setCursor(ResourceInterface $resource, string $cursor): void { + public function setCursor(ResourceInterface & HasMetaInterface $resource, string $cursor): void { $this->setItemMeta($resource, $cursor); } /** * set count(s) to tell about the (estimated) total size * - * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param PaginableInterface & HasMetaInterface $paginable a CollectionDocument or RelationshipObject */ - public function setCount(PaginableInterface $paginable, ?int $exactTotal=null, ?int $bestGuessTotal=null) { + public function setCount(PaginableInterface & HasMetaInterface $paginable, ?int $exactTotal=null, ?int $bestGuessTotal=null) { $this->setPaginationMeta($paginable, $exactTotal, $bestGuessTotal); } @@ -137,15 +137,15 @@ public function setPaginationLinkObjects( $paginable->addLinkObject('next', $nextLinkObject); } - public function setPaginationLinkObjectsWithoutNext(PaginableInterface $paginable, string $baseOrCurrentUrl, string $firstCursor): void { + public function setPaginationLinkObjectsWithoutNext(PaginableInterface & HasLinksInterface $paginable, string $baseOrCurrentUrl, string $firstCursor): void { $this->setPaginationLinkObjects($paginable, new LinkObject($this->generatePreviousLink($baseOrCurrentUrl, $firstCursor)), new LinkObject()); } - public function setPaginationLinkObjectsWithoutPrevious(PaginableInterface $paginable, string $baseOrCurrentUrl, string $lastCursor): void { + public function setPaginationLinkObjectsWithoutPrevious(PaginableInterface & HasLinksInterface $paginable, string $baseOrCurrentUrl, string $lastCursor): void { $this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject($this->generateNextLink($baseOrCurrentUrl, $lastCursor))); } - public function setPaginationLinkObjectsExplicitlyEmpty(PaginableInterface $paginable): void { + public function setPaginationLinkObjectsExplicitlyEmpty(PaginableInterface & HasLinksInterface $paginable): void { $this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject()); } From 17579ff7753e61d065d7e1e3c514c562160804c9 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 2 Jan 2026 23:50:32 +0100 Subject: [PATCH 06/20] improve type linting for options --- phpstan.neon | 12 +++++++-- src/CollectionDocument.php | 8 +++--- src/Document.php | 8 +++--- src/ErrorsDocument.php | 12 ++++----- src/ResourceDocument.php | 30 +++++++++++------------ src/helpers/RequestParser.php | 12 ++++----- src/helpers/Validator.php | 8 +++--- src/interfaces/DocumentInterface.php | 4 +-- src/interfaces/HasAttributesInterface.php | 4 +-- src/objects/ErrorObject.php | 8 +++--- src/objects/ResourceObject.php | 20 +++++++-------- 11 files changed, 67 insertions(+), 59 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 303d0a25..bbd71f3d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,8 +7,16 @@ parameters: - examples/ typeAliases: - PHPStanTypeAlias_InternalOptions: 'array' - PHPStanTypeAlias_Document: 'array' + PHPStanTypeAlias_CollectionDocumentOptions: 'array{includeContainedResources?: bool}' + PHPStanTypeAlias_DocumentOptions: 'array{encodeOptions?: int, prettyPrint?: bool, contentType?: \alsvanzelf\jsonapi\enums\ContentTypeEnum, array?: ?PHPStanTypeAlias_DocumentArray, json?: ?string, jsonpCallback?: ?string}' + PHPStanTypeAlias_ErrorsDocumentOptions: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool}' + PHPStanTypeAlias_ResourceDocumentOptions: 'array{includeContainedResources?: bool}' + PHPStanTypeAlias_ResourceDocumentAndValidatorOptions: 'array{includeContainedResources?: bool, enforceTypeFieldNamespace?: bool}' + PHPStanTypeAlias_ValidatorOptions: 'array{enforceTypeFieldNamespace?: bool}' + PHPStanTypeAlias_RequestParserOptions: 'array{useNestedIncludePaths?: bool, useAnnotatedSortFields?: bool}' + PHPStanTypeAlias_ErrorObjectOptions: 'array{includeExceptionTrace?: bool, stripExceptionBasePath?: ?string}' + PHPStanTypeAlias_ErrorsDocumentAndErrorObjectOptions: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool, stripExceptionBasePath?: ?string}' + PHPStanTypeAlias_DocumentArray: 'array' treatPhpDocTypesAsCertain: true diff --git a/src/CollectionDocument.php b/src/CollectionDocument.php index d3663039..9262966b 100644 --- a/src/CollectionDocument.php +++ b/src/CollectionDocument.php @@ -21,8 +21,8 @@ class CollectionDocument extends DataDocument implements PaginableInterface, ResourceContainerInterface { /** @var ResourceInterface[] */ protected array $resources = []; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_CollectionDocumentOptions */ + protected static array $collectionDocumentDefaults = [ /** * add resources inside relationships to /included when adding resources to the collection */ @@ -89,7 +89,7 @@ public function setPaginationLinks( * * adds included resources if found inside the resource's relationships, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_InternalOptions $options {@see CollectionDocument::$defaults} + * @param PHPStanTypeAlias_CollectionDocumentOptions $options {@see CollectionDocument::$collectionDocumentDefaults} * * @throws InputException if the resource is empty */ @@ -98,7 +98,7 @@ public function addResource(ResourceInterface $resource, array $options=[]): voi throw new InputException('does not make sense to add empty resources to a collection'); } - $options = [...self::$defaults, ...$options]; + $options = [...self::$collectionDocumentDefaults, ...$options]; $this->validator->claimUsedResourceIdentifier($resource); diff --git a/src/Document.php b/src/Document.php index f1d599bb..06bd2b51 100644 --- a/src/Document.php +++ b/src/Document.php @@ -47,8 +47,8 @@ abstract class Document implements DocumentInterface, \JsonSerializable, HasLink protected array $extensions = []; /** @var ProfileInterface[] */ protected array $profiles = []; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_DocumentOptions */ + protected static array $documentDefaults = [ /** * encode to json with these default options */ @@ -260,7 +260,7 @@ public function toArray(): array { * @throws \JsonException */ public function toJson(array $options=[]): string { - $options = [...self::$defaults, ...$options]; + $options = [...self::$documentDefaults, ...$options]; $array = $options['array'] ?? $this->toArray(); @@ -278,7 +278,7 @@ public function toJson(array $options=[]): string { } public function sendResponse(array $options=[]): void { - $options = [...self::$defaults, ...$options]; + $options = [...self::$documentDefaults, ...$options]; if ($this->httpStatusCode === 204) { http_response_code($this->httpStatusCode); diff --git a/src/ErrorsDocument.php b/src/ErrorsDocument.php index 546fbab1..8e6c7bc9 100644 --- a/src/ErrorsDocument.php +++ b/src/ErrorsDocument.php @@ -15,8 +15,8 @@ class ErrorsDocument extends Document { protected array $errors = []; /** @var array> */ protected array $httpStatusCodes; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_ErrorsDocumentOptions */ + protected static array $errorsDocumentDefaults = [ /** * add the trace of exceptions when adding exceptions * in some cases it might be handy to disable if traces are too big @@ -42,10 +42,10 @@ public function __construct(?ErrorObject $errorObject=null) { */ /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ErrorsDocument::$defaults} + * @param PHPStanTypeAlias_ErrorsDocumentOptions $options {@see ErrorsDocument::$errorsDocumentDefaults} */ public static function fromException(\Throwable $exception, array $options=[]): static { - $options = [...self::$defaults, ...$options]; + $options = [...self::$errorsDocumentDefaults, ...$options]; $errorsDocument = new static(); $errorsDocument->addException($exception, $options); @@ -58,10 +58,10 @@ public static function fromException(\Throwable $exception, array $options=[]): * * recursively adds multiple ErrorObjects if $exception carries a ->getPrevious() * - * @param PHPStanTypeAlias_InternalOptions $options {@see ErrorsDocument::$defaults} + * @param PHPStanTypeAlias_ErrorsDocumentAndErrorObjectOptions $options {@see ErrorsDocument::$errorsDocumentDefaults} */ public function addException(\Throwable $exception, array $options=[]): void { - $options = [...self::$defaults, ...$options]; + $options = [...self::$errorsDocumentDefaults, ...$options]; $this->addErrorObject(ErrorObject::fromException($exception, $options)); diff --git a/src/ResourceDocument.php b/src/ResourceDocument.php index 17bfda7d..6d9580cd 100644 --- a/src/ResourceDocument.php +++ b/src/ResourceDocument.php @@ -27,8 +27,8 @@ */ class ResourceDocument extends DataDocument implements HasAttributesInterface, ResourceInterface { protected ResourceIdentifierObject|ResourceObject $resource; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_ResourceDocumentOptions */ + protected static array $resourceDocumentDefaults = [ /** * add resources inside relationships to /included when adding resources to the collection */ @@ -51,7 +51,7 @@ public function __construct(?string $type=null, string|int|null $id=null) { */ /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentAndValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray( array $attributes, @@ -66,7 +66,7 @@ public static function fromArray( } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentAndValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} */ public static function fromObject( object $attributes, @@ -82,8 +82,8 @@ public static function fromObject( /** * add key-value pairs to the resource's attributes * - * @param mixed $value objects will be converted using `get_object_vars()` - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param mixed $value objects will be converted using `get_object_vars()` + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function add(string $key, mixed $value, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -101,7 +101,7 @@ public function add(string $key, mixed $value, array $options=[]): void { * @param CollectionDocument|ResourceInterface|ResourceInterface[]|null $relation * @param array $links * @param array $meta - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function addRelationship( string $key, @@ -114,7 +114,7 @@ public function addRelationship( throw new Exception('the resource is an identifier-only object'); } - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceDocumentDefaults, ...$options]; $relationshipObject = $this->resource->addRelationship($key, $relation, $links, $meta); @@ -188,7 +188,7 @@ public function setLocalId(string|int $localId): void { } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public function setAttributesObject(AttributesObject $attributesObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -203,14 +203,14 @@ public function setAttributesObject(AttributesObject $attributesObject, array $o * * adds included resources if found inside the RelationshipObject, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function addRelationshipObject(string $key, RelationshipObject $relationshipObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { throw new Exception('the resource is an identifier-only object'); } - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceDocumentDefaults, ...$options]; $this->resource->addRelationshipObject($key, $relationshipObject); @@ -224,14 +224,14 @@ public function addRelationshipObject(string $key, RelationshipObject $relations * * adds included resources if found inside the RelationshipObjects inside the RelationshipsObject, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function setRelationshipsObject(RelationshipsObject $relationshipsObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { throw new Exception('the resource is an identifier-only object'); } - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceDocumentDefaults, ...$options]; $this->resource->setRelationshipsObject($relationshipsObject); @@ -249,7 +249,7 @@ public function setRelationshipsObject(RelationshipsObject $relationshipsObject, * * adds included resources if found inside the resource's relationships, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} * * @throws InputException if the $resource is a ResourceDocument itself */ @@ -260,7 +260,7 @@ public function setPrimaryResource(ResourceInterface $resource, array $options=[ /** @var ResourceIdentifierObject|ResourceObject $resource */ - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceDocumentDefaults, ...$options]; $this->resource = $resource; diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index 24918ca8..e2a951c3 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -15,8 +15,8 @@ * that might break the class since we use `new static()` */ class RequestParser { - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_RequestParserOptions */ + protected static array $requestParserDefaults = [ /** * reformat the include query parameter paths to nested arrays * this allows easier processing on each step of the chain @@ -111,7 +111,7 @@ public function hasIncludePaths(): bool { * the nested format allows easier processing on each step of the chain * the raw format allows for custom processing * - * @param PHPStanTypeAlias_InternalOptions $options {@see RequestParser::$defaults} + * @param PHPStanTypeAlias_RequestParserOptions $options {@see RequestParser::$requestParserDefaults} * @return array|array */ public function getIncludePaths(array $options=[]): array { @@ -121,7 +121,7 @@ public function getIncludePaths(array $options=[]): array { $includePaths = explode(',', (string) $this->queryParameters['include']); - $options = [...self::$defaults, ...$options]; + $options = [...self::$requestParserDefaults, ...$options]; if ($options['useNestedIncludePaths'] === false) { return $includePaths; } @@ -169,7 +169,7 @@ public function hasSortFields(): bool { * * @todo return some kind of SortFieldObject * - * @param PHPStanTypeAlias_InternalOptions $options {@see RequestParser::$defaults} + * @param PHPStanTypeAlias_RequestParserOptions $options {@see RequestParser::$requestParserDefaults} * @return string[]|arrayqueryParameters['sort']); - $options = [...self::$defaults, ...$options]; + $options = [...self::$requestParserDefaults, ...$options]; if ($options['useAnnotatedSortFields'] === false) { return $fields; } diff --git a/src/helpers/Validator.php b/src/helpers/Validator.php index d1010b61..40f81fac 100644 --- a/src/helpers/Validator.php +++ b/src/helpers/Validator.php @@ -17,8 +17,8 @@ class Validator { protected array $usedFields = []; /** @var array */ protected array $usedResourceIdentifiers = []; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_ValidatorOptions */ + protected static array $validatorDefaults = [ /** * blocks 'type' as a keyword inside attributes or relationships * the specification doesn't allow this as 'type' is already set at the root of a resource @@ -33,12 +33,12 @@ class Validator { * @see https://jsonapi.org/format/1.1/#document-resource-object-fields * * @param string[] $fieldNames - * @param PHPStanTypeAlias_InternalOptions $options {@see Validator::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see Validator::$validatorDefaults} * * @throws DuplicateException */ public function claimUsedFields(array $fieldNames, ObjectContainerEnum $objectContainer, array $options=[]): void { - $options = [...self::$defaults, ...$options]; + $options = [...self::$validatorDefaults, ...$options]; foreach ($fieldNames as $fieldName) { if (isset($this->usedFields[$fieldName]) === false) { diff --git a/src/interfaces/DocumentInterface.php b/src/interfaces/DocumentInterface.php index fc80a4a0..fc400dd9 100644 --- a/src/interfaces/DocumentInterface.php +++ b/src/interfaces/DocumentInterface.php @@ -17,7 +17,7 @@ public function toArray(): array; /** * generate json with the contents of the document, used by {@see ->sendResponse()} * - * @param PHPStanTypeAlias_InternalOptions $options + * @param PHPStanTypeAlias_DocumentOptions $options * * @throws Exception if generating json fails */ @@ -28,7 +28,7 @@ public function toJson(array $options=[]): string; * * @note will set http status code and content type, and echo json * - * @param PHPStanTypeAlias_InternalOptions $options + * @param PHPStanTypeAlias_DocumentOptions $options */ public function sendResponse(array $options=[]): void; } diff --git a/src/interfaces/HasAttributesInterface.php b/src/interfaces/HasAttributesInterface.php index f42b426b..15f37e54 100644 --- a/src/interfaces/HasAttributesInterface.php +++ b/src/interfaces/HasAttributesInterface.php @@ -8,9 +8,9 @@ interface HasAttributesInterface { /** * add key-value pairs to attributes * - * @see ResourceObject::$defaults + * @see Validator::$validatorDefaults * - * @param PHPStanTypeAlias_InternalOptions $options + * @param PHPStanTypeAlias_ValidatorOptions $options */ public function addAttribute(string $key, mixed $value, array $options=[]): void; } diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index cd47138c..6ab16982 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -29,8 +29,8 @@ class ErrorObject extends AbstractObject implements HasLinksInterface, HasMetaIn /** @var array{pointer?: string, parameter?: string, header?: string} */ protected array $source = []; protected MetaObject $meta; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_ErrorObjectOptions */ + protected static array $errorObjectDefaults = [ /** * add the trace of exceptions when adding exceptions * in some cases it might be handy to disable if traces are too big @@ -71,10 +71,10 @@ public function __construct( */ /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ErrorObject::$defaults} + * @param PHPStanTypeAlias_ErrorObjectOptions $options {@see ErrorObject::$errorObjectDefaults} */ public static function fromException(\Throwable $exception, array $options=[]): static { - $options = [...self::$defaults, ...$options]; + $options = [...self::$errorObjectDefaults, ...$options]; $errorObject = new static(); diff --git a/src/objects/ResourceObject.php b/src/objects/ResourceObject.php index 8f115f59..ac62ff73 100644 --- a/src/objects/ResourceObject.php +++ b/src/objects/ResourceObject.php @@ -23,8 +23,8 @@ class ResourceObject extends ResourceIdentifierObject implements HasAttributesIn protected AttributesObject $attributes; protected RelationshipsObject $relationships; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var PHPStanTypeAlias_ValidatorOptions */ + protected static array $resourceObjectDefaults = [ /** * blocks 'type' as a keyword inside attributes or relationships * the specification doesn't allow this as 'type' is already set at the root of a resource @@ -42,8 +42,8 @@ class ResourceObject extends ResourceIdentifierObject implements HasAttributesIn * and if $id is null, it is filled with that value * it is common to find it inside, and not doing so will cause an exception * - * @param array $attributes - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param array $attributes + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray(array $attributes, ?string $type=null, string|int|null $id=null, array $options=[]): static { if (isset($attributes['id'])) { @@ -61,7 +61,7 @@ public static function fromArray(array $attributes, ?string $type=null, string|i } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public static function fromObject(object $attributes, ?string $type=null, string|int|null $id=null, array $options=[]): static { $array = Converter::objectToArray($attributes); @@ -72,10 +72,10 @@ public static function fromObject(object $attributes, ?string $type=null, string /** * add key-value pairs to attributes * - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public function add(string $key, mixed $value, array $options=[]): void { - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceObjectDefaults, ...$options]; if (isset($this->attributes) === false) { $this->attributes = new AttributesObject(); @@ -90,7 +90,7 @@ public function add(string $key, mixed $value, array $options=[]): void { * @param CollectionDocument|ResourceInterface|ResourceInterface[]|null $relation * @param array $links * @param array $meta - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public function addRelationship( string $key, @@ -118,7 +118,7 @@ public function setSelfLink(string $href, array $meta=[]): void { */ /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} */ public function setAttributesObject(AttributesObject $attributesObject, array $options=[]): void { $newKeys = $attributesObject->getKeys(); @@ -129,7 +129,7 @@ public function setAttributesObject(AttributesObject $attributesObject, array $o } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} * * @throws DuplicateException if the resource is contained as a resource in the relationship */ From 30bd4a955042f60893042064dbe4906b5e342987 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 2 Jan 2026 23:50:43 +0100 Subject: [PATCH 07/20] pass missed options --- src/ResourceDocument.php | 2 +- src/objects/ResourceObject.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ResourceDocument.php b/src/ResourceDocument.php index 6d9580cd..e60a2b47 100644 --- a/src/ResourceDocument.php +++ b/src/ResourceDocument.php @@ -293,7 +293,7 @@ public function toArray(): array { */ public function addAttribute(string $key, mixed $value, array $options=[]): void { - $this->add($key, $value); + $this->add($key, $value, $options); } /** diff --git a/src/objects/ResourceObject.php b/src/objects/ResourceObject.php index ac62ff73..a92edc7e 100644 --- a/src/objects/ResourceObject.php +++ b/src/objects/ResourceObject.php @@ -185,7 +185,7 @@ public function hasIdentifierPropertiesOnly(): bool { */ public function addAttribute(string $key, mixed $value, array $options=[]): void { - $this->add($key, $value); + $this->add($key, $value, $options); } /** From a61efa31f12566a4e9f2ce77124491932f3c9f61 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 00:35:50 +0100 Subject: [PATCH 08/20] improving linting for phpstan level 6, mostly hinting array --- examples/bootstrap_examples.php | 37 +++++++++++++------ phpstan.neon | 2 +- rector.php | 2 + src/Document.php | 3 ++ src/ResourceDocument.php | 1 + src/exceptions/DuplicateException.php | 2 +- src/exceptions/InputException.php | 2 +- src/helpers/Converter.php | 3 ++ src/objects/AttributesObject.php | 2 + src/objects/RelationshipObject.php | 4 +- src/profiles/CursorPaginationProfile.php | 4 +- tests/CollectionDocumentTest.php | 13 ++++++- tests/ConverterTest.php | 18 ++++++--- tests/ErrorsDocumentTest.php | 5 ++- tests/ExampleOutputTest.php | 26 +++++++------ tests/ValidatorTest.php | 19 +++++++--- .../ExampleTimestampsProfile.php | 2 +- tests/example_output/ExampleUser.php | 10 ++--- .../ExampleVersionExtension.php | 2 +- .../at_members_everywhere.php | 2 +- .../at_members_in_errors.php | 2 +- .../example_output/collection/collection.php | 2 +- .../collection_canonical.php | 2 +- .../cursor_pagination_profile.php | 2 +- .../errors_all_options/errors_all_options.php | 2 +- .../errors_exception_native.php | 2 +- tests/example_output/extension/extension.php | 2 +- .../extension_members_everywhere.php | 2 +- tests/example_output/meta_only/meta_only.php | 2 +- .../null_values/null_values.php | 2 +- tests/example_output/profile/profile.php | 2 +- .../relationship_to_many_document.php | 2 +- .../relationship_to_one_document.php | 2 +- .../relationships/relationships.php | 2 +- .../resource_document_identifier_only.php | 2 +- .../resource_human_api/resource_human_api.php | 2 +- .../resource_links/resource_links.php | 2 +- .../resource_nested_relations.php | 2 +- .../resource_spec_api/resource_spec_api.php | 2 +- .../status_only/status_only.php | 2 +- tests/objects/RelationshipObjectTest.php | 10 ++++- 41 files changed, 135 insertions(+), 74 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index c18cf55c..d461a932 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -18,7 +18,8 @@ require_once __DIR__.'/../vendor/autoload.php'; class ExampleDataset { - private static $records = [ + /** @var array>> */ + private static array $records = [ 'articles' => [ 1 => [ 'title' => 'JSON:API paints my bikeshed!', @@ -58,7 +59,10 @@ class ExampleDataset { ], ]; - public static function getRecord($type, $id) { + /** + * @return array + */ + public static function getRecord(string $type, int $id): array { if (isset(self::$records[$type][$id]) === false) { throw new \Exception('sorry, we have a limited dataset'); } @@ -66,7 +70,10 @@ public static function getRecord($type, $id) { return self::$records[$type][$id]; } - public static function getEntity($type, $id) { + /** + * @return ExampleUser + */ + public static function getEntity(string $type, int $id): ExampleUser { $record = self::getRecord($type, $id); $user = new ExampleUser($id); @@ -77,11 +84,17 @@ public static function getEntity($type, $id) { return $user; } - public static function findRecords($type) { + /** + * @return array> + */ + public static function findRecords(string $type): array { return self::$records[$type]; } - public static function findEntities($type) { + /** + * @return ExampleUser[] + */ + public static function findEntities(string $type): array { $records = self::findRecords($type); $entities = []; @@ -94,15 +107,15 @@ public static function findEntities($type) { } class ExampleUser { - public $name; - public $heads; - public $unknown; + public string $name; + public int|string $heads; + public mixed $unknown; public function __construct( - public $id, + public int $id, ) {} - function getCurrentLocation() { + function getCurrentLocation(): string { return 'Earth'; } } @@ -124,7 +137,7 @@ public function getNamespace(): string { * optionally helpers for the specific extension */ - public function setVersion(ResourceInterface $resource, $version) { + public function setVersion(ResourceInterface $resource, string $version): void { if ($resource instanceof HasExtensionMembersInterface === false) { throw new \Exception('resource doesn\'t have extension members'); } @@ -155,7 +168,7 @@ public function setTimestamps( ResourceInterface & HasAttributesInterface $resource, ?\DateTimeInterface $created=null, ?\DateTimeInterface $updated=null, - ) { + ): void { $timestamps = []; if ($created !== null) { $timestamps['created'] = $created->format(\DateTime::ISO8601); diff --git a/phpstan.neon b/phpstan.neon index bbd71f3d..3cf6d585 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ parameters: # slowly increase - level: 5 + level: 6 paths: - src/ - tests/ diff --git a/rector.php b/rector.php index bba93d87..53d77893 100644 --- a/rector.php +++ b/rector.php @@ -4,6 +4,7 @@ use Rector\Config\RectorConfig; use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; +use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Class_\AddTestsVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; @@ -18,6 +19,7 @@ ->withRules([ DeclareStrictTypesRector::class, AddTestsVoidReturnTypeWhereNoReturnRector::class, + AddVoidReturnTypeWhereNoReturnRector::class, ]) ->withSkip([ // better explicit readability diff --git a/src/Document.php b/src/Document.php index 06bd2b51..77d8f01b 100644 --- a/src/Document.php +++ b/src/Document.php @@ -299,6 +299,9 @@ public function sendResponse(array $options=[]): void { * JsonSerializable */ + /** + * @return array + */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); diff --git a/src/ResourceDocument.php b/src/ResourceDocument.php index e60a2b47..3a74d14b 100644 --- a/src/ResourceDocument.php +++ b/src/ResourceDocument.php @@ -51,6 +51,7 @@ public function __construct(?string $type=null, string|int|null $id=null) { */ /** + * @param array $attributes * @param PHPStanTypeAlias_ResourceDocumentAndValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray( diff --git a/src/exceptions/DuplicateException.php b/src/exceptions/DuplicateException.php index a1c5c9a1..a1a7c176 100644 --- a/src/exceptions/DuplicateException.php +++ b/src/exceptions/DuplicateException.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\exceptions\Exception; class DuplicateException extends Exception { - public function __construct($message='', $code=409, $previous=null) { + public function __construct(string $message='', int $code=409, ?\Throwable $previous=null) { parent::__construct($message, $code, $previous); } } diff --git a/src/exceptions/InputException.php b/src/exceptions/InputException.php index ce121c14..4f049cb3 100644 --- a/src/exceptions/InputException.php +++ b/src/exceptions/InputException.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\exceptions\Exception; class InputException extends Exception { - public function __construct($message='', $code=400, $previous=null) { + public function __construct(string $message='', int $code=400, ?\Throwable $previous=null) { parent::__construct($message, $code, $previous); } } diff --git a/src/helpers/Converter.php b/src/helpers/Converter.php index b0c0edd3..0052e086 100644 --- a/src/helpers/Converter.php +++ b/src/helpers/Converter.php @@ -13,6 +13,9 @@ * @internal */ class Converter { + /** + * @return array + */ public static function objectToArray(object $object): array { if ($object instanceof ObjectInterface) { return $object->toArray(); diff --git a/src/objects/AttributesObject.php b/src/objects/AttributesObject.php index 86600ffe..6463e07e 100644 --- a/src/objects/AttributesObject.php +++ b/src/objects/AttributesObject.php @@ -24,6 +24,8 @@ class AttributesObject extends AbstractObject { /** * @note if an `id` is set inside $attributes, it is removed from there * it is common to find it inside, and not doing so will cause an exception + * + * @param array $attributes */ public static function fromArray(array $attributes): static { unset($attributes['id']); diff --git a/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php index 6d72df4b..4c74e41c 100644 --- a/src/objects/RelationshipObject.php +++ b/src/objects/RelationshipObject.php @@ -117,14 +117,14 @@ public static function fromCollectionDocument(CollectionDocument $collectionDocu } /** - * @param array $meta if given a LinkObject is added, otherwise a link string is added + * @param array $meta if given a LinkObject is added, otherwise a link string is added */ public function setSelfLink(string $href, array $meta=[]): void { $this->addLink('self', $href, $meta); } /** - * @param array $meta if given a LinkObject is added, otherwise a link string is added + * @param array $meta if given a LinkObject is added, otherwise a link string is added */ public function setRelatedLink(string $href, array $meta=[]): void { $this->addLink('related', $href, $meta); diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 399b5f86..784300b1 100644 --- a/src/profiles/CursorPaginationProfile.php +++ b/src/profiles/CursorPaginationProfile.php @@ -96,7 +96,7 @@ public function setCursor(ResourceInterface & HasMetaInterface $resource, string * * @param PaginableInterface & HasMetaInterface $paginable a CollectionDocument or RelationshipObject */ - public function setCount(PaginableInterface & HasMetaInterface $paginable, ?int $exactTotal=null, ?int $bestGuessTotal=null) { + public function setCount(PaginableInterface & HasMetaInterface $paginable, ?int $exactTotal=null, ?int $bestGuessTotal=null): void { $this->setPaginationMeta($paginable, $exactTotal, $bestGuessTotal); } @@ -114,7 +114,7 @@ public function generatePreviousLink(string $baseOrCurrentUrl, string $beforeCur /** * helper to get generate a correct page[after] link, use to apply manually */ - public function generateNextLink($baseOrCurrentUrl, $afterCursor) { + public function generateNextLink(string $baseOrCurrentUrl, string $afterCursor): string { return $this->setQueryParameter($baseOrCurrentUrl, 'page[after]', $afterCursor); } diff --git a/tests/CollectionDocumentTest.php b/tests/CollectionDocumentTest.php index b05004ab..fe0ce50f 100644 --- a/tests/CollectionDocumentTest.php +++ b/tests/CollectionDocumentTest.php @@ -101,7 +101,7 @@ public function testSetPaginationLinks_HappyPath(): void { } #[DataProvider('dataProviderSetPaginationLinks_IndividualLinks')] - public function testSetPaginationLinks_IndividualLinks($key, $previous, $next, $first, $last): void { + public function testSetPaginationLinks_IndividualLinks(?string $key, ?string $previous, ?string $next, ?string $first, ?string $last): void { $document = new CollectionDocument(); $document->setPaginationLinks($previous, $next, $first, $last); @@ -119,7 +119,16 @@ public function testSetPaginationLinks_IndividualLinks($key, $previous, $next, $ } } - public static function dataProviderSetPaginationLinks_IndividualLinks() { + /** + * @return array + */ + public static function dataProviderSetPaginationLinks_IndividualLinks(): array { return [ ['prev', 'https://jsonapi.org', null, null, null], ['next', null, 'https://jsonapi.org', null, null], diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index d2af386a..7bf54758 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -29,10 +29,10 @@ public function testObjectToArray_HappyPath(): void { public function testObjectToArray_MethodsAndPrivateProperties(): void { $object = new class { - public $foo = 'bar'; - public $baz = 42; - private $secret = 'value'; // @phpstan-ignore property.onlyWritten - public function method() {} + public string $foo = 'bar'; + public int $baz = 42; + private string $secret = 'value'; // @phpstan-ignore property.onlyWritten + public function method(): void {} }; $array = Converter::objectToArray($object); @@ -58,11 +58,17 @@ public function testObjectToArray_FromInternalObject(): void { } #[DataProvider('dataProviderCamelCaseToWords_HappyPath')] - public function testCamelCaseToWords_HappyPath($camelCase, $expectedOutput): void { + public function testCamelCaseToWords_HappyPath(string $camelCase, string $expectedOutput): void { parent::assertSame($expectedOutput, Converter::camelCaseToWords($camelCase)); } - public static function dataProviderCamelCaseToWords_HappyPath() { + /** + * @return array + */ + public static function dataProviderCamelCaseToWords_HappyPath(): array { return [ ['value', 'value'], ['camelValue', 'camel Value'], diff --git a/tests/ErrorsDocumentTest.php b/tests/ErrorsDocumentTest.php index c7952517..88b9b830 100644 --- a/tests/ErrorsDocumentTest.php +++ b/tests/ErrorsDocumentTest.php @@ -120,7 +120,10 @@ public function testDetermineHttpStatusCode_HappyPath(int $expectedAdvisedErrorC parent::assertSame($expectedAdvisedErrorCode, $advisedErrorCode); } - public static function dataProviderDetermineHttpStatusCode_HappyPath() { + /** + * @return array}> + */ + public static function dataProviderDetermineHttpStatusCode_HappyPath(): array { return [ [422, [422]], [422, [422, 422]], diff --git a/tests/ExampleOutputTest.php b/tests/ExampleOutputTest.php index 75a699f0..db6e9c70 100644 --- a/tests/ExampleOutputTest.php +++ b/tests/ExampleOutputTest.php @@ -6,21 +6,22 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use alsvanzelf\jsonapi\interfaces\DocumentInterface; /** * @group OutputOnly */ class ExampleOutputTest extends TestCase { - private static $defaults = [ + /** @var PHPStanTypeAlias_DocumentOptions */ + private static array $defaults = [ 'prettyPrint' => true, ]; #[DataProvider('dataProviderTestOutput')] - public function testOutput($generator, $expectedJson, array $options=[], $testName=null): void { - $options = [...self::$defaults, ...$options]; - + public function testOutput(object $generator, ?string $expectedJson, ?string $testName): void { + /** @var DocumentInterface $document */ $document = $generator::createJsonapiDocument(); - $actualJson = $document->toJson($options); + $actualJson = $document->toJson(self::$defaults); // adhere to editorconfig $actualJson = str_replace(' ', "\t", $actualJson).PHP_EOL; @@ -35,7 +36,14 @@ public function testOutput($generator, $expectedJson, array $options=[], $testNa parent::assertSame($expectedJson, $actualJson); } - public static function dataProviderTestOutput() { + /** + * @return array + */ + public static function dataProviderTestOutput(): array { $directories = glob(__DIR__.'/example_output/*', GLOB_ONLYDIR); $testCases = []; @@ -47,16 +55,12 @@ public static function dataProviderTestOutput() { $generator = new $className; $expectedJson = null; - $options = []; if (file_exists($directory.'/'.$testName.'.json')) { $expectedJson = file_get_contents($directory.'/'.$testName.'.json'); } - if (file_exists($directory.'/options.txt')) { - $options = json_decode(file_get_contents($directory.'/options.txt'), associative: true, flags: JSON_THROW_ON_ERROR); - } - $testCases[$testName] = [$generator, $expectedJson, $options, $testName]; + $testCases[$testName] = [$generator, $expectedJson, $testName]; } return $testCases; diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 6fa9235d..f93ce1f3 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -147,11 +147,14 @@ public function testClaimUsedResourceIdentifier_BlocksDuplicates(): void { #[DoesNotPerformAssertions] #[DataProvider('dataProviderCheckMemberName_HappyPath')] - public function testCheckMemberName_HappyPath($memberName): void { + public function testCheckMemberName_HappyPath(string $memberName): void { Validator::checkMemberName($memberName); } - public static function dataProviderCheckMemberName_HappyPath() { + /** + * @return array + */ + public static function dataProviderCheckMemberName_HappyPath(): array { return [ ['foo'], ['f_o'], @@ -162,13 +165,16 @@ public static function dataProviderCheckMemberName_HappyPath() { } #[DataProvider('dataProviderCheckMemberName_InvalidNames')] - public function testCheckMemberName_InvalidNames($memberName): void { + public function testCheckMemberName_InvalidNames(string $memberName): void { $this->expectException(InputException::class); Validator::checkMemberName($memberName); } - public static function dataProviderCheckMemberName_InvalidNames() { + /** + * @return array + */ + public static function dataProviderCheckMemberName_InvalidNames(): array { return [ ['_'], ['-'], @@ -178,10 +184,13 @@ public static function dataProviderCheckMemberName_InvalidNames() { } #[DataProvider('dataProviderCheckHttpStatusCode_HappyPath')] - public function testCheckHttpStatusCode_HappyPath($expectedOutput, $httpStatusCode): void { + public function testCheckHttpStatusCode_HappyPath(bool $expectedOutput, int|string $httpStatusCode): void { parent::assertSame($expectedOutput, Validator::checkHttpStatusCode($httpStatusCode)); } + /** + * @return array + */ public static function dataProviderCheckHttpStatusCode_HappyPath() { return [ [false, 42], diff --git a/tests/example_output/ExampleTimestampsProfile.php b/tests/example_output/ExampleTimestampsProfile.php index 4129a35f..24fc00e5 100644 --- a/tests/example_output/ExampleTimestampsProfile.php +++ b/tests/example_output/ExampleTimestampsProfile.php @@ -18,7 +18,7 @@ public function setTimestamps( ResourceInterface & HasAttributesInterface $resource, ?\DateTimeInterface $created=null, ?\DateTimeInterface $updated=null, - ) { + ): void { $timestamps = []; if ($created !== null) { $timestamps['created'] = $created->format(\DateTime::ISO8601); diff --git a/tests/example_output/ExampleUser.php b/tests/example_output/ExampleUser.php index b01edab3..c01cf47d 100644 --- a/tests/example_output/ExampleUser.php +++ b/tests/example_output/ExampleUser.php @@ -5,15 +5,15 @@ namespace alsvanzelf\jsonapiTests\example_output; class ExampleUser { - public $name; - public $heads; - public $unknown; + public ?string $name = null; + public null|int|string $heads = null; + public mixed $unknown = null; public function __construct( - public $id, + public int $id, ) {} - function getCurrentLocation() { + function getCurrentLocation(): string { return 'Earth'; } } diff --git a/tests/example_output/ExampleVersionExtension.php b/tests/example_output/ExampleVersionExtension.php index dea5cde6..e8061cd4 100644 --- a/tests/example_output/ExampleVersionExtension.php +++ b/tests/example_output/ExampleVersionExtension.php @@ -19,7 +19,7 @@ public function getNamespace(): string { return 'version'; } - public function setVersion(ResourceInterface $resource, $version) { + public function setVersion(ResourceInterface $resource, string $version): void { if ($resource instanceof HasExtensionMembersInterface === false) { throw new InputException('resource doesn\'t have extension members'); } diff --git a/tests/example_output/at_members_everywhere/at_members_everywhere.php b/tests/example_output/at_members_everywhere/at_members_everywhere.php index a6774a0d..49c1c29c 100644 --- a/tests/example_output/at_members_everywhere/at_members_everywhere.php +++ b/tests/example_output/at_members_everywhere/at_members_everywhere.php @@ -17,7 +17,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; class at_members_everywhere { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { /** * root */ diff --git a/tests/example_output/at_members_in_errors/at_members_in_errors.php b/tests/example_output/at_members_in_errors/at_members_in_errors.php index e937eb88..55783709 100644 --- a/tests/example_output/at_members_in_errors/at_members_in_errors.php +++ b/tests/example_output/at_members_in_errors/at_members_in_errors.php @@ -12,7 +12,7 @@ use alsvanzelf\jsonapi\objects\MetaObject; class at_members_in_errors { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ErrorsDocument { /** * root */ diff --git a/tests/example_output/collection/collection.php b/tests/example_output/collection/collection.php index a3a02659..a7a3e4f4 100644 --- a/tests/example_output/collection/collection.php +++ b/tests/example_output/collection/collection.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleUser; class collection { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): CollectionDocument { $user1 = new ExampleUser(1); $user1->name = 'Ford Prefect'; $user1->heads = 1; diff --git a/tests/example_output/collection_canonical/collection_canonical.php b/tests/example_output/collection_canonical/collection_canonical.php index a58f9451..3ea39447 100644 --- a/tests/example_output/collection_canonical/collection_canonical.php +++ b/tests/example_output/collection_canonical/collection_canonical.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; class collection_canonical { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): CollectionDocument { $articleRecords = [ 1 => [ 'title' => 'JSON:API paints my bikeshed!', diff --git a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php index 77363cdf..ccf26f69 100644 --- a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php +++ b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapi\profiles\CursorPaginationProfile; class cursor_pagination_profile { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): CollectionDocument { $profile = new CursorPaginationProfile(); $user1 = new ResourceObject('user', 1); diff --git a/tests/example_output/errors_all_options/errors_all_options.php b/tests/example_output/errors_all_options/errors_all_options.php index 6d44d3a8..fd9b3dc5 100644 --- a/tests/example_output/errors_all_options/errors_all_options.php +++ b/tests/example_output/errors_all_options/errors_all_options.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\ErrorObject; class errors_all_options { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ErrorsDocument { $errorHumanApi = new ErrorObject('Invalid input', 'Too much options', 'Please, choose a bit less. Consult your ...', 'https://www.example.com/explanation.html', 'https://www.example.com/documentation.html'); $errorSpecApi = new ErrorObject(); diff --git a/tests/example_output/errors_exception_native/errors_exception_native.php b/tests/example_output/errors_exception_native/errors_exception_native.php index 9cc0e54d..933a4d15 100644 --- a/tests/example_output/errors_exception_native/errors_exception_native.php +++ b/tests/example_output/errors_exception_native/errors_exception_native.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\ErrorsDocument; class errors_exception_native { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ErrorsDocument { $exception = new \Exception('unknown user', 404); $options = [ 'includeExceptionTrace' => false, diff --git a/tests/example_output/extension/extension.php b/tests/example_output/extension/extension.php index 8870a337..2648ebb3 100644 --- a/tests/example_output/extension/extension.php +++ b/tests/example_output/extension/extension.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleVersionExtension; class extension { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $extension = new ExampleVersionExtension(); $document = new ResourceDocument('user', 42); diff --git a/tests/example_output/extension_members_everywhere/extension_members_everywhere.php b/tests/example_output/extension_members_everywhere/extension_members_everywhere.php index 6790a1f3..9b4779fb 100644 --- a/tests/example_output/extension_members_everywhere/extension_members_everywhere.php +++ b/tests/example_output/extension_members_everywhere/extension_members_everywhere.php @@ -18,7 +18,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleEverywhereExtension; class extension_members_everywhere { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $extension = new ExampleEverywhereExtension(); $document = new ResourceDocument('user', 42); diff --git a/tests/example_output/meta_only/meta_only.php b/tests/example_output/meta_only/meta_only.php index 2bf360d4..2aee4e7d 100644 --- a/tests/example_output/meta_only/meta_only.php +++ b/tests/example_output/meta_only/meta_only.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\MetaDocument; class meta_only { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): MetaDocument { $document = new MetaDocument(); $document->addMeta('foo', 'bar'); diff --git a/tests/example_output/null_values/null_values.php b/tests/example_output/null_values/null_values.php index 488910b9..f8fd8e1e 100644 --- a/tests/example_output/null_values/null_values.php +++ b/tests/example_output/null_values/null_values.php @@ -10,7 +10,7 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; class null_values { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $document = new ResourceDocument('user', 42); $document->add('foo', null); diff --git a/tests/example_output/profile/profile.php b/tests/example_output/profile/profile.php index 56bdde48..0796a3ac 100644 --- a/tests/example_output/profile/profile.php +++ b/tests/example_output/profile/profile.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleTimestampsProfile; class profile { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $profile = new ExampleTimestampsProfile(); $document = new ResourceDocument('user', 42); diff --git a/tests/example_output/relationship_to_many_document/relationship_to_many_document.php b/tests/example_output/relationship_to_many_document/relationship_to_many_document.php index d5ea3561..de34adf6 100644 --- a/tests/example_output/relationship_to_many_document/relationship_to_many_document.php +++ b/tests/example_output/relationship_to_many_document/relationship_to_many_document.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\CollectionDocument; class relationship_to_many_document { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): CollectionDocument { $document = new CollectionDocument(); $document->add('tags', 2); $document->add('tags', 3); diff --git a/tests/example_output/relationship_to_one_document/relationship_to_one_document.php b/tests/example_output/relationship_to_one_document/relationship_to_one_document.php index 25979b76..1d5200a8 100644 --- a/tests/example_output/relationship_to_one_document/relationship_to_one_document.php +++ b/tests/example_output/relationship_to_one_document/relationship_to_one_document.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\enums\DocumentLevelEnum; class relationship_to_one_document { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $document = new ResourceDocument('author', 12); $document->setSelfLink('/articles/1/relationship/author', level: DocumentLevelEnum::Root); diff --git a/tests/example_output/relationships/relationships.php b/tests/example_output/relationships/relationships.php index 4fc9d1ed..8b01eef3 100644 --- a/tests/example_output/relationships/relationships.php +++ b/tests/example_output/relationships/relationships.php @@ -11,7 +11,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; class relationships { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $document = new ResourceDocument('user', 1); $ship1Resource = new ResourceObject('ship', 24); diff --git a/tests/example_output/resource_document_identifier_only/resource_document_identifier_only.php b/tests/example_output/resource_document_identifier_only/resource_document_identifier_only.php index 3c960584..72511a9a 100644 --- a/tests/example_output/resource_document_identifier_only/resource_document_identifier_only.php +++ b/tests/example_output/resource_document_identifier_only/resource_document_identifier_only.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\ResourceDocument; class resource_document_identifier_only { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { return new ResourceDocument('user', 42); } } diff --git a/tests/example_output/resource_human_api/resource_human_api.php b/tests/example_output/resource_human_api/resource_human_api.php index 7f7d3284..60a3e83d 100644 --- a/tests/example_output/resource_human_api/resource_human_api.php +++ b/tests/example_output/resource_human_api/resource_human_api.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleUser; class resource_human_api { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $user1 = new ExampleUser(1); $user1->name = 'Ford Prefect'; $user1->heads = 1; diff --git a/tests/example_output/resource_links/resource_links.php b/tests/example_output/resource_links/resource_links.php index 8ce95ac7..48caf52e 100644 --- a/tests/example_output/resource_links/resource_links.php +++ b/tests/example_output/resource_links/resource_links.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleUser; class resource_links { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $user42 = new ExampleUser(42); $user42->name = 'Zaphod Beeblebrox'; $user42->heads = 2; diff --git a/tests/example_output/resource_nested_relations/resource_nested_relations.php b/tests/example_output/resource_nested_relations/resource_nested_relations.php index 244c5574..a81c99f1 100644 --- a/tests/example_output/resource_nested_relations/resource_nested_relations.php +++ b/tests/example_output/resource_nested_relations/resource_nested_relations.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleUser; class resource_nested_relations { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $user42 = new ExampleUser(42); $user42->name = 'Zaphod Beeblebrox'; $user42->heads = 2; diff --git a/tests/example_output/resource_spec_api/resource_spec_api.php b/tests/example_output/resource_spec_api/resource_spec_api.php index 62bcc5e6..fd542c66 100644 --- a/tests/example_output/resource_spec_api/resource_spec_api.php +++ b/tests/example_output/resource_spec_api/resource_spec_api.php @@ -15,7 +15,7 @@ use alsvanzelf\jsonapiTests\example_output\ExampleUser; class resource_spec_api { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): ResourceDocument { $user1 = new ExampleUser(1); $user1->name = 'Ford Prefect'; $user1->heads = 1; diff --git a/tests/example_output/status_only/status_only.php b/tests/example_output/status_only/status_only.php index 209e9cb7..e141fdbf 100644 --- a/tests/example_output/status_only/status_only.php +++ b/tests/example_output/status_only/status_only.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\MetaDocument; class status_only { - public static function createJsonapiDocument() { + public static function createJsonapiDocument(): MetaDocument { $document = new MetaDocument(); $document->setHttpStatusCode(201); diff --git a/tests/objects/RelationshipObjectTest.php b/tests/objects/RelationshipObjectTest.php index e35a0a51..d784bf9f 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -311,7 +311,10 @@ public function testIsEmpty_WithExtensionMembers(): void { parent::assertFalse($relationshipObject->isEmpty()); } - private function validateToOneRelationshipArray(array $array) { + /** + * @param array $array + */ + private function validateToOneRelationshipArray(array $array): void { parent::assertNotEmpty($array); parent::assertArrayHasKey('data', $array); parent::assertArrayHasKey('type', $array['data']); @@ -320,7 +323,10 @@ private function validateToOneRelationshipArray(array $array) { parent::assertSame('42', $array['data']['id']); } - private function validateToManyRelationshipArray(array $array) { + /** + * @param array $array + */ + private function validateToManyRelationshipArray(array $array): void { parent::assertNotEmpty($array); parent::assertArrayHasKey('data', $array); parent::assertCount(1, $array['data']); From a73be4eed8a1721cbed6af312a731885f7e60330 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 11:10:31 +0100 Subject: [PATCH 09/20] satisfy phpstan level 7 with a few ignored errors --- examples/output.php | 2 +- phpstan.neon | 65 +++++++++++++++++++---- src/CollectionDocument.php | 4 +- src/Document.php | 10 +++- src/ErrorsDocument.php | 31 +++++------ src/ResourceDocument.php | 18 +++---- src/helpers/Converter.php | 7 +++ src/helpers/RequestParser.php | 64 +++++++++++++++------- src/helpers/Validator.php | 4 +- src/interfaces/DocumentInterface.php | 4 +- src/interfaces/HasAttributesInterface.php | 2 +- src/objects/ErrorObject.php | 4 +- src/objects/ResourceObject.php | 14 ++--- src/profiles/CursorPaginationProfile.php | 9 +++- tests/DocumentTest.php | 2 +- tests/ExampleOutputTest.php | 10 +++- tests/ResourceDocumentTest.php | 14 ++--- tests/SeparateProcessTest.php | 2 +- 18 files changed, 178 insertions(+), 88 deletions(-) diff --git a/examples/output.php b/examples/output.php index a0ac0592..b6e35c10 100644 --- a/examples/output.php +++ b/examples/output.php @@ -71,4 +71,4 @@ echo '

Send json response

'; echo '
$document->sendResponse();
'; -echo '

Echo\'s the result of $document->json() and sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.ContentTypeEnum::Official->value.']

'; +echo '

Echo\'s the result of $document->toJson() and sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.ContentTypeEnum::Official->value.']

'; diff --git a/phpstan.neon b/phpstan.neon index 3cf6d585..9b154452 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,22 +1,34 @@ parameters: # slowly increase - level: 6 + level: 7 paths: - src/ - tests/ - examples/ typeAliases: - PHPStanTypeAlias_CollectionDocumentOptions: 'array{includeContainedResources?: bool}' - PHPStanTypeAlias_DocumentOptions: 'array{encodeOptions?: int, prettyPrint?: bool, contentType?: \alsvanzelf\jsonapi\enums\ContentTypeEnum, array?: ?PHPStanTypeAlias_DocumentArray, json?: ?string, jsonpCallback?: ?string}' - PHPStanTypeAlias_ErrorsDocumentOptions: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool}' - PHPStanTypeAlias_ResourceDocumentOptions: 'array{includeContainedResources?: bool}' - PHPStanTypeAlias_ResourceDocumentAndValidatorOptions: 'array{includeContainedResources?: bool, enforceTypeFieldNamespace?: bool}' - PHPStanTypeAlias_ValidatorOptions: 'array{enforceTypeFieldNamespace?: bool}' - PHPStanTypeAlias_RequestParserOptions: 'array{useNestedIncludePaths?: bool, useAnnotatedSortFields?: bool}' - PHPStanTypeAlias_ErrorObjectOptions: 'array{includeExceptionTrace?: bool, stripExceptionBasePath?: ?string}' - PHPStanTypeAlias_ErrorsDocumentAndErrorObjectOptions: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool, stripExceptionBasePath?: ?string}' + PHPStanTypeAlias_Options_CollectionDocument: 'array{includeContainedResources?: bool}' + PHPStanTypeAlias_Options_Document: 'array{encodeOptions?: int, prettyPrint?: bool, contentType?: \alsvanzelf\jsonapi\enums\ContentTypeEnum, array?: ?PHPStanTypeAlias_DocumentArray, json?: ?string, jsonpCallback?: ?string}' + PHPStanTypeAlias_Options_ErrorsDocument: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool}' + PHPStanTypeAlias_Options_ResourceDocument: 'array{includeContainedResources?: bool}' + PHPStanTypeAlias_Options_ResourceDocumentAndValidator: 'array{includeContainedResources?: bool, enforceTypeFieldNamespace?: bool}' + PHPStanTypeAlias_Options_Validator: 'array{enforceTypeFieldNamespace?: bool}' + PHPStanTypeAlias_Options_RequestParser: 'array{useNestedIncludePaths?: bool, useAnnotatedSortFields?: bool}' + PHPStanTypeAlias_Options_ErrorObject: 'array{includeExceptionTrace?: bool, stripExceptionBasePath?: ?string}' + PHPStanTypeAlias_Options_ErrorsDocumentAndErrorObject: 'array{includeExceptionTrace?: bool, includeExceptionPrevious?: bool, stripExceptionBasePath?: ?string}' + + PHPStanTypeAlias_DefaultOptions_CollectionDocument: 'array{includeContainedResources: bool}' + PHPStanTypeAlias_DefaultOptions_Document: 'array{encodeOptions: int, prettyPrint: bool, contentType: \alsvanzelf\jsonapi\enums\ContentTypeEnum, array: ?PHPStanTypeAlias_DocumentArray, json: ?string, jsonpCallback: ?string}' + PHPStanTypeAlias_DefaultOptions_ErrorsDocument: 'array{includeExceptionTrace: bool, includeExceptionPrevious: bool}' + PHPStanTypeAlias_DefaultOptions_ResourceDocument: 'array{includeContainedResources: bool}' + PHPStanTypeAlias_DefaultOptions_ResourceDocumentAndValidator: 'array{includeContainedResources: bool, enforceTypeFieldNamespace: bool}' + PHPStanTypeAlias_DefaultOptions_Validator: 'array{enforceTypeFieldNamespace: bool}' + PHPStanTypeAlias_DefaultOptions_RequestParser: 'array{useNestedIncludePaths: bool, useAnnotatedSortFields: bool}' + PHPStanTypeAlias_DefaultOptions_ErrorObject: 'array{includeExceptionTrace: bool, stripExceptionBasePath: ?string}' + PHPStanTypeAlias_DefaultOptions_ErrorsDocumentAndErrorObject: 'array{includeExceptionTrace: bool, includeExceptionPrevious: bool, stripExceptionBasePath: ?string}' + PHPStanTypeAlias_DocumentArray: 'array' + PHPStanTypeAlias_QueryParameters: 'array{fields?: array, filter?: string|array, include?: string, page?: array, sort?: string}' treatPhpDocTypesAsCertain: true @@ -27,3 +39,36 @@ parameters: ignoreErrors: # add cases to ignore because they are too much work for now # @see https://phpstan.org/user-guide/ignoring-errors#ignoring-in-configuration-file + + # testing AtMemberManager trait + - messages: + - '#Call to an undefined method object::hasAtMembers\(\)\.#' + - '#Call to an undefined method object::getAtMembers\(\)\.#' + - '#Call to an undefined method object::addAtMember\(\)\.#' + identifier: method.notFound + path: tests/helpers/AtMemberManagerTest.php + + # testing ExtensionMemberManager trait + - messages: + - '#Call to an undefined method object::addExtensionMember\(\)\.#' + - '#Call to an undefined method object::hasExtensionMembers\(\)\.#' + - '#Call to an undefined method object::getExtensionMembers\(\)\.#' + identifier: method.notFound + path: tests/helpers/ExtensionMemberManagerTest.php + + # testing HttpStatusCodeManager trait + - messages: + - '#Call to an undefined method object::setHttpStatusCode\(\)\.#' + - '#Call to an undefined method object::hasHttpStatusCode\(\)\.#' + - '#Call to an undefined method object::getHttpStatusCode\(\)\.#' + identifier: method.notFound + path: tests/helpers/HttpStatusCodeManagerTest.php + + # testing LinksManager trait + - messages: + - '#Call to an undefined method object::addLink\(\)\.#' + - '#Call to an undefined method object::addLinkObject\(\)\.#' + - '#Call to an undefined method object::setLinksObject\(\)\.#' + - '#Call to an undefined method object::toArray\(\)\.#' + identifier: method.notFound + path: tests/helpers/LinksManagerTest.php diff --git a/src/CollectionDocument.php b/src/CollectionDocument.php index 9262966b..9bbed6e2 100644 --- a/src/CollectionDocument.php +++ b/src/CollectionDocument.php @@ -21,7 +21,7 @@ class CollectionDocument extends DataDocument implements PaginableInterface, ResourceContainerInterface { /** @var ResourceInterface[] */ protected array $resources = []; - /** @var PHPStanTypeAlias_CollectionDocumentOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_CollectionDocument */ protected static array $collectionDocumentDefaults = [ /** * add resources inside relationships to /included when adding resources to the collection @@ -89,7 +89,7 @@ public function setPaginationLinks( * * adds included resources if found inside the resource's relationships, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_CollectionDocumentOptions $options {@see CollectionDocument::$collectionDocumentDefaults} + * @param PHPStanTypeAlias_Options_CollectionDocument $options {@see CollectionDocument::$collectionDocumentDefaults} * * @throws InputException if the resource is empty */ diff --git a/src/Document.php b/src/Document.php index 77d8f01b..3f0276e9 100644 --- a/src/Document.php +++ b/src/Document.php @@ -47,7 +47,7 @@ abstract class Document implements DocumentInterface, \JsonSerializable, HasLink protected array $extensions = []; /** @var ProfileInterface[] */ protected array $profiles = []; - /** @var PHPStanTypeAlias_DocumentOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_Document */ protected static array $documentDefaults = [ /** * encode to json with these default options @@ -257,7 +257,8 @@ public function toArray(): array { } /** - * @throws \JsonException + * @throws \JsonException if encoding fails + * @throws Exception if encoding fails and $options['encodeOptions'] doesn't include JSON_THROW_ON_ERROR */ public function toJson(array $options=[]): string { $options = [...self::$documentDefaults, ...$options]; @@ -270,6 +271,11 @@ public function toJson(array $options=[]): string { $json = json_encode($array, $options['encodeOptions']); + // 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().', '.json_last_error_msg()); + } + if ($options['jsonpCallback'] !== null) { $json = $options['jsonpCallback'].'('.$json.')'; } diff --git a/src/ErrorsDocument.php b/src/ErrorsDocument.php index 8e6c7bc9..ca15c6a5 100644 --- a/src/ErrorsDocument.php +++ b/src/ErrorsDocument.php @@ -13,9 +13,9 @@ class ErrorsDocument extends Document { /** @var ErrorObject[] */ protected array $errors = []; - /** @var array> */ - protected array $httpStatusCodes; - /** @var PHPStanTypeAlias_ErrorsDocumentOptions */ + /** @var array> */ + protected array $httpStatusCodes = []; + /** @var PHPStanTypeAlias_DefaultOptions_ErrorsDocument */ protected static array $errorsDocumentDefaults = [ /** * add the trace of exceptions when adding exceptions @@ -42,7 +42,7 @@ public function __construct(?ErrorObject $errorObject=null) { */ /** - * @param PHPStanTypeAlias_ErrorsDocumentOptions $options {@see ErrorsDocument::$errorsDocumentDefaults} + * @param PHPStanTypeAlias_Options_ErrorsDocument $options {@see ErrorsDocument::$errorsDocumentDefaults} */ public static function fromException(\Throwable $exception, array $options=[]): static { $options = [...self::$errorsDocumentDefaults, ...$options]; @@ -58,7 +58,7 @@ public static function fromException(\Throwable $exception, array $options=[]): * * recursively adds multiple ErrorObjects if $exception carries a ->getPrevious() * - * @param PHPStanTypeAlias_ErrorsDocumentAndErrorObjectOptions $options {@see ErrorsDocument::$errorsDocumentDefaults} + * @param PHPStanTypeAlias_Options_ErrorsDocumentAndErrorObject $options {@see ErrorsDocument::$errorsDocumentDefaults} */ public function addException(\Throwable $exception, array $options=[]): void { $options = [...self::$errorsDocumentDefaults, ...$options]; @@ -137,27 +137,28 @@ public function toArray(): array { protected function determineHttpStatusCode(string|int $httpStatusCode): int { // add the new code $category = substr((string) $httpStatusCode, 0, 1); - $this->httpStatusCodes[$category][$httpStatusCode] = true; + $category .= 'xx'; // help phpstan understand the array-key is a string not an int + $this->httpStatusCodes[$category][(int) $httpStatusCode] = true; - $advisedStatusCode = $httpStatusCode; + $advisedStatusCode = (int) $httpStatusCode; // when there's multiple, give preference to 5xx errors - if (isset($this->httpStatusCodes['5']) && isset($this->httpStatusCodes['4'])) { + if (isset($this->httpStatusCodes['5xx']) && isset($this->httpStatusCodes['4xx'])) { // use a generic one $advisedStatusCode = 500; } - elseif (isset($this->httpStatusCodes['5'])) { - if (count($this->httpStatusCodes['5']) === 1) { - $advisedStatusCode = key($this->httpStatusCodes['5']); + elseif (isset($this->httpStatusCodes['5xx'])) { + if (count($this->httpStatusCodes['5xx']) === 1) { + $advisedStatusCode = key($this->httpStatusCodes['5xx']); } else { // use a generic one $advisedStatusCode = 500; } } - elseif (isset($this->httpStatusCodes['4'])) { - if (count($this->httpStatusCodes['4']) === 1) { - $advisedStatusCode = key($this->httpStatusCodes['4']); + elseif (isset($this->httpStatusCodes['4xx'])) { + if (count($this->httpStatusCodes['4xx']) === 1) { + $advisedStatusCode = key($this->httpStatusCodes['4xx']); } else { // use a generic one @@ -165,6 +166,6 @@ protected function determineHttpStatusCode(string|int $httpStatusCode): int { } } - return (int) $advisedStatusCode; + return $advisedStatusCode; } } diff --git a/src/ResourceDocument.php b/src/ResourceDocument.php index 3a74d14b..6b9f4b39 100644 --- a/src/ResourceDocument.php +++ b/src/ResourceDocument.php @@ -27,7 +27,7 @@ */ class ResourceDocument extends DataDocument implements HasAttributesInterface, ResourceInterface { protected ResourceIdentifierObject|ResourceObject $resource; - /** @var PHPStanTypeAlias_ResourceDocumentOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_ResourceDocument */ protected static array $resourceDocumentDefaults = [ /** * add resources inside relationships to /included when adding resources to the collection @@ -52,7 +52,7 @@ public function __construct(?string $type=null, string|int|null $id=null) { /** * @param array $attributes - * @param PHPStanTypeAlias_ResourceDocumentAndValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocumentAndValidator $options {@see ResourceDocument::$resourceDocumentDefaults} {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray( array $attributes, @@ -67,7 +67,7 @@ public static function fromArray( } /** - * @param PHPStanTypeAlias_ResourceDocumentAndValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocumentAndValidator $options {@see ResourceDocument::$resourceDocumentDefaults} */ public static function fromObject( object $attributes, @@ -84,7 +84,7 @@ public static function fromObject( * add key-value pairs to the resource's attributes * * @param mixed $value objects will be converted using `get_object_vars()` - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function add(string $key, mixed $value, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -102,7 +102,7 @@ public function add(string $key, mixed $value, array $options=[]): void { * @param CollectionDocument|ResourceInterface|ResourceInterface[]|null $relation * @param array $links * @param array $meta - * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function addRelationship( string $key, @@ -189,7 +189,7 @@ public function setLocalId(string|int $localId): void { } /** - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public function setAttributesObject(AttributesObject $attributesObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -204,7 +204,7 @@ public function setAttributesObject(AttributesObject $attributesObject, array $o * * adds included resources if found inside the RelationshipObject, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function addRelationshipObject(string $key, RelationshipObject $relationshipObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -225,7 +225,7 @@ public function addRelationshipObject(string $key, RelationshipObject $relations * * adds included resources if found inside the RelationshipObjects inside the RelationshipsObject, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function setRelationshipsObject(RelationshipsObject $relationshipsObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -250,7 +250,7 @@ public function setRelationshipsObject(RelationshipsObject $relationshipsObject, * * adds included resources if found inside the resource's relationships, unless $options['includeContainedResources'] is set to false * - * @param PHPStanTypeAlias_ResourceDocumentOptions $options {@see ResourceDocument::$resourceDocumentDefaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} * * @throws InputException if the $resource is a ResourceDocument itself */ diff --git a/src/helpers/Converter.php b/src/helpers/Converter.php index 0052e086..5851a686 100644 --- a/src/helpers/Converter.php +++ b/src/helpers/Converter.php @@ -5,6 +5,7 @@ namespace alsvanzelf\jsonapi\helpers; use alsvanzelf\jsonapi\enums\ContentTypeEnum; +use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; @@ -26,10 +27,16 @@ public static function objectToArray(object $object): array { /** * @see https://stackoverflow.com/questions/7593969/regex-to-split-camelcase-or-titlecase-advanced/7599674#7599674 + * + * @throws Exception if string is impossible to split to words */ public static function camelCaseToWords(string $camelCase): string { $parts = preg_split('/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/', $camelCase); + if ($parts === false) { + throw new Exception('failed to convert camel case string to words'); + } + return implode(' ', $parts); } diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index e2a951c3..00830f3a 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -15,7 +15,7 @@ * that might break the class since we use `new static()` */ class RequestParser { - /** @var PHPStanTypeAlias_RequestParserOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_RequestParser */ protected static array $requestParserDefaults = [ /** * reformat the include query parameter paths to nested arrays @@ -31,9 +31,9 @@ class RequestParser { ]; /** - * @param string $selfLink the uri used to make this request {@see getSelfLink()} - * @param array> $queryParameters all query parameters defined by the specification - * @param array $document the request jsonapi document + * @param string $selfLink the uri used to make this request {@see getSelfLink()} + * @param PHPStanTypeAlias_QueryParameters $queryParameters all query parameters defined by the specification + * @param array $document the request jsonapi document * * @throws \JsonException if $document's content type is json but it can't be json decoded */ @@ -57,7 +57,7 @@ public static function fromSuperglobals(): static { $documentIsJson = (str_contains((string) $_SERVER['CONTENT_TYPE'], ContentTypeEnum::Debug->value)); $document = file_get_contents('php://input'); - if ($document === '') { + if ($document === '' || $document === false) { $document = []; } elseif ($documentIsJsonapi || $documentIsJson) { @@ -102,7 +102,7 @@ public function getSelfLink(): string { } public function hasIncludePaths(): bool { - return isset($this->queryParameters['include']); + return $this->hasQueryParameter('include'); } /** @@ -111,15 +111,15 @@ public function hasIncludePaths(): bool { * the nested format allows easier processing on each step of the chain * the raw format allows for custom processing * - * @param PHPStanTypeAlias_RequestParserOptions $options {@see RequestParser::$requestParserDefaults} + * @param PHPStanTypeAlias_Options_RequestParser $options {@see RequestParser::$requestParserDefaults} * @return array|array */ public function getIncludePaths(array $options=[]): array { - if ($this->queryParameters['include'] === '') { + if ($this->getQueryParameter('include') === '') { return []; } - $includePaths = explode(',', (string) $this->queryParameters['include']); + $includePaths = explode(',', $this->getQueryParameter('include')); $options = [...self::$requestParserDefaults, ...$options]; if ($options['useNestedIncludePaths'] === false) { @@ -143,22 +143,22 @@ public function getIncludePaths(array $options=[]): array { } public function hasSparseFieldset(string $type): bool { - return isset($this->queryParameters['fields'][$type]); + return isset($this->getQueryParameter('fields')[$type]); } /** * @return string[] */ public function getSparseFieldset(string $type): array { - if ($this->queryParameters['fields'][$type] === '') { + if ($this->getQueryParameter('fields')[$type] === '') { return []; } - return explode(',', (string) $this->queryParameters['fields'][$type]); + return explode(',', $this->getQueryParameter('fields')[$type]); } public function hasSortFields(): bool { - return isset($this->queryParameters['sort']); + return $this->hasQueryParameter('sort'); } /** @@ -169,18 +169,18 @@ public function hasSortFields(): bool { * * @todo return some kind of SortFieldObject * - * @param PHPStanTypeAlias_RequestParserOptions $options {@see RequestParser::$requestParserDefaults} + * @param PHPStanTypeAlias_Options_RequestParser $options {@see RequestParser::$requestParserDefaults} * @return string[]|array */ public function getSortFields(array $options=[]): array { - if ($this->queryParameters['sort'] === '') { + if ($this->getQueryParameter('sort') === '') { return []; } - $fields = explode(',', (string) $this->queryParameters['sort']); + $fields = explode(',', $this->getQueryParameter('sort')); $options = [...self::$requestParserDefaults, ...$options]; if ($options['useAnnotatedSortFields'] === false) { @@ -206,7 +206,7 @@ public function getSortFields(array $options=[]): array { } public function hasPagination(): bool { - return isset($this->queryParameters['page']); + return $this->hasQueryParameter('page'); } /** @@ -216,18 +216,18 @@ public function hasPagination(): bool { * @return array */ public function getPagination(): array { - return $this->queryParameters['page']; + return $this->getQueryParameter('page'); } public function hasFilter(): bool { - return isset($this->queryParameters['filter']); + return $this->hasQueryParameter('filter'); } /** * @return string|array */ public function getFilter(): string|array { - return $this->queryParameters['filter']; + return $this->getQueryParameter('filter'); } public function hasLocalId(): bool { @@ -294,4 +294,28 @@ public function getMeta(string $metaKey): mixed { public function getDocument(): array { return $this->document; } + + /** + * @internal + */ + protected function hasQueryParameter(string $key): bool { + return isset($this->queryParameters[$key]); + } + + /** + * @internal + * + * @return ($key is 'fields'|'page' ? array : string) + */ + protected function getQueryParameter(string $key): string|array { + return match ($key) { + // array shape + 'fields', 'page' => $this->queryParameters[$key] ?? [], + // string shape + 'include', 'sort' => $this->queryParameters[$key] ?? '', + // mixed shape + 'filter' => $this->queryParameters[$key] ?? '', + default => $this->queryParameters[$key] ?? '', + }; + } } diff --git a/src/helpers/Validator.php b/src/helpers/Validator.php index 40f81fac..981168c3 100644 --- a/src/helpers/Validator.php +++ b/src/helpers/Validator.php @@ -17,7 +17,7 @@ class Validator { protected array $usedFields = []; /** @var array */ protected array $usedResourceIdentifiers = []; - /** @var PHPStanTypeAlias_ValidatorOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_Validator */ protected static array $validatorDefaults = [ /** * blocks 'type' as a keyword inside attributes or relationships @@ -33,7 +33,7 @@ class Validator { * @see https://jsonapi.org/format/1.1/#document-resource-object-fields * * @param string[] $fieldNames - * @param PHPStanTypeAlias_ValidatorOptions $options {@see Validator::$validatorDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see Validator::$validatorDefaults} * * @throws DuplicateException */ diff --git a/src/interfaces/DocumentInterface.php b/src/interfaces/DocumentInterface.php index fc400dd9..5463efc7 100644 --- a/src/interfaces/DocumentInterface.php +++ b/src/interfaces/DocumentInterface.php @@ -17,7 +17,7 @@ public function toArray(): array; /** * generate json with the contents of the document, used by {@see ->sendResponse()} * - * @param PHPStanTypeAlias_DocumentOptions $options + * @param PHPStanTypeAlias_Options_Document $options * * @throws Exception if generating json fails */ @@ -28,7 +28,7 @@ public function toJson(array $options=[]): string; * * @note will set http status code and content type, and echo json * - * @param PHPStanTypeAlias_DocumentOptions $options + * @param PHPStanTypeAlias_Options_Document $options */ public function sendResponse(array $options=[]): void; } diff --git a/src/interfaces/HasAttributesInterface.php b/src/interfaces/HasAttributesInterface.php index 15f37e54..cfa16bf1 100644 --- a/src/interfaces/HasAttributesInterface.php +++ b/src/interfaces/HasAttributesInterface.php @@ -10,7 +10,7 @@ interface HasAttributesInterface { * * @see Validator::$validatorDefaults * - * @param PHPStanTypeAlias_ValidatorOptions $options + * @param PHPStanTypeAlias_Options_Validator $options */ public function addAttribute(string $key, mixed $value, array $options=[]): void; } diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index 6ab16982..23e07c75 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -29,7 +29,7 @@ class ErrorObject extends AbstractObject implements HasLinksInterface, HasMetaIn /** @var array{pointer?: string, parameter?: string, header?: string} */ protected array $source = []; protected MetaObject $meta; - /** @var PHPStanTypeAlias_ErrorObjectOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_ErrorObject */ protected static array $errorObjectDefaults = [ /** * add the trace of exceptions when adding exceptions @@ -71,7 +71,7 @@ public function __construct( */ /** - * @param PHPStanTypeAlias_ErrorObjectOptions $options {@see ErrorObject::$errorObjectDefaults} + * @param PHPStanTypeAlias_Options_ErrorObject $options {@see ErrorObject::$errorObjectDefaults} */ public static function fromException(\Throwable $exception, array $options=[]): static { $options = [...self::$errorObjectDefaults, ...$options]; diff --git a/src/objects/ResourceObject.php b/src/objects/ResourceObject.php index a92edc7e..c42b4697 100644 --- a/src/objects/ResourceObject.php +++ b/src/objects/ResourceObject.php @@ -23,7 +23,7 @@ class ResourceObject extends ResourceIdentifierObject implements HasAttributesIn protected AttributesObject $attributes; protected RelationshipsObject $relationships; - /** @var PHPStanTypeAlias_ValidatorOptions */ + /** @var PHPStanTypeAlias_DefaultOptions_Validator */ protected static array $resourceObjectDefaults = [ /** * blocks 'type' as a keyword inside attributes or relationships @@ -43,7 +43,7 @@ class ResourceObject extends ResourceIdentifierObject implements HasAttributesIn * it is common to find it inside, and not doing so will cause an exception * * @param array $attributes - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray(array $attributes, ?string $type=null, string|int|null $id=null, array $options=[]): static { if (isset($attributes['id'])) { @@ -61,7 +61,7 @@ public static function fromArray(array $attributes, ?string $type=null, string|i } /** - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public static function fromObject(object $attributes, ?string $type=null, string|int|null $id=null, array $options=[]): static { $array = Converter::objectToArray($attributes); @@ -72,7 +72,7 @@ public static function fromObject(object $attributes, ?string $type=null, string /** * add key-value pairs to attributes * - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public function add(string $key, mixed $value, array $options=[]): void { $options = [...self::$resourceObjectDefaults, ...$options]; @@ -90,7 +90,7 @@ public function add(string $key, mixed $value, array $options=[]): void { * @param CollectionDocument|ResourceInterface|ResourceInterface[]|null $relation * @param array $links * @param array $meta - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public function addRelationship( string $key, @@ -118,7 +118,7 @@ public function setSelfLink(string $href, array $meta=[]): void { */ /** - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public function setAttributesObject(AttributesObject $attributesObject, array $options=[]): void { $newKeys = $attributesObject->getKeys(); @@ -129,7 +129,7 @@ public function setAttributesObject(AttributesObject $attributesObject, array $o } /** - * @param PHPStanTypeAlias_ValidatorOptions $options {@see ResourceObject::$resourceObjectDefaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} * * @throws DuplicateException if the resource is contained as a resource in the relationship */ diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 784300b1..72c31c0e 100644 --- a/src/profiles/CursorPaginationProfile.php +++ b/src/profiles/CursorPaginationProfile.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\ResourceDocument; use alsvanzelf\jsonapi\enums\DocumentLevelEnum; +use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\HasLinksInterface; use alsvanzelf\jsonapi\interfaces\HasMetaInterface; @@ -304,9 +305,15 @@ public function getRangePaginationNotSupportedErrorObject(?string $genericTitle= /** * add or adjust a key in the query string of a url + * + * @throws InputException on missing or broken query parameters inside $url */ private function setQueryParameter(string $url, string $key, string $value): string { - $originalQuery = parse_url($url, PHP_URL_QUERY); + $originalQuery = parse_url($url, PHP_URL_QUERY); + if ($originalQuery === null || $originalQuery === false) { + throw new InputException('missing or broken query parameters in url'); + } + $decodedQuery = urldecode($originalQuery); $originalIsEncoded = ($decodedQuery !== $originalQuery); diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 625088ef..f3fdab02 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase; class DocumentTest extends TestCase { - private object $document; + private Document $document; public function setUp(): void { /** diff --git a/tests/ExampleOutputTest.php b/tests/ExampleOutputTest.php index db6e9c70..2d393e9e 100644 --- a/tests/ExampleOutputTest.php +++ b/tests/ExampleOutputTest.php @@ -12,7 +12,7 @@ * @group OutputOnly */ class ExampleOutputTest extends TestCase { - /** @var PHPStanTypeAlias_DocumentOptions */ + /** @var PHPStanTypeAlias_Options_Document */ private static array $defaults = [ 'prettyPrint' => true, ]; @@ -20,7 +20,7 @@ class ExampleOutputTest extends TestCase { #[DataProvider('dataProviderTestOutput')] public function testOutput(object $generator, ?string $expectedJson, ?string $testName): void { /** @var DocumentInterface $document */ - $document = $generator::createJsonapiDocument(); + $document = $generator::createJsonapiDocument(); // @phpstan-ignore staticMethod.notFound $actualJson = $document->toJson(self::$defaults); // adhere to editorconfig @@ -45,6 +45,9 @@ public function testOutput(object $generator, ?string $expectedJson, ?string $te */ public static function dataProviderTestOutput(): array { $directories = glob(__DIR__.'/example_output/*', GLOB_ONLYDIR); + if ($directories === false) { + throw new \Exception('failed to fetch example output'); + } $testCases = []; foreach ($directories as $directory) { @@ -58,6 +61,9 @@ public static function dataProviderTestOutput(): array { if (file_exists($directory.'/'.$testName.'.json')) { $expectedJson = file_get_contents($directory.'/'.$testName.'.json'); + if ($expectedJson === false) { + throw new \Exception('something went wrong fetching expected output'); + } } $testCases[$testName] = [$generator, $expectedJson, $testName]; diff --git a/tests/ResourceDocumentTest.php b/tests/ResourceDocumentTest.php index f9a2276c..b72882e2 100644 --- a/tests/ResourceDocumentTest.php +++ b/tests/ResourceDocumentTest.php @@ -171,18 +171,12 @@ public function testAddMeta_HappyPath(): void { parent::assertArrayHasKey('meta', $array); parent::assertArrayHasKey('data', $array); - parent::assertArrayHasKey('meta', $array['data']); parent::assertArrayHasKey('jsonapi', $array); + parent::assertArrayHasKey('meta', $array['data']); parent::assertArrayHasKey('meta', $array['jsonapi']); - parent::assertArrayHasKey('foo', $array['meta']); - parent::assertArrayHasKey('bar', $array['data']['meta']); - parent::assertArrayHasKey('baz', $array['jsonapi']['meta']); - parent::assertCount(1, $array['meta']); - parent::assertCount(1, $array['data']['meta']); - parent::assertCount(1, $array['jsonapi']['meta']); - parent::assertSame('root', $array['meta']['foo']); - parent::assertSame('resource', $array['data']['meta']['bar']); - parent::assertSame('jsonapi', $array['jsonapi']['meta']['baz']); + parent::assertSame(['foo' => 'root'], $array['meta']); + parent::assertSame(['bar' => 'resource'], $array['data']['meta']); + parent::assertSame(['baz' => 'jsonapi'], $array['jsonapi']['meta']); } public function testAddMeta_RecreateJsonapiObject(): void { diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 2dc68ce1..5b760bbe 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -14,7 +14,7 @@ * @group SeparateProcess */ class SeparateProcessTest extends TestCase { - private object $document; + private Document $document; public function setUp(): void { /** From 94b22ac657d78a3b272918529963c95e96bd83dc Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 11:19:00 +0100 Subject: [PATCH 10/20] easy to reach phpstan level 8 --- phpstan.neon | 2 +- src/Document.php | 1 + src/objects/LinksObject.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 9b154452..a4a3938a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ parameters: # slowly increase - level: 7 + level: 8 paths: - src/ - tests/ diff --git a/src/Document.php b/src/Document.php index 3f0276e9..074f36c8 100644 --- a/src/Document.php +++ b/src/Document.php @@ -173,6 +173,7 @@ public function setMetaObject(MetaObject $metaObject): void { $this->meta = $metaObject; } + /** @phpstan-assert JsonapiObject $this->jsonapi */ public function setJsonapiObject(JsonapiObject $jsonapiObject): void { $this->jsonapi = $jsonapiObject; } diff --git a/src/objects/LinksObject.php b/src/objects/LinksObject.php index 3485f309..29314bc1 100644 --- a/src/objects/LinksObject.php +++ b/src/objects/LinksObject.php @@ -16,7 +16,7 @@ * that might break the class since we use `new static()` */ class LinksObject extends AbstractObject { - /** @var array */ + /** @var array */ protected array $links = []; /** From b8d4e9715003a740eb5ea813b28568bf6a648e37 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 11:55:14 +0100 Subject: [PATCH 11/20] check against phpstan level 9 --- examples/bootstrap_examples.php | 6 ++-- phpstan.bonus.neon | 2 +- phpstan.neon | 52 ++++++++++++++++++++++-------- src/helpers/RequestParser.php | 16 +++++++-- src/objects/LinksObject.php | 1 + tests/helpers/LinksManagerTest.php | 2 +- 6 files changed, 58 insertions(+), 21 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index d461a932..fb8ae4a6 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -18,7 +18,7 @@ require_once __DIR__.'/../vendor/autoload.php'; class ExampleDataset { - /** @var array>> */ + /** @var array>> */ private static array $records = [ 'articles' => [ 1 => [ @@ -60,7 +60,7 @@ class ExampleDataset { ]; /** - * @return array + * @return array */ public static function getRecord(string $type, int $id): array { if (isset(self::$records[$type][$id]) === false) { @@ -85,7 +85,7 @@ public static function getEntity(string $type, int $id): ExampleUser { } /** - * @return array> + * @return array> */ public static function findRecords(string $type): array { return self::$records[$type]; diff --git a/phpstan.bonus.neon b/phpstan.bonus.neon index ddc4b487..f642c3fd 100644 --- a/phpstan.bonus.neon +++ b/phpstan.bonus.neon @@ -3,7 +3,7 @@ includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: - level: 10 + level: max # @see https://github.com/phpstan/phpstan-strict-rules strictRules: diff --git a/phpstan.neon b/phpstan.neon index a4a3938a..e33f93b1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ parameters: # slowly increase - level: 8 + level: 9 paths: - src/ - tests/ @@ -42,33 +42,57 @@ parameters: # testing AtMemberManager trait - messages: - - '#Call to an undefined method object::hasAtMembers\(\)\.#' - - '#Call to an undefined method object::getAtMembers\(\)\.#' - - '#Call to an undefined method object::addAtMember\(\)\.#' + - '#Call to an undefined method object::hasAtMembers\(\)#' + - '#Call to an undefined method object::getAtMembers\(\)#' + - '#Call to an undefined method object::addAtMember\(\)#' identifier: method.notFound path: tests/helpers/AtMemberManagerTest.php # testing ExtensionMemberManager trait - messages: - - '#Call to an undefined method object::addExtensionMember\(\)\.#' - - '#Call to an undefined method object::hasExtensionMembers\(\)\.#' - - '#Call to an undefined method object::getExtensionMembers\(\)\.#' + - '#Call to an undefined method object::addExtensionMember\(\)#' + - '#Call to an undefined method object::hasExtensionMembers\(\)#' + - '#Call to an undefined method object::getExtensionMembers\(\)#' identifier: method.notFound path: tests/helpers/ExtensionMemberManagerTest.php # testing HttpStatusCodeManager trait - messages: - - '#Call to an undefined method object::setHttpStatusCode\(\)\.#' - - '#Call to an undefined method object::hasHttpStatusCode\(\)\.#' - - '#Call to an undefined method object::getHttpStatusCode\(\)\.#' + - '#Call to an undefined method object::setHttpStatusCode\(\)#' + - '#Call to an undefined method object::hasHttpStatusCode\(\)#' + - '#Call to an undefined method object::getHttpStatusCode\(\)#' identifier: method.notFound path: tests/helpers/HttpStatusCodeManagerTest.php # testing LinksManager trait - messages: - - '#Call to an undefined method object::addLink\(\)\.#' - - '#Call to an undefined method object::addLinkObject\(\)\.#' - - '#Call to an undefined method object::setLinksObject\(\)\.#' - - '#Call to an undefined method object::toArray\(\)\.#' + - '#Call to an undefined method object::addLink\(\)#' + - '#Call to an undefined method object::addLinkObject\(\)#' + - '#Call to an undefined method object::setLinksObject\(\)#' + - '#Call to an undefined method object::toArray\(\)#' identifier: method.notFound path: tests/helpers/LinksManagerTest.php + + # mixed is only used for input from library implementations + - identifier: argument.type + message: '#Parameter \#\d \$.+ of function .+ expects .+, mixed given#' + path: src/helpers/RequestParser.php + + # mixed is only used for input from library implementations + - identifier: argument.type + message: '#Parameter \#\d \$.+ of class .+ constructor expects .+, mixed given#' + paths: + - src/helpers/RequestParser.php + - src/objects/ResourceObject.php + + # mixed is only used for input from library implementations + - identifier: argument.type + message: '#Parameter \#\d \$.+ of static method .+ expects .+, mixed given#' + path: tests/* + + # mixed is only used for input from library implementations + - identifier: offsetAccess.nonOffsetAccessible + message: '#Cannot access offset .+ on mixed#' + paths: + - src/helpers/RequestParser.php + - tests/* diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index 00830f3a..fafa1b72 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -44,11 +44,21 @@ public function __construct( ) {} public static function fromSuperglobals(): static { + /** + * @var array{ + * REQUEST_SCHEME?: string, + * HTTP_HOST?: string, + * REQUEST_URI?: string, + * CONTENT_TYPE?: string + * } $_SERVER + */ + $selfLink = ''; if (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['HTTP_HOST']) && isset($_SERVER['REQUEST_URI'])) { $selfLink = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; } + /** @var PHPStanTypeAlias_QueryParameters $queryParameters */ $queryParameters = $_GET; $document = $_POST; @@ -82,6 +92,8 @@ public static function fromPsrRequest(ServerRequestInterface|RequestInterface $r parse_str($request->getUri()->getQuery(), $queryParameters); } + /** @var PHPStanTypeAlias_QueryParameters $queryParameters */ + if ($request->getBody()->getContents() === '') { $document = []; } @@ -235,7 +247,7 @@ public function hasLocalId(): bool { } public function getLocalId(): string { - return $this->document['data']['lid']; + return $this->document['data']['lid']; // @phpstan-ignore return.type (implementation returns mixed) } public function hasAttribute(string $attributeName): bool { @@ -270,7 +282,7 @@ public function hasRelationship(string $relationshipName): bool { * @return ?array */ public function getRelationship(string $relationshipName): ?array { - return $this->document['data']['relationships'][$relationshipName]; + return $this->document['data']['relationships'][$relationshipName]; // @phpstan-ignore return.type (implementation returns mixed) } public function hasMeta(string $metaKey): bool { diff --git a/src/objects/LinksObject.php b/src/objects/LinksObject.php index 29314bc1..e8eb7595 100644 --- a/src/objects/LinksObject.php +++ b/src/objects/LinksObject.php @@ -37,6 +37,7 @@ public static function fromArray(array $links): LinksObject { } public static function fromObject(object $links): LinksObject { + /** @var array $array */ $array = Converter::objectToArray($links); return static::fromArray($array); diff --git a/tests/helpers/LinksManagerTest.php b/tests/helpers/LinksManagerTest.php index 47df373c..9376220e 100644 --- a/tests/helpers/LinksManagerTest.php +++ b/tests/helpers/LinksManagerTest.php @@ -20,7 +20,7 @@ public function setUp(): void { * @return array */ public function toArray(): array { - return $this->links->toArray(); + return $this->links->toArray(); // @phpstan-ignore return.type (toArray() methods don't have explicit array shapes yet) } }; } From 40a07d26bb4e3b1edf284f8a9d3e5161284723fc Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 12:10:47 +0100 Subject: [PATCH 12/20] cleanup with increased rector levels --- examples/bootstrap_examples.php | 3 --- rector.php | 22 +++++++++++-------- src/MetaDocument.php | 3 --- src/helpers/RequestParser.php | 4 ++-- src/objects/RelationshipsObject.php | 2 +- tests/ValidatorTest.php | 2 +- tests/helpers/RequestParserTest.php | 4 ---- tests/objects/ErrorObjectTest.php | 1 - .../objects/ResourceIdentifierObjectTest.php | 2 +- 9 files changed, 18 insertions(+), 25 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index fb8ae4a6..9cfec325 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -70,9 +70,6 @@ public static function getRecord(string $type, int $id): array { return self::$records[$type][$id]; } - /** - * @return ExampleUser - */ public static function getEntity(string $type, int $id): ExampleUser { $record = self::getRecord($type, $id); diff --git a/rector.php b/rector.php index 53d77893..26ac1608 100644 --- a/rector.php +++ b/rector.php @@ -2,10 +2,10 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector; use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; -use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; -use Rector\TypeDeclaration\Rector\Class_\AddTestsVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; // @see https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md for more rules @@ -18,12 +18,16 @@ ]) ->withRules([ DeclareStrictTypesRector::class, - AddTestsVoidReturnTypeWhereNoReturnRector::class, - AddVoidReturnTypeWhereNoReturnRector::class, ]) ->withSkip([ // better explicit readability IfIssetToCoalescingRector::class, + // better explicit readability + SimplifyUselessVariableRector::class, + // explicit testing private properties + RemoveUnusedPrivatePropertyRector::class => [ + 'tests/ConverterTest.php', + ], ]) // tab-based indenting @@ -32,9 +36,9 @@ // lowest supported php version ->withPhpSets(php82: true) - // slowly increase levels - ->withTypeCoverageLevel(1) - ->withDeadCodeLevel(1) - - // @todo add `->withPreparedSets()` once on a higher level with other rules + // slowly expand keys + ->withPreparedSets( + deadCode: true, + typeDeclarations: true, + ) ; diff --git a/src/MetaDocument.php b/src/MetaDocument.php index 585daf95..40d0a599 100644 --- a/src/MetaDocument.php +++ b/src/MetaDocument.php @@ -28,9 +28,6 @@ public static function fromArray(array $meta): static { return $metaDocument; } - /** - * @param object $meta - */ public static function fromObject(object $meta): static { $array = Converter::objectToArray($meta); diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index fafa1b72..5c2ac6ad 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -63,8 +63,8 @@ public static function fromSuperglobals(): static { $document = $_POST; if ($document === [] && isset($_SERVER['CONTENT_TYPE'])) { - $documentIsJsonapi = (str_contains((string) $_SERVER['CONTENT_TYPE'], ContentTypeEnum::Official->value)); - $documentIsJson = (str_contains((string) $_SERVER['CONTENT_TYPE'], ContentTypeEnum::Debug->value)); + $documentIsJsonapi = (str_contains($_SERVER['CONTENT_TYPE'], ContentTypeEnum::Official->value)); + $documentIsJson = (str_contains($_SERVER['CONTENT_TYPE'], ContentTypeEnum::Debug->value)); $document = file_get_contents('php://input'); if ($document === '' || $document === false) { diff --git a/src/objects/RelationshipsObject.php b/src/objects/RelationshipsObject.php index 537f909c..f5e02a0e 100644 --- a/src/objects/RelationshipsObject.php +++ b/src/objects/RelationshipsObject.php @@ -29,7 +29,7 @@ class RelationshipsObject extends AbstractObject implements RecursiveResourceCon */ public function add( string $key, - $relation, + array|CollectionDocument|ResourceInterface|null $relation, array $links=[], array $meta=[], ): RelationshipObject { diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index f93ce1f3..8b4f1fc7 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -191,7 +191,7 @@ public function testCheckHttpStatusCode_HappyPath(bool $expectedOutput, int|stri /** * @return array */ - public static function dataProviderCheckHttpStatusCode_HappyPath() { + public static function dataProviderCheckHttpStatusCode_HappyPath(): array { return [ [false, 42], [true, 100], diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php index fa2120f9..04c899a4 100644 --- a/tests/helpers/RequestParserTest.php +++ b/tests/helpers/RequestParserTest.php @@ -185,10 +185,6 @@ public function testFromPsrRequest_WithRequestInterface(): void { } public function testFromPsrRequest_WithEmptyDocument(): void { - $selfLink = ''; - $queryParameters = []; - $document = null; - $request = parent::createConfiguredStub(RequestInterface::class, [ 'getBody' => parent::createConfiguredStub(StreamInterface::class, ['getContents' => '']), 'getUri' => parent::createConfiguredStub(UriInterface::class, ['getQuery' => '']), diff --git a/tests/objects/ErrorObjectTest.php b/tests/objects/ErrorObjectTest.php index 8bb42453..0b8fdc13 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -42,7 +42,6 @@ public function testFromException_HappyPath(): void { public function testFromException_DoNotExposeTrace(): void { $exception = new \Exception('foo', 1); - $expectedLine = (__LINE__ - 1); $options = ['includeExceptionTrace' => false]; $errorObject = ErrorObject::fromException($exception, $options); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index 89539196..c845347c 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -78,7 +78,7 @@ public function testFromResourceObject_HappyPath(): void { public function testFromResourceObject_NoFullIdentification(): void { $resource = new ResourceObject(); - $array = $resource->toArray(); + $resource->toArray(); $this->expectException(InputException::class); $this->expectExceptionMessage('resource has no identification yet'); From 97e7f9aecea4fc2f0ec4c8e53becc8a3d19496b3 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 12:23:51 +0100 Subject: [PATCH 13/20] improve code quality & styling with rector --- examples/atomic_operations_extension.php | 2 +- examples/bootstrap_examples.php | 8 ++++---- examples/collection.php | 2 +- examples/collection_canonical.php | 2 +- examples/cursor_pagination_profile.php | 2 +- examples/errors_all_options.php | 2 +- examples/errors_exception_native.php | 6 +++--- examples/extension.php | 2 +- examples/meta_only.php | 2 +- examples/null_values.php | 2 +- examples/output.php | 2 +- examples/profile.php | 2 +- examples/relationship_to_many_document.php | 2 +- examples/relationship_to_one_document.php | 2 +- examples/relationships.php | 2 +- examples/request_superglobals.php | 2 +- examples/resource_human_api.php | 2 +- examples/resource_links.php | 2 +- examples/resource_nested_relations.php | 2 +- examples/resource_spec_api.php | 2 +- examples/status_only.php | 2 +- rector.php | 22 +++++++++++++++++++++- tests/DocumentTest.php | 2 +- tests/SeparateProcessTest.php | 2 +- tests/example_output/ExampleUser.php | 2 +- tests/helpers/LinksManagerTest.php | 2 +- 26 files changed, 51 insertions(+), 31 deletions(-) diff --git a/examples/atomic_operations_extension.php b/examples/atomic_operations_extension.php index caa6aaae..0ecc92a9 100644 --- a/examples/atomic_operations_extension.php +++ b/examples/atomic_operations_extension.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use alsvanzelf\jsonapi\extensions\AtomicOperationsDocument; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * use the atomic operations extension as extension to the document diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index 9cfec325..cff0a093 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -92,10 +92,10 @@ public static function findRecords(string $type): array { * @return ExampleUser[] */ public static function findEntities(string $type): array { - $records = self::findRecords($type); - $entities = []; + $recordIds = array_keys(self::findRecords($type)); + $entities = []; - foreach ($records as $id => $record) { + foreach ($recordIds as $id) { $entities[$id] = self::getEntity($type, $id); } @@ -112,7 +112,7 @@ public function __construct( public int $id, ) {} - function getCurrentLocation(): string { + public function getCurrentLocation(): string { return 'Earth'; } } diff --git a/examples/collection.php b/examples/collection.php index ab11fc8a..7041a2a2 100644 --- a/examples/collection.php +++ b/examples/collection.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\CollectionDocument; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $users = ExampleDataset::findEntities('user'); diff --git a/examples/collection_canonical.php b/examples/collection_canonical.php index a29cb295..82fba06b 100644 --- a/examples/collection_canonical.php +++ b/examples/collection_canonical.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\CollectionDocument; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $articleRecords = ExampleDataset::findRecords('articles'); $commentRecords = ExampleDataset::findRecords('comments'); diff --git a/examples/cursor_pagination_profile.php b/examples/cursor_pagination_profile.php index 5ad180fb..01b7e5e0 100644 --- a/examples/cursor_pagination_profile.php +++ b/examples/cursor_pagination_profile.php @@ -6,7 +6,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use alsvanzelf\jsonapi\profiles\CursorPaginationProfile; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * use the cursor pagination profile as extension to the document diff --git a/examples/errors_all_options.php b/examples/errors_all_options.php index 0174599f..7c14e85b 100644 --- a/examples/errors_all_options.php +++ b/examples/errors_all_options.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\ErrorsDocument; use alsvanzelf\jsonapi\objects\ErrorObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * setting all options diff --git a/examples/errors_exception_native.php b/examples/errors_exception_native.php index 9f2b4058..5cc78803 100644 --- a/examples/errors_exception_native.php +++ b/examples/errors_exception_native.php @@ -4,7 +4,7 @@ use alsvanzelf\jsonapi\ErrorsDocument; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * via an exception @@ -17,12 +17,12 @@ try { throw new \Exception('unknown user', 404); } -catch (Exception $e) { +catch (Exception $exception) { $options = [ 'includeExceptionTrace' => true, 'includeExceptionPrevious' => true, ]; - $document = ErrorsDocument::fromException($e, $options); + $document = ErrorsDocument::fromException($exception, $options); $options = [ 'prettyPrint' => true, diff --git a/examples/extension.php b/examples/extension.php index 3ecf4ddb..716e13ae 100644 --- a/examples/extension.php +++ b/examples/extension.php @@ -6,7 +6,7 @@ use alsvanzelf\jsonapi\enums\ContentTypeEnum; use alsvanzelf\jsonapi\helpers\Converter; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * use an extension extend the document with new members diff --git a/examples/meta_only.php b/examples/meta_only.php index 60562d7f..83c96a1f 100644 --- a/examples/meta_only.php +++ b/examples/meta_only.php @@ -4,7 +4,7 @@ use alsvanzelf\jsonapi\MetaDocument; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * there are a few use-cases for sending meta-only responses diff --git a/examples/null_values.php b/examples/null_values.php index 4a7cd92c..3c1ac14d 100644 --- a/examples/null_values.php +++ b/examples/null_values.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\objects\LinkObject; use alsvanzelf\jsonapi\objects\RelationshipObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * tell that a value is non-existing diff --git a/examples/output.php b/examples/output.php index b6e35c10..4cd9e133 100644 --- a/examples/output.php +++ b/examples/output.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\MetaDocument; use alsvanzelf\jsonapi\enums\ContentTypeEnum; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $document = new MetaDocument(); $document->add('foo', 'bar'); diff --git a/examples/profile.php b/examples/profile.php index 5bce8471..fe1d64fb 100644 --- a/examples/profile.php +++ b/examples/profile.php @@ -6,7 +6,7 @@ use alsvanzelf\jsonapi\enums\ContentTypeEnum; use alsvanzelf\jsonapi\helpers\Converter; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * use a profile to define rules for members diff --git a/examples/relationship_to_many_document.php b/examples/relationship_to_many_document.php index f2a2af2a..d199bf1d 100644 --- a/examples/relationship_to_many_document.php +++ b/examples/relationship_to_many_document.php @@ -4,7 +4,7 @@ use alsvanzelf\jsonapi\CollectionDocument; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * a to-many relationship response diff --git a/examples/relationship_to_one_document.php b/examples/relationship_to_one_document.php index a715ea64..a6584609 100644 --- a/examples/relationship_to_one_document.php +++ b/examples/relationship_to_one_document.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\ResourceDocument; use alsvanzelf\jsonapi\enums\DocumentLevelEnum; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * a to-one relationship response diff --git a/examples/relationships.php b/examples/relationships.php index 5f0cb5e3..235a174e 100644 --- a/examples/relationships.php +++ b/examples/relationships.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * the different ways of adding relationships to a resource diff --git a/examples/request_superglobals.php b/examples/request_superglobals.php index cb93d06f..403c1fe9 100644 --- a/examples/request_superglobals.php +++ b/examples/request_superglobals.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\enums\ContentTypeEnum; use alsvanzelf\jsonapi\helpers\RequestParser; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * preparing request data in superglobals from a webserver diff --git a/examples/resource_human_api.php b/examples/resource_human_api.php index 64fdc576..6d080737 100644 --- a/examples/resource_human_api.php +++ b/examples/resource_human_api.php @@ -10,7 +10,7 @@ use alsvanzelf\jsonapi\objects\RelationshipsObject; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $user1 = ExampleDataset::getEntity('user', 1); $user42 = ExampleDataset::getEntity('user', 42); diff --git a/examples/resource_links.php b/examples/resource_links.php index 6d9e8860..aa4cef1f 100644 --- a/examples/resource_links.php +++ b/examples/resource_links.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\ResourceDocument; use alsvanzelf\jsonapi\enums\DocumentLevelEnum; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $userEntity = ExampleDataset::getEntity('user', 42); diff --git a/examples/resource_nested_relations.php b/examples/resource_nested_relations.php index b94b07e2..9545d47f 100644 --- a/examples/resource_nested_relations.php +++ b/examples/resource_nested_relations.php @@ -5,7 +5,7 @@ use alsvanzelf\jsonapi\ResourceDocument; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $userEntity = ExampleDataset::getEntity('user', 42); diff --git a/examples/resource_spec_api.php b/examples/resource_spec_api.php index fa160d44..e45006df 100644 --- a/examples/resource_spec_api.php +++ b/examples/resource_spec_api.php @@ -11,7 +11,7 @@ use alsvanzelf\jsonapi\objects\RelationshipsObject; use alsvanzelf\jsonapi\objects\ResourceObject; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; $user1 = ExampleDataset::getEntity('user', 1); $user42 = ExampleDataset::getEntity('user', 42); diff --git a/examples/status_only.php b/examples/status_only.php index 94439c01..bcd35226 100644 --- a/examples/status_only.php +++ b/examples/status_only.php @@ -4,7 +4,7 @@ use alsvanzelf\jsonapi\MetaDocument; -require 'bootstrap_examples.php'; +require __DIR__.'/bootstrap_examples.php'; /** * use jsonapi to send out a status code diff --git a/rector.php b/rector.php index 26ac1608..26ec5af1 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,14 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\FuncCall\SortNamedParamRector; use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; +use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector; +use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector; +use Rector\CodingStyle\Rector\ClassLike\NewlineBetweenClassLikeStmtsRector; +use Rector\CodingStyle\Rector\ClassMethod\NewlineBeforeNewAssignSetRector; +use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector; +use Rector\CodingStyle\Rector\String_\SimplifyQuoteEscapeRector; use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector; use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; @@ -21,9 +28,20 @@ ]) ->withSkip([ // better explicit readability + FlipTypeControlToUseExclusiveTypeRector::class, IfIssetToCoalescingRector::class, - // better explicit readability SimplifyUselessVariableRector::class, + SimplifyIfReturnBoolRector::class, + + // not all rules from code style + NewlineAfterStatementRector::class, + NewlineBeforeNewAssignSetRector::class, + NewlineBetweenClassLikeStmtsRector::class, + SimplifyQuoteEscapeRector::class, + + // better readability using function declaration sorting + SortNamedParamRector::class, + // explicit testing private properties RemoveUnusedPrivatePropertyRector::class => [ 'tests/ConverterTest.php', @@ -39,6 +57,8 @@ // slowly expand keys ->withPreparedSets( deadCode: true, + codeQuality: true, + codingStyle: true, typeDeclarations: true, ) ; diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index f3fdab02..2af0fed1 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -17,7 +17,7 @@ class DocumentTest extends TestCase { private Document $document; - public function setUp(): void { + protected function setUp(): void { /** * extending Document to make it non-abstract to test against it * diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 5b760bbe..4881aaf2 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -16,7 +16,7 @@ class SeparateProcessTest extends TestCase { private Document $document; - public function setUp(): void { + protected function setUp(): void { /** * extending Document to make it non-abstract to test against it * diff --git a/tests/example_output/ExampleUser.php b/tests/example_output/ExampleUser.php index c01cf47d..6e607609 100644 --- a/tests/example_output/ExampleUser.php +++ b/tests/example_output/ExampleUser.php @@ -13,7 +13,7 @@ public function __construct( public int $id, ) {} - function getCurrentLocation(): string { + public function getCurrentLocation(): string { return 'Earth'; } } diff --git a/tests/helpers/LinksManagerTest.php b/tests/helpers/LinksManagerTest.php index 9376220e..6ea97631 100644 --- a/tests/helpers/LinksManagerTest.php +++ b/tests/helpers/LinksManagerTest.php @@ -11,7 +11,7 @@ class LinksManagerTest extends TestCase { private object $linksManager; - public function setUp(): void { + protected function setUp(): void { // using LinksManager to make it non-trait to test against it $this->linksManager = new class { use LinksManager; From fb295f1bbcce7677f46f8cc9fd189c1bb9918835 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 12:46:26 +0100 Subject: [PATCH 14/20] expand rector prepared sets --- rector.php | 21 ++++++- tests/CollectionDocumentTest.php | 24 +++----- tests/ConverterTest.php | 21 +++---- tests/DocumentTest.php | 2 +- tests/ErrorsDocumentTest.php | 28 +++++----- tests/ExampleOutputTest.php | 2 +- tests/MetaDocumentTest.php | 2 +- tests/ResourceDocumentTest.php | 2 +- tests/SeparateProcessTest.php | 2 +- tests/ValidatorTest.php | 56 +++++++++---------- .../AtomicOperationsDocumentTest.php | 2 +- tests/helpers/AtMemberManagerTest.php | 2 +- tests/helpers/ExtensionMemberManagerTest.php | 2 +- tests/helpers/HttpStatusCodeManagerTest.php | 2 +- tests/helpers/LinksManagerTest.php | 2 +- tests/helpers/RequestParserTest.php | 2 +- tests/objects/AttributesObjectTest.php | 2 +- tests/objects/ErrorObjectTest.php | 2 +- tests/objects/JsonapiObjectTest.php | 2 +- tests/objects/LinkObjectTest.php | 2 +- tests/objects/LinksObjectTest.php | 2 +- tests/objects/MetaObjectTest.php | 2 +- tests/objects/RelationshipObjectTest.php | 2 +- tests/objects/RelationshipsObjectTest.php | 2 +- .../objects/ResourceIdentifierObjectTest.php | 2 +- tests/objects/ResourceObjectTest.php | 2 +- .../profiles/CursorPaginationProfileTest.php | 2 +- 27 files changed, 96 insertions(+), 98 deletions(-) diff --git a/rector.php b/rector.php index 26ec5af1..fd40676d 100644 --- a/rector.php +++ b/rector.php @@ -13,6 +13,7 @@ use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector; use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; +use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockGetterReturnArrayFromPropertyDocblockVarRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; // @see https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md for more rules @@ -39,6 +40,9 @@ NewlineBetweenClassLikeStmtsRector::class, SimplifyQuoteEscapeRector::class, + // rely on types from interfaces + DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, + // better readability using function declaration sorting SortNamedParamRector::class, @@ -54,11 +58,26 @@ // lowest supported php version ->withPhpSets(php82: true) - // slowly expand keys + // expand with new keys ->withPreparedSets( deadCode: true, codeQuality: true, codingStyle: true, typeDeclarations: true, + typeDeclarationDocblocks: true, + privatization: true, + phpunitCodeQuality: true, + + // prefer own style + // naming: true, + // instanceOf: true, + // earlyReturn: true, + // rectorPreset: true, + + // not used + // carbon: true, + // doctrineCodeQuality: true, + // symfonyCodeQuality: true, + // symfonyConfigs: true, ) ; diff --git a/tests/CollectionDocumentTest.php b/tests/CollectionDocumentTest.php index fe0ce50f..fddf3009 100644 --- a/tests/CollectionDocumentTest.php +++ b/tests/CollectionDocumentTest.php @@ -10,7 +10,7 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\objects\ResourceObject; -class CollectionDocumentTest extends TestCase { +final class CollectionDocumentTest extends TestCase { public function testConstructor_NoResources(): void { $document = new CollectionDocument(); @@ -120,22 +120,14 @@ public function testSetPaginationLinks_IndividualLinks(?string $key, ?string $pr } /** - * @return array + * @return \Iterator<(int | string), array{(string | null), (string | null), (string | null), (string | null), (string | null)}> */ - public static function dataProviderSetPaginationLinks_IndividualLinks(): array { - return [ - ['prev', 'https://jsonapi.org', null, null, null], - ['next', null, 'https://jsonapi.org', null, null], - ['first', null, null, 'https://jsonapi.org', null], - ['last', null, null, null, 'https://jsonapi.org'], - [null, null, null, null, null], - ]; + public static function dataProviderSetPaginationLinks_IndividualLinks(): \Iterator { + yield ['prev', 'https://jsonapi.org', null, null, null]; + yield ['next', null, 'https://jsonapi.org', null, null]; + yield ['first', null, null, 'https://jsonapi.org', null]; + yield ['last', null, null, null, 'https://jsonapi.org']; + yield [null, null, null, null, null]; } public function testAddResource_HappyPath(): void { diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index 7bf54758..c0830fb8 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -12,7 +12,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -class ConverterTest extends TestCase { +final class ConverterTest extends TestCase { public function testObjectToArray_HappyPath(): void { $object = new \stdClass(); $object->foo = 'bar'; @@ -63,19 +63,14 @@ public function testCamelCaseToWords_HappyPath(string $camelCase, string $expect } /** - * @return array + * @return \Iterator<(int | string), array{string, string}> */ - public static function dataProviderCamelCaseToWords_HappyPath(): array { - return [ - ['value', 'value'], - ['camelValue', 'camel Value'], - ['TitleValue', 'Title Value'], - ['VALUE', 'VALUE'], - ['eclipseRCPExt', 'eclipse RCP Ext'], - ]; + public static function dataProviderCamelCaseToWords_HappyPath(): \Iterator { + yield ['value', 'value']; + yield ['camelValue', 'camel Value']; + yield ['TitleValue', 'Title Value']; + yield ['VALUE', 'VALUE']; + yield ['eclipseRCPExt', 'eclipse RCP Ext']; } /** diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 2af0fed1..feb03d34 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -14,7 +14,7 @@ use alsvanzelf\jsonapi\objects\LinkObject; use PHPUnit\Framework\TestCase; -class DocumentTest extends TestCase { +final class DocumentTest extends TestCase { private Document $document; protected function setUp(): void { diff --git a/tests/ErrorsDocumentTest.php b/tests/ErrorsDocumentTest.php index 88b9b830..e19a0eeb 100644 --- a/tests/ErrorsDocumentTest.php +++ b/tests/ErrorsDocumentTest.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapi\ErrorsDocument; use alsvanzelf\jsonapi\objects\ErrorObject; -class ErrorsDocumentTest extends TestCase { +final class ErrorsDocumentTest extends TestCase { public function testFromException_HappyPath(): void { $document = ErrorsDocument::fromException(new \Exception('foo', 42)); @@ -121,21 +121,19 @@ public function testDetermineHttpStatusCode_HappyPath(int $expectedAdvisedErrorC } /** - * @return array}> + * @return \Iterator<(int | string), array{int, non-empty-array}> */ - public static function dataProviderDetermineHttpStatusCode_HappyPath(): array { - return [ - [422, [422]], - [422, [422, 422]], - [400, [422, 404]], - [400, [400]], - [501, [501]], - [501, [501, 501]], - [500, [501, 503]], - [500, [422, 404, 501, 503]], - [500, [500]], - [302, [302]], - ]; + public static function dataProviderDetermineHttpStatusCode_HappyPath(): \Iterator { + yield [422, [422]]; + yield [422, [422, 422]]; + yield [400, [422, 404]]; + yield [400, [400]]; + yield [501, [501]]; + yield [501, [501, 501]]; + yield [500, [501, 503]]; + yield [500, [422, 404, 501, 503]]; + yield [500, [500]]; + yield [302, [302]]; } public function testDetermineHttpStatusCode_Override(): void { diff --git a/tests/ExampleOutputTest.php b/tests/ExampleOutputTest.php index 2d393e9e..299b2049 100644 --- a/tests/ExampleOutputTest.php +++ b/tests/ExampleOutputTest.php @@ -11,7 +11,7 @@ /** * @group OutputOnly */ -class ExampleOutputTest extends TestCase { +final class ExampleOutputTest extends TestCase { /** @var PHPStanTypeAlias_Options_Document */ private static array $defaults = [ 'prettyPrint' => true, diff --git a/tests/MetaDocumentTest.php b/tests/MetaDocumentTest.php index 1db500d8..7cfe65a2 100644 --- a/tests/MetaDocumentTest.php +++ b/tests/MetaDocumentTest.php @@ -7,7 +7,7 @@ use alsvanzelf\jsonapi\MetaDocument; use PHPUnit\Framework\TestCase; -class MetaDocumentTest extends TestCase { +final class MetaDocumentTest extends TestCase { public function testConstructor_NoMeta(): void { $document = new MetaDocument(); diff --git a/tests/ResourceDocumentTest.php b/tests/ResourceDocumentTest.php index b72882e2..85dc4ed1 100644 --- a/tests/ResourceDocumentTest.php +++ b/tests/ResourceDocumentTest.php @@ -17,7 +17,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; -class ResourceDocumentTest extends TestCase { +final class ResourceDocumentTest extends TestCase { public function testConstructor_NoResource(): void { $document = new ResourceDocument(); diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 4881aaf2..92718fd1 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -13,7 +13,7 @@ /** * @group SeparateProcess */ -class SeparateProcessTest extends TestCase { +final class SeparateProcessTest extends TestCase { private Document $document; protected function setUp(): void { diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 8b4f1fc7..fdab54c3 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; -class ValidatorTest extends TestCase { +final class ValidatorTest extends TestCase { #[DoesNotPerformAssertions] public function testClaimUsedFields_HappyPath(): void { $validator = new Validator(); @@ -152,16 +152,14 @@ public function testCheckMemberName_HappyPath(string $memberName): void { } /** - * @return array + * @return \Iterator<(int | string), array{string}> */ - public static function dataProviderCheckMemberName_HappyPath(): array { - return [ - ['foo'], - ['f_o'], - ['f-o'], - ['42foo'], - ['42'], - ]; + public static function dataProviderCheckMemberName_HappyPath(): \Iterator { + yield ['foo']; + yield ['f_o']; + yield ['f-o']; + yield ['42foo']; + yield ['42']; } #[DataProvider('dataProviderCheckMemberName_InvalidNames')] @@ -172,15 +170,13 @@ public function testCheckMemberName_InvalidNames(string $memberName): void { } /** - * @return array + * @return \Iterator<(int | string), array{string}> */ - public static function dataProviderCheckMemberName_InvalidNames(): array { - return [ - ['_'], - ['-'], - ['foo-'], - ['-foo'], - ]; + public static function dataProviderCheckMemberName_InvalidNames(): \Iterator { + yield ['_']; + yield ['-']; + yield ['foo-']; + yield ['-foo']; } #[DataProvider('dataProviderCheckHttpStatusCode_HappyPath')] @@ -189,19 +185,17 @@ public function testCheckHttpStatusCode_HappyPath(bool $expectedOutput, int|stri } /** - * @return array + * @return \Iterator<(int | string), array{bool, (int | string)}> */ - public static function dataProviderCheckHttpStatusCode_HappyPath(): array { - return [ - [false, 42], - [true, 100], - [true, 200], - [true, 300], - [true, 400], - [true, 500], - [false, 600], - [false, '42'], - [true, '100'], - ]; + public static function dataProviderCheckHttpStatusCode_HappyPath(): \Iterator { + yield [false, 42]; + yield [true, 100]; + yield [true, 200]; + yield [true, 300]; + yield [true, 400]; + yield [true, 500]; + yield [false, 600]; + yield [false, '42']; + yield [true, '100']; } } diff --git a/tests/extensions/AtomicOperationsDocumentTest.php b/tests/extensions/AtomicOperationsDocumentTest.php index 56b9bb9c..846c4d07 100644 --- a/tests/extensions/AtomicOperationsDocumentTest.php +++ b/tests/extensions/AtomicOperationsDocumentTest.php @@ -12,7 +12,7 @@ /** * @group Extensions */ -class AtomicOperationsDocumentTest extends TestCase { +final class AtomicOperationsDocumentTest extends TestCase { public function testSetResults_HappyPath(): void { $document = new AtomicOperationsDocument(); diff --git a/tests/helpers/AtMemberManagerTest.php b/tests/helpers/AtMemberManagerTest.php index 40174cc9..49656536 100644 --- a/tests/helpers/AtMemberManagerTest.php +++ b/tests/helpers/AtMemberManagerTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\helpers\AtMemberManager; use PHPUnit\Framework\TestCase; -class AtMemberManagerTest extends TestCase { +final class AtMemberManagerTest extends TestCase { private static object $helper; public static function setUpBeforeClass(): void { diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php index deee7e32..70d65b21 100644 --- a/tests/helpers/ExtensionMemberManagerTest.php +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -12,7 +12,7 @@ /** * @group Extensions */ -class ExtensionMemberManagerTest extends TestCase { +final class ExtensionMemberManagerTest extends TestCase { private static object $helper; public static function setUpBeforeClass(): void { diff --git a/tests/helpers/HttpStatusCodeManagerTest.php b/tests/helpers/HttpStatusCodeManagerTest.php index 9fe2b96f..c30abc1f 100644 --- a/tests/helpers/HttpStatusCodeManagerTest.php +++ b/tests/helpers/HttpStatusCodeManagerTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager; use PHPUnit\Framework\TestCase; -class HttpStatusCodeManagerTest extends TestCase { +final class HttpStatusCodeManagerTest extends TestCase { private static object $helper; public static function setUpBeforeClass(): void { diff --git a/tests/helpers/LinksManagerTest.php b/tests/helpers/LinksManagerTest.php index 6ea97631..836099fc 100644 --- a/tests/helpers/LinksManagerTest.php +++ b/tests/helpers/LinksManagerTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\LinkObject; use PHPUnit\Framework\TestCase; -class LinksManagerTest extends TestCase { +final class LinksManagerTest extends TestCase { private object $linksManager; protected function setUp(): void { diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php index 04c899a4..9bfd5ab2 100644 --- a/tests/helpers/RequestParserTest.php +++ b/tests/helpers/RequestParserTest.php @@ -13,7 +13,7 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -class RequestParserTest extends TestCase { +final class RequestParserTest extends TestCase { public function testFromSuperglobals_HappyPath(): void { $_GET = [ 'include' => 'ship,ship.wing', diff --git a/tests/objects/AttributesObjectTest.php b/tests/objects/AttributesObjectTest.php index ca604990..2d99bea6 100644 --- a/tests/objects/AttributesObjectTest.php +++ b/tests/objects/AttributesObjectTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\AttributesObject; use PHPUnit\Framework\TestCase; -class AttributesObjectTest extends TestCase { +final class AttributesObjectTest extends TestCase { public function testFromObject_HappyPath(): void { $object = new \stdClass(); $object->foo = 'bar'; diff --git a/tests/objects/ErrorObjectTest.php b/tests/objects/ErrorObjectTest.php index 0b8fdc13..7d9106b3 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapi\objects\ErrorObject; use PHPUnit\Framework\TestCase; -class ErrorObjectTest extends TestCase { +final class ErrorObjectTest extends TestCase { public function testFromException_HappyPath(): void { $exception = new \Exception('foo', 1); $expectedLine = (__LINE__ - 1); diff --git a/tests/objects/JsonapiObjectTest.php b/tests/objects/JsonapiObjectTest.php index 29965b54..4465c7db 100644 --- a/tests/objects/JsonapiObjectTest.php +++ b/tests/objects/JsonapiObjectTest.php @@ -9,7 +9,7 @@ use alsvanzelf\jsonapi\objects\JsonapiObject; use PHPUnit\Framework\TestCase; -class JsonapiObjectTest extends TestCase { +final class JsonapiObjectTest extends TestCase { public function testAddMeta_HappyPath(): void { $jsonapiObject = new JsonapiObject($version=null); diff --git a/tests/objects/LinkObjectTest.php b/tests/objects/LinkObjectTest.php index fe6da28a..7c8b616b 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\LinkObject; use PHPUnit\Framework\TestCase; -class LinkObjectTest extends TestCase { +final class LinkObjectTest extends TestCase { public function testSetDescribedBy_HappyPath(): void { $linkObject = new LinkObject(); diff --git a/tests/objects/LinksObjectTest.php b/tests/objects/LinksObjectTest.php index 846766ed..ff38232e 100644 --- a/tests/objects/LinksObjectTest.php +++ b/tests/objects/LinksObjectTest.php @@ -10,7 +10,7 @@ use alsvanzelf\jsonapi\objects\LinksObject; use PHPUnit\Framework\TestCase; -class LinksObjectTest extends TestCase { +final class LinksObjectTest extends TestCase { public function testFromObject_HappyPath(): void { $object = new \stdClass(); $object->foo = 'https://jsonapi.org'; diff --git a/tests/objects/MetaObjectTest.php b/tests/objects/MetaObjectTest.php index baf46664..bf1552e3 100644 --- a/tests/objects/MetaObjectTest.php +++ b/tests/objects/MetaObjectTest.php @@ -8,7 +8,7 @@ use alsvanzelf\jsonapi\objects\MetaObject; use PHPUnit\Framework\TestCase; -class MetaObjectTest extends TestCase { +final class MetaObjectTest extends TestCase { public function testAdd_AllowsMixedValue(): void { $metaObject = new MetaObject(); $metaObject->add('array-list', ['foo']); diff --git a/tests/objects/RelationshipObjectTest.php b/tests/objects/RelationshipObjectTest.php index d784bf9f..6ab77cda 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -15,7 +15,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; -class RelationshipObjectTest extends TestCase { +final class RelationshipObjectTest extends TestCase { public function testConstructor_ToOne(): void { $relationshipObject = new RelationshipObject(RelationshipTypeEnum::ToOne); $relationshipObject->setResource(new ResourceObject('user', 42)); diff --git a/tests/objects/RelationshipsObjectTest.php b/tests/objects/RelationshipsObjectTest.php index 6b7f0af3..a345b1a1 100644 --- a/tests/objects/RelationshipsObjectTest.php +++ b/tests/objects/RelationshipsObjectTest.php @@ -12,7 +12,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; -class RelationshipsObjectTest extends TestCase { +final class RelationshipsObjectTest extends TestCase { public function testAdd_HappyPath(): void { $relationshipsObject = new RelationshipsObject(); $relationshipsObject->add('foo', new ResourceObject('user', 42)); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index c845347c..d04cb257 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -12,7 +12,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; -class ResourceIdentifierObjectTest extends TestCase { +final class ResourceIdentifierObjectTest extends TestCase { public function testSetId_HappyPath(): void { $resourceIdentifierObject = new ResourceIdentifierObject(); $resourceIdentifierObject->setType('test'); diff --git a/tests/objects/ResourceObjectTest.php b/tests/objects/ResourceObjectTest.php index 0b1410f9..1699fd2c 100644 --- a/tests/objects/ResourceObjectTest.php +++ b/tests/objects/ResourceObjectTest.php @@ -14,7 +14,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; use PHPUnit\Framework\TestCase; -class ResourceObjectTest extends TestCase { +final class ResourceObjectTest extends TestCase { public function testConstructor_ClientDocumentWithoutId(): void { $resourceObject = new ResourceObject('user'); $resourceObject->add('foo', 'bar'); diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 9d67e331..54216527 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -14,7 +14,7 @@ /** * @group Profiles */ -class CursorPaginationProfileTest extends TestCase { +final class CursorPaginationProfileTest extends TestCase { public function testSetLinks_HappyPath(): void { $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); From 4305150d2fc56f664eb79a7664b9785a8a67cfd8 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 12:53:19 +0100 Subject: [PATCH 15/20] fix examples --- examples/bootstrap_examples.php | 6 ++-- examples/request_superglobals.php | 60 +++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index cff0a093..6cd955f1 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -104,9 +104,9 @@ public static function findEntities(string $type): array { } class ExampleUser { - public string $name; - public int|string $heads; - public mixed $unknown; + public ?string $name = null; + public null|int|string $heads = null; + public mixed $unknown = null; public function __construct( public int $id, diff --git a/examples/request_superglobals.php b/examples/request_superglobals.php index 403c1fe9..e4a0be9a 100644 --- a/examples/request_superglobals.php +++ b/examples/request_superglobals.php @@ -60,31 +60,55 @@ */ // useful for filling a self link in responses -var_dump($requestParser->getSelfLink()); +echo '

Get self link

'; +echo '
$requestParser->getSelfLink()
'; +echo '
'.var_export($requestParser->getSelfLink(), return: true).'
'; // useful for determining how to process the request (list/get/create/update) -var_dump($requestParser->hasIncludePaths()); -var_dump($requestParser->hasSparseFieldset('user')); -var_dump($requestParser->hasSortFields()); -var_dump($requestParser->hasPagination()); -var_dump($requestParser->hasFilter()); +echo '

Check query parameters

'; +echo '
$requestParser->hasIncludePaths()
'; +echo '
'.var_export($requestParser->hasIncludePaths(), return: true).'
'; +echo '
$requestParser->hasSparseFieldset()
'; +echo '
'.var_export($requestParser->hasSparseFieldset('user'), return: true).'
'; +echo '
$requestParser->hasSortFields()
'; +echo '
'.var_export($requestParser->hasSortFields(), return: true).'
'; +echo '
$requestParser->hasPagination()
'; +echo '
'.var_export($requestParser->hasPagination(), return: true).'
'; +echo '
$requestParser->hasFilter()
'; +echo '
'.var_export($requestParser->hasFilter(), return: true).'
'; // these methods often return arrays where comma separated query parameter values are processed for ease of use -var_dump($requestParser->getIncludePaths()); -var_dump($requestParser->getSparseFieldset('user')); -var_dump($requestParser->getSortFields()); -var_dump($requestParser->getPagination()); -var_dump($requestParser->getFilter()); +echo '

Get query parameters

'; +echo '
$requestParser->getIncludePaths()
'; +echo '
'.var_export($requestParser->getIncludePaths(), return: true).'
'; +echo '
$requestParser->getSparseFieldset()
'; +echo '
'.var_export($requestParser->getSparseFieldset('user'), return: true).'
'; +echo '
$requestParser->getSortFields()
'; +echo '
'.var_export($requestParser->getSortFields(), return: true).'
'; +echo '
$requestParser->getPagination()
'; +echo '
'.var_export($requestParser->getPagination(), return: true).'
'; +echo '
$requestParser->getFilter()
'; +echo '
'.var_export($requestParser->getFilter(), return: true).'
'; // use for determinging whether keys were given without having to dive deep into the POST data yourself -var_dump($requestParser->hasAttribute('name')); -var_dump($requestParser->hasRelationship('ship')); -var_dump($requestParser->hasMeta('lock')); +echo '

Check parts of the document

'; +echo '
$requestParser->hasAttribute()
'; +echo '
'.var_export($requestParser->hasAttribute('name'), return: true).'
'; +echo '
$requestParser->hasRelationship()
'; +echo '
'.var_export($requestParser->hasRelationship('ship'), return: true).'
'; +echo '
$requestParser->hasMeta()
'; +echo '
'.var_export($requestParser->hasMeta('lock'), return: true).'
'; // get the raw data from the document, this doesn't (yet) return specific objects -var_dump($requestParser->getAttribute('name')); -var_dump($requestParser->getRelationship('ship')); -var_dump($requestParser->getMeta('lock')); +echo '

Get parts of the document

'; +echo '
$requestParser->getAttribute()
'; +echo '
'.var_export($requestParser->getAttribute('name'), return: true).'
'; +echo '
$requestParser->getRelationship()
'; +echo '
'.var_export($requestParser->getRelationship('ship'), return: true).'
'; +echo '
$requestParser->getMeta()
'; +echo '
'.var_export($requestParser->getMeta('lock'), return: true).'
'; // get the full document for custom processing -var_dump($requestParser->getDocument()); +echo '

Get full document

'; +echo '
$requestParser->getDocument()
'; +echo '
'.var_export($requestParser->getDocument(), return: true).'
'; From 34ae82a65801570109a5f20d14c3ff54937381e9 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 13:22:40 +0100 Subject: [PATCH 16/20] increase rector with vendor changes --- rector.php | 7 ++++ tests/ConverterTest.php | 21 ++++------ tests/DocumentTest.php | 17 +++------ tests/ExampleOutputTest.php | 7 ++-- tests/ResourceDocumentTest.php | 5 +-- tests/SeparateProcessTest.php | 38 ++++++------------- .../AtomicOperationsDocumentTest.php | 5 +-- tests/helpers/ExtensionMemberManagerTest.php | 5 +-- tests/objects/AttributesObjectTest.php | 5 +-- tests/objects/ErrorObjectTest.php | 5 +-- tests/objects/JsonapiObjectTest.php | 13 ++----- tests/objects/LinkObjectTest.php | 5 +-- tests/objects/MetaObjectTest.php | 5 +-- tests/objects/RelationshipObjectTest.php | 5 +-- .../objects/ResourceIdentifierObjectTest.php | 5 +-- .../profiles/CursorPaginationProfileTest.php | 5 +-- 16 files changed, 58 insertions(+), 95 deletions(-) diff --git a/rector.php b/rector.php index fd40676d..4d2b6b90 100644 --- a/rector.php +++ b/rector.php @@ -24,6 +24,7 @@ __DIR__ . '/tests', __DIR__ . '/examples', ]) + ->withRootFiles() ->withRules([ DeclareStrictTypesRector::class, ]) @@ -54,6 +55,8 @@ // tab-based indenting ->withIndent(indentChar: "\t", indentSize: 1) + // importing FQNs + ->withImportNames(importShortClasses: false) // lowest supported php version ->withPhpSets(php82: true) @@ -80,4 +83,8 @@ // symfonyCodeQuality: true, // symfonyConfigs: true, ) + + // vendor sets + ->withAttributesSets() + ->withComposerBased(phpunit: true) ; diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index c0830fb8..8eeb973f 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -10,6 +10,7 @@ use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\AttributesObject; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class ConverterTest extends TestCase { @@ -73,36 +74,28 @@ public static function dataProviderCamelCaseToWords_HappyPath(): \Iterator { yield ['eclipseRCPExt', 'eclipse RCP Ext']; } - /** - * @group Extensions - * @group Profiles - */ + #[Group('Extensions')] + #[Group('Profiles')] public function testPrepareContentType_HappyPath(): void { parent::assertSame(ContentTypeEnum::Official->value, Converter::prepareContentType(ContentTypeEnum::Official, [], [])); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testPrepareContentType_WithExtensionStringLink(): void { $extension = parent::createConfiguredStub(ExtensionInterface::class, ['getOfficialLink' => 'bar']); parent::assertSame(ContentTypeEnum::Official->value.'; ext="bar"', Converter::prepareContentType(ContentTypeEnum::Official, [$extension], [])); } - /** - * @group Profiles - */ + #[Group('Profiles')] public function testPrepareContentType_WithProfileStringLink(): void { $profile = parent::createConfiguredStub(ProfileInterface::class, ['getOfficialLink' => 'bar']); parent::assertSame(ContentTypeEnum::Official->value.'; profile="bar"', Converter::prepareContentType(ContentTypeEnum::Official, [], [$profile])); } - /** - * @group Extensions - * @group Profiles - */ + #[Group('Extensions')] + #[Group('Profiles')] public function testPrepareContentType_WithMultipleExtensionsAndProfiles(): void { $extension1 = parent::createConfiguredStub(ExtensionInterface::class, ['getOfficialLink' => 'bar']); $extension2 = parent::createConfiguredStub(ExtensionInterface::class, ['getOfficialLink' => 'baz']); diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index feb03d34..3c8d3131 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -12,6 +12,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\LinkObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class DocumentTest extends TestCase { @@ -180,9 +181,7 @@ public function testAddLinkObject_HappyPath(): void { parent::assertSame('https://jsonapi.org', $array['links']['foo']['href']); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testApplyExtension_HappyPath(): void { $extension = parent::createConfiguredStub(ExtensionInterface::class, [ 'getNamespace' => 'test', @@ -215,9 +214,7 @@ public function testApplyExtension_HappyPath(): void { parent::assertSame('application/vnd.api+json; ext="https://jsonapi.org/extension"', $array['links']['self']['type']); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testApplyExtension_InvalidNamespace(): void { $extension = parent::createConfiguredStub(ExtensionInterface::class, ['getNamespace' => 'foo-bar']); @@ -227,9 +224,7 @@ public function testApplyExtension_InvalidNamespace(): void { $this->document->applyExtension($extension); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testApplyExtension_ConflictingNamespace(): void { $extension1 = parent::createConfiguredStub(ExtensionInterface::class, ['getNamespace' => 'foo']); $this->document->applyExtension($extension1); @@ -245,9 +240,7 @@ public function testApplyExtension_ConflictingNamespace(): void { $this->document->applyExtension($extension3); } - /** - * @group Profiles - */ + #[Group('Profiles')] public function testApplyProfile_HappyPath(): void { $profile = parent::createConfiguredStub(ProfileInterface::class, ['getOfficialLink' => 'https://jsonapi.org/profile']); diff --git a/tests/ExampleOutputTest.php b/tests/ExampleOutputTest.php index 299b2049..260efe26 100644 --- a/tests/ExampleOutputTest.php +++ b/tests/ExampleOutputTest.php @@ -4,13 +4,12 @@ namespace alsvanzelf\jsonapiTests; +use alsvanzelf\jsonapi\interfaces\DocumentInterface; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -use alsvanzelf\jsonapi\interfaces\DocumentInterface; -/** - * @group OutputOnly - */ +#[Group('OutputOnly')] final class ExampleOutputTest extends TestCase { /** @var PHPStanTypeAlias_Options_Document */ private static array $defaults = [ diff --git a/tests/ResourceDocumentTest.php b/tests/ResourceDocumentTest.php index 85dc4ed1..d903272e 100644 --- a/tests/ResourceDocumentTest.php +++ b/tests/ResourceDocumentTest.php @@ -15,6 +15,7 @@ use alsvanzelf\jsonapi\objects\RelationshipsObject; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class ResourceDocumentTest extends TestCase { @@ -62,9 +63,7 @@ public function testAdd_IdentifierOnlyObject(): void { $document->add('foo', 'bar'); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testAdd_BlocksExtensionMembersViaRegularAdd(): void { $document = new ResourceDocument(); $document->applyExtension(parent::createConfiguredStub(ExtensionInterface::class, ['getNamespace' => 'test'])); diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 92718fd1..b4a9a758 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -8,11 +8,11 @@ use alsvanzelf\jsonapi\enums\ContentTypeEnum; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; -/** - * @group SeparateProcess - */ +#[Group('SeparateProcess')] final class SeparateProcessTest extends TestCase { private Document $document; @@ -26,9 +26,7 @@ protected function setUp(): void { $this->document = new class extends Document {}; } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testSendResponse_HappyPath(): void { ob_start(); $this->document->sendResponse(); @@ -37,9 +35,7 @@ public function testSendResponse_HappyPath(): void { parent::assertSame('{"jsonapi":{"version":"1.1"}}', $output); } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testSendResponse_NoContent(): void { $this->document->setHttpStatusCode(204); @@ -51,9 +47,7 @@ public function testSendResponse_NoContent(): void { parent::assertSame(204, http_response_code()); } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testSendResponse_ContentTypeHeader(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -83,10 +77,8 @@ public function testSendResponse_ContentTypeHeader(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Jsonp->value], xdebug_get_headers()); } - /** - * @runInSeparateProcess - * @group Extensions - */ + #[RunInSeparateProcess] + #[Group('Extensions')] public function testSendResponse_ContentTypeHeaderWithExtensions(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -115,10 +107,8 @@ public function testSendResponse_ContentTypeHeaderWithExtensions(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Official->value.'; ext="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); } - /** - * @runInSeparateProcess - * @group Profiles - */ + #[RunInSeparateProcess] + #[Group('Profiles')] public function testSendResponse_ContentTypeHeaderWithProfiles(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -141,9 +131,7 @@ public function testSendResponse_ContentTypeHeaderWithProfiles(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Official->value.'; profile="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testSendResponse_StatusCodeHeader(): void { ob_start(); @@ -170,9 +158,7 @@ public function testSendResponse_StatusCodeHeader(): void { parent::assertSame(503, http_response_code()); } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testSendResponse_CustomJson(): void { $options = ['json' => '{"foo":42}']; diff --git a/tests/extensions/AtomicOperationsDocumentTest.php b/tests/extensions/AtomicOperationsDocumentTest.php index 846c4d07..af9bee67 100644 --- a/tests/extensions/AtomicOperationsDocumentTest.php +++ b/tests/extensions/AtomicOperationsDocumentTest.php @@ -7,11 +7,10 @@ use alsvanzelf\jsonapi\extensions\AtomicOperationsDocument; use alsvanzelf\jsonapi\extensions\AtomicOperationsExtension; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group Extensions - */ +#[Group('Extensions')] final class AtomicOperationsDocumentTest extends TestCase { public function testSetResults_HappyPath(): void { $document = new AtomicOperationsDocument(); diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php index 70d65b21..e9bd1ae7 100644 --- a/tests/helpers/ExtensionMemberManagerTest.php +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -7,11 +7,10 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group Extensions - */ +#[Group('Extensions')] final class ExtensionMemberManagerTest extends TestCase { private static object $helper; diff --git a/tests/objects/AttributesObjectTest.php b/tests/objects/AttributesObjectTest.php index 2d99bea6..061c174b 100644 --- a/tests/objects/AttributesObjectTest.php +++ b/tests/objects/AttributesObjectTest.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\AttributesObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class AttributesObjectTest extends TestCase { @@ -67,9 +68,7 @@ public function testAdd_WithObject(): void { parent::assertSame('baz', $array['foo']['bar']); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testAddExtensionMember_HappyPath(): void { $attributesObject = new AttributesObject(); $extension = parent::createConfiguredStub(ExtensionInterface::class, ['getNamespace' => 'test']); diff --git a/tests/objects/ErrorObjectTest.php b/tests/objects/ErrorObjectTest.php index 7d9106b3..ba374c79 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -7,6 +7,7 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\ErrorObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class ErrorObjectTest extends TestCase { @@ -154,9 +155,7 @@ public function testIsEmpty_All(): void { parent::assertFalse($errorObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testToArray_WithExtensionMembers(): void { $errorObject = new ErrorObject(); $extension = parent::createConfiguredStub(ExtensionInterface::class, ['getNamespace' => 'test']); diff --git a/tests/objects/JsonapiObjectTest.php b/tests/objects/JsonapiObjectTest.php index 4465c7db..2ba8a776 100644 --- a/tests/objects/JsonapiObjectTest.php +++ b/tests/objects/JsonapiObjectTest.php @@ -7,6 +7,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\JsonapiObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class JsonapiObjectTest extends TestCase { @@ -36,9 +37,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($jsonapiObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionLink(): void { $jsonapiObject = new JsonapiObject($version=null); @@ -49,9 +48,7 @@ public function testIsEmpty_WithExtensionLink(): void { parent::assertFalse($jsonapiObject->isEmpty()); } - /** - * @group Profiles - */ + #[Group('Profiles')] public function testIsEmpty_WithProfileLink(): void { $jsonapiObject = new JsonapiObject($version=null); @@ -62,9 +59,7 @@ public function testIsEmpty_WithProfileLink(): void { parent::assertFalse($jsonapiObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $jsonapiObject = new JsonapiObject($version=null); diff --git a/tests/objects/LinkObjectTest.php b/tests/objects/LinkObjectTest.php index 7c8b616b..78b5eab2 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\LinkObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class LinkObjectTest extends TestCase { @@ -155,9 +156,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($linkObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $linkObject = new LinkObject(); diff --git a/tests/objects/MetaObjectTest.php b/tests/objects/MetaObjectTest.php index bf1552e3..050254f1 100644 --- a/tests/objects/MetaObjectTest.php +++ b/tests/objects/MetaObjectTest.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\MetaObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class MetaObjectTest extends TestCase { @@ -49,9 +50,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($metaObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $metaObject = new MetaObject(); diff --git a/tests/objects/RelationshipObjectTest.php b/tests/objects/RelationshipObjectTest.php index 6ab77cda..174e83f8 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -13,6 +13,7 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class RelationshipObjectTest extends TestCase { @@ -298,9 +299,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($relationshipObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $relationshipObject = new RelationshipObject(RelationshipTypeEnum::ToOne); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index d04cb257..e1035347 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -10,6 +10,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; final class ResourceIdentifierObjectTest extends TestCase { @@ -216,9 +217,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($resourceIdentifierObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $resourceIdentifierObject = new ResourceIdentifierObject(); diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 54216527..91ecaa36 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -9,11 +9,10 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceObject; use alsvanzelf\jsonapi\profiles\CursorPaginationProfile; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group Profiles - */ +#[Group('Profiles')] final class CursorPaginationProfileTest extends TestCase { public function testSetLinks_HappyPath(): void { $profile = new CursorPaginationProfile(); From 8146fd705f21eef15bc799dd82b962de68061d2f Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 13:22:56 +0100 Subject: [PATCH 17/20] skip slow separate processes not actually needed anyway --- tests/SeparateProcessTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index b4a9a758..98820827 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -9,7 +9,6 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; #[Group('SeparateProcess')] @@ -26,7 +25,6 @@ protected function setUp(): void { $this->document = new class extends Document {}; } - #[RunInSeparateProcess] public function testSendResponse_HappyPath(): void { ob_start(); $this->document->sendResponse(); @@ -35,7 +33,6 @@ public function testSendResponse_HappyPath(): void { parent::assertSame('{"jsonapi":{"version":"1.1"}}', $output); } - #[RunInSeparateProcess] public function testSendResponse_NoContent(): void { $this->document->setHttpStatusCode(204); @@ -47,7 +44,6 @@ public function testSendResponse_NoContent(): void { parent::assertSame(204, http_response_code()); } - #[RunInSeparateProcess] public function testSendResponse_ContentTypeHeader(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -77,7 +73,6 @@ public function testSendResponse_ContentTypeHeader(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Jsonp->value], xdebug_get_headers()); } - #[RunInSeparateProcess] #[Group('Extensions')] public function testSendResponse_ContentTypeHeaderWithExtensions(): void { if (extension_loaded('xdebug') === false) { @@ -107,7 +102,6 @@ public function testSendResponse_ContentTypeHeaderWithExtensions(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Official->value.'; ext="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); } - #[RunInSeparateProcess] #[Group('Profiles')] public function testSendResponse_ContentTypeHeaderWithProfiles(): void { if (extension_loaded('xdebug') === false) { @@ -131,7 +125,6 @@ public function testSendResponse_ContentTypeHeaderWithProfiles(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Official->value.'; profile="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); } - #[RunInSeparateProcess] public function testSendResponse_StatusCodeHeader(): void { ob_start(); @@ -158,7 +151,6 @@ public function testSendResponse_StatusCodeHeader(): void { parent::assertSame(503, http_response_code()); } - #[RunInSeparateProcess] public function testSendResponse_CustomJson(): void { $options = ['json' => '{"foo":42}']; From dc35c9dd7294da184ca4ab79a2777d29884d7120 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 3 Jan 2026 13:31:34 +0100 Subject: [PATCH 18/20] make extra the default --- examples/bootstrap_examples.php | 2 +- phpstan.bonus.neon | 5 -- phpstan.neon | 104 +++++++++++++++++++------------- script/lint | 2 +- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index 6cd955f1..a8532886 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -75,7 +75,7 @@ public static function getEntity(string $type, int $id): ExampleUser { $user = new ExampleUser($id); foreach ($record as $key => $value) { - $user->$key = $value; + $user->$key = $value; // @phpstan-ignore property.dynamicName } return $user; diff --git a/phpstan.bonus.neon b/phpstan.bonus.neon index f642c3fd..08b9682a 100644 --- a/phpstan.bonus.neon +++ b/phpstan.bonus.neon @@ -1,10 +1,5 @@ includes: - phpstan.neon - - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: level: max - - # @see https://github.com/phpstan/phpstan-strict-rules - strictRules: - allRules: true diff --git a/phpstan.neon b/phpstan.neon index e33f93b1..67bb4b53 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,6 @@ +includes: + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + parameters: # slowly increase level: 9 @@ -33,7 +36,7 @@ parameters: treatPhpDocTypesAsCertain: true strictRules: - allRules: false + allRules: true reportUnmatchedIgnoredErrors: true ignoreErrors: @@ -41,58 +44,75 @@ parameters: # @see https://phpstan.org/user-guide/ignoring-errors#ignoring-in-configuration-file # testing AtMemberManager trait - - messages: - - '#Call to an undefined method object::hasAtMembers\(\)#' - - '#Call to an undefined method object::getAtMembers\(\)#' - - '#Call to an undefined method object::addAtMember\(\)#' - identifier: method.notFound - path: tests/helpers/AtMemberManagerTest.php + - + messages: + - '#Call to an undefined method object::hasAtMembers\(\)#' + - '#Call to an undefined method object::getAtMembers\(\)#' + - '#Call to an undefined method object::addAtMember\(\)#' + identifier: method.notFound + path: tests/helpers/AtMemberManagerTest.php # testing ExtensionMemberManager trait - - messages: - - '#Call to an undefined method object::addExtensionMember\(\)#' - - '#Call to an undefined method object::hasExtensionMembers\(\)#' - - '#Call to an undefined method object::getExtensionMembers\(\)#' - identifier: method.notFound - path: tests/helpers/ExtensionMemberManagerTest.php + - + messages: + - '#Call to an undefined method object::addExtensionMember\(\)#' + - '#Call to an undefined method object::hasExtensionMembers\(\)#' + - '#Call to an undefined method object::getExtensionMembers\(\)#' + identifier: method.notFound + path: tests/helpers/ExtensionMemberManagerTest.php # testing HttpStatusCodeManager trait - - messages: - - '#Call to an undefined method object::setHttpStatusCode\(\)#' - - '#Call to an undefined method object::hasHttpStatusCode\(\)#' - - '#Call to an undefined method object::getHttpStatusCode\(\)#' - identifier: method.notFound - path: tests/helpers/HttpStatusCodeManagerTest.php + - + messages: + - '#Call to an undefined method object::setHttpStatusCode\(\)#' + - '#Call to an undefined method object::hasHttpStatusCode\(\)#' + - '#Call to an undefined method object::getHttpStatusCode\(\)#' + identifier: method.notFound + path: tests/helpers/HttpStatusCodeManagerTest.php # testing LinksManager trait - - messages: - - '#Call to an undefined method object::addLink\(\)#' - - '#Call to an undefined method object::addLinkObject\(\)#' - - '#Call to an undefined method object::setLinksObject\(\)#' - - '#Call to an undefined method object::toArray\(\)#' - identifier: method.notFound - path: tests/helpers/LinksManagerTest.php + - + messages: + - '#Call to an undefined method object::addLink\(\)#' + - '#Call to an undefined method object::addLinkObject\(\)#' + - '#Call to an undefined method object::setLinksObject\(\)#' + - '#Call to an undefined method object::toArray\(\)#' + identifier: method.notFound + path: tests/helpers/LinksManagerTest.php # mixed is only used for input from library implementations - - identifier: argument.type - message: '#Parameter \#\d \$.+ of function .+ expects .+, mixed given#' - path: src/helpers/RequestParser.php + - + identifier: argument.type + message: '#Parameter \#\d \$.+ of function .+ expects .+, mixed given#' + path: src/helpers/RequestParser.php # mixed is only used for input from library implementations - - identifier: argument.type - message: '#Parameter \#\d \$.+ of class .+ constructor expects .+, mixed given#' - paths: - - src/helpers/RequestParser.php - - src/objects/ResourceObject.php + - + identifier: argument.type + message: '#Parameter \#\d \$.+ of class .+ constructor expects .+, mixed given#' + paths: + - src/helpers/RequestParser.php + - src/objects/ResourceObject.php # mixed is only used for input from library implementations - - identifier: argument.type - message: '#Parameter \#\d \$.+ of static method .+ expects .+, mixed given#' - path: tests/* + - + identifier: argument.type + message: '#Parameter \#\d \$.+ of static method .+ expects .+, mixed given#' + path: tests/* # mixed is only used for input from library implementations - - identifier: offsetAccess.nonOffsetAccessible - message: '#Cannot access offset .+ on mixed#' - paths: - - src/helpers/RequestParser.php - - tests/* + - + identifier: offsetAccess.nonOffsetAccessible + message: '#Cannot access offset .+ on mixed#' + paths: + - src/helpers/RequestParser.php + - tests/* + + # allow internal methods in examples and tests + - + identifiers: + - method.internal + - staticMethod.internalClass + paths: + - examples/* + - tests/* diff --git a/script/lint b/script/lint index 41580fcc..e9a265f8 100755 --- a/script/lint +++ b/script/lint @@ -5,7 +5,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd $SCRIPT_DIR/.. # whether to run bonus levels by default, or only when requested -BONUS_DEFAULT=0 +BONUS_DEFAULT=1 # docker configured and not inside docker CONSOLE_PREFIX="" From d4e5558358214241937a217379fc5f83ba081b08 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 4 Jan 2026 22:02:43 +0100 Subject: [PATCH 19/20] cleanup exceptions which can be catched by type hinting --- examples/bootstrap_examples.php | 7 +------ tests/example_output/ExampleVersionExtension.php | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index a8532886..6de38497 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -10,7 +10,6 @@ use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; -use alsvanzelf\jsonapi\objects\ResourceObject; ini_set('display_errors', 1); error_reporting(-1); @@ -134,11 +133,7 @@ public function getNamespace(): string { * optionally helpers for the specific extension */ - public function setVersion(ResourceInterface $resource, string $version): void { - if ($resource instanceof HasExtensionMembersInterface === false) { - throw new \Exception('resource doesn\'t have extension members'); - } - + public function setVersion(ResourceInterface & HasExtensionMembersInterface $resource, string $version): void { if ($resource instanceof ResourceDocument) { $resource->getResource()->addExtensionMember($this, 'id', $version); } diff --git a/tests/example_output/ExampleVersionExtension.php b/tests/example_output/ExampleVersionExtension.php index e8061cd4..4ac3b719 100644 --- a/tests/example_output/ExampleVersionExtension.php +++ b/tests/example_output/ExampleVersionExtension.php @@ -5,7 +5,6 @@ namespace alsvanzelf\jsonapiTests\example_output; use alsvanzelf\jsonapi\ResourceDocument; -use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\HasExtensionMembersInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; @@ -19,11 +18,7 @@ public function getNamespace(): string { return 'version'; } - public function setVersion(ResourceInterface $resource, string $version): void { - if ($resource instanceof HasExtensionMembersInterface === false) { - throw new InputException('resource doesn\'t have extension members'); - } - + public function setVersion(ResourceInterface & HasExtensionMembersInterface $resource, string $version): void { if ($resource instanceof ResourceDocument) { $resource->getResource()->addExtensionMember($this, 'id', $version); } From d994b818978c3dbaaabb585a376ee4cb128c7c21 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 4 Jan 2026 22:16:48 +0100 Subject: [PATCH 20/20] increase coverage again --- src/Document.php | 2 +- tests/DocumentTest.php | 9 +++++++++ tests/profiles/CursorPaginationProfileTest.php | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Document.php b/src/Document.php index 074f36c8..3af54d97 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().', '.json_last_error_msg()); + throw new Exception('failed to encode json: '.json_last_error_msg()); } if ($options['jsonpCallback'] !== null) { diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 3c8d3131..c166f27c 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -305,6 +305,15 @@ public function testToJson_InvalidUtf8(): void { $this->document->toJson($options); } + 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->document->toJson($options); + } + public function testJsonSerialize_HappyPath(): void { $this->document->addMeta('foo', 'bar'); diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 91ecaa36..9df411a3 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\CollectionDocument; use alsvanzelf\jsonapi\ResourceDocument; +use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceObject; use alsvanzelf\jsonapi\profiles\CursorPaginationProfile; @@ -351,4 +352,18 @@ public function testSetQueryParameter_EncodedUrl(): void { parent::assertSame('/people?sort=x&page%5Bsize%5D=10&page%5Bafter%5D=bar', $newUrl); } + + public function testSetQueryParameter_WithBrokenUrl(): void { + $profile = new CursorPaginationProfile(); + $method = new \ReflectionMethod($profile, 'setQueryParameter'); + + $url = 'foo'; + $key = 'page[after]'; + $value = 'bar'; + + $this->expectException(InputException::class); + $this->expectExceptionMessage('missing or broken query parameters in url'); + + $method->invoke($profile, $url, $key, $value); + } }