diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php
index acf810c..c18cf55 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');
}
@@ -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/examples/collection.php b/examples/collection.php
index cdd5a38..ab11fc8 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 ee7fe9e..5cb3dec 100644
--- a/phpcs.bonus.xml
+++ b/phpcs.bonus.xml
@@ -21,5 +21,7 @@
-
+
+
+
diff --git a/phpstan.bonus.neon b/phpstan.bonus.neon
index ae9db4e..ddc4b48 100644
--- a/phpstan.bonus.neon
+++ b/phpstan.bonus.neon
@@ -4,9 +4,7 @@ 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 a5a5f41..d0cf43a 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/Document.php b/src/Document.php
index 0c2dfcd..f1d599b 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/helpers/RequestParser.php b/src/helpers/RequestParser.php
index 0863c30..24918ca 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/LinkObject.php b/src/objects/LinkObject.php
index 5a13d2f..7fbb4fd 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/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php
index e44ece9..6d72df4 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;
}
@@ -316,6 +311,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/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php
index 5f09cd4..b45be05 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 144d574..f9a2276 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/example_output/ExampleTimestampsProfile.php b/tests/example_output/ExampleTimestampsProfile.php
index 210af09..4129a35 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);
diff --git a/tests/example_output/collection/collection.php b/tests/example_output/collection/collection.php
index 991fbae..a3a0265 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);
diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php
index 2f03689..fa2120f 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 64e65ff..8953919 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);
+ }
}
diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php
index 42250a6..9d67e33 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 {