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 acf810cc..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); @@ -18,7 +17,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,34 +58,43 @@ class ExampleDataset { ], ]; - public static function getRecord($type, $id) { - if (!isset(self::$records[$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'); } return self::$records[$type][$id]; } - public static function getEntity($type, $id) { + public static function getEntity(string $type, int $id): ExampleUser { $record = self::getRecord($type, $id); $user = new ExampleUser($id); foreach ($record as $key => $value) { - $user->$key = $value; + $user->$key = $value; // @phpstan-ignore property.dynamicName } 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) { - $records = self::findRecords($type); - $entities = []; + /** + * @return ExampleUser[] + */ + public static function findEntities(string $type): array { + $recordIds = array_keys(self::findRecords($type)); + $entities = []; - foreach ($records as $id => $record) { + foreach ($recordIds as $id) { $entities[$id] = self::getEntity($type, $id); } @@ -94,15 +103,15 @@ public static function findEntities($type) { } 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() { + public function getCurrentLocation(): string { return 'Earth'; } } @@ -124,11 +133,7 @@ public function getNamespace(): string { * optionally helpers for the specific extension */ - public function setVersion(ResourceInterface $resource, $version) { - 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); } @@ -151,14 +156,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, + ): void { $timestamps = []; if ($created !== null) { $timestamps['created'] = $created->format(\DateTime::ISO8601); diff --git a/examples/collection.php b/examples/collection.php index cdd5a38c..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'); @@ -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/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 1cb9d4ab..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'); @@ -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->toJson() and sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.ContentTypeEnum::Official->value.']

'; 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 73db5dc8..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 @@ -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/examples/request_superglobals.php b/examples/request_superglobals.php index cb93d06f..e4a0be9a 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 @@ -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).'
'; 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/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..08b9682a 100644 --- a/phpstan.bonus.neon +++ b/phpstan.bonus.neon @@ -1,12 +1,5 @@ includes: - phpstan.neon - - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: - level: 10 - - treatPhpDocTypesAsCertain: true - - # @see https://github.com/phpstan/phpstan-strict-rules - strictRules: - allRules: true + level: max diff --git a/phpstan.neon b/phpstan.neon index a5a5f410..67bb4b53 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,20 +1,118 @@ +includes: + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + parameters: # slowly increase - level: 4 + level: 9 paths: - src/ - tests/ - examples/ - + typeAliases: - PHPStanTypeAlias_InternalOptions: 'array' + 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: false + treatPhpDocTypesAsCertain: true strictRules: - allRules: false + allRules: true reportUnmatchedIgnoredErrors: true 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 + + # 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/* + + # allow internal methods in examples and tests + - + identifiers: + - method.internal + - staticMethod.internalClass + paths: + - examples/* + - tests/* diff --git a/rector.php b/rector.php index bba93d87..4d2b6b90 100644 --- a/rector.php +++ b/rector.php @@ -2,9 +2,18 @@ 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; -use Rector\TypeDeclaration\Rector\Class_\AddTestsVoidReturnTypeWhereNoReturnRector; +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 @@ -15,24 +24,67 @@ __DIR__ . '/tests', __DIR__ . '/examples', ]) + ->withRootFiles() ->withRules([ DeclareStrictTypesRector::class, - AddTestsVoidReturnTypeWhereNoReturnRector::class, ]) ->withSkip([ // better explicit readability + FlipTypeControlToUseExclusiveTypeRector::class, IfIssetToCoalescingRector::class, + SimplifyUselessVariableRector::class, + SimplifyIfReturnBoolRector::class, + + // not all rules from code style + NewlineAfterStatementRector::class, + NewlineBeforeNewAssignSetRector::class, + NewlineBetweenClassLikeStmtsRector::class, + SimplifyQuoteEscapeRector::class, + + // rely on types from interfaces + DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, + + // better readability using function declaration sorting + SortNamedParamRector::class, + + // explicit testing private properties + RemoveUnusedPrivatePropertyRector::class => [ + 'tests/ConverterTest.php', + ], ]) // tab-based indenting ->withIndent(indentChar: "\t", indentSize: 1) + // importing FQNs + ->withImportNames(importShortClasses: false) // lowest supported php version ->withPhpSets(php82: true) - // slowly increase levels - ->withTypeCoverageLevel(1) - ->withDeadCodeLevel(1) + // 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, + ) - // @todo add `->withPreparedSets()` once on a higher level with other rules + // vendor sets + ->withAttributesSets() + ->withComposerBased(phpunit: true) ; 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="" diff --git a/src/CollectionDocument.php b/src/CollectionDocument.php index d3663039..9bbed6e2 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_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_InternalOptions $options {@see CollectionDocument::$defaults} + * @param PHPStanTypeAlias_Options_CollectionDocument $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 0c2dfcdd..3af54d97 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_DefaultOptions_Document */ + protected static array $documentDefaults = [ /** * encode to json with these default options */ @@ -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.'"'); } } @@ -177,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; } @@ -261,10 +258,11 @@ 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::$defaults, ...$options]; + $options = [...self::$documentDefaults, ...$options]; $array = $options['array'] ?? $this->toArray(); @@ -274,6 +272,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_msg()); + } + if ($options['jsonpCallback'] !== null) { $json = $options['jsonpCallback'].'('.$json.')'; } @@ -282,7 +285,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); @@ -303,6 +306,9 @@ public function sendResponse(array $options=[]): void { * JsonSerializable */ + /** + * @return array + */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); diff --git a/src/ErrorsDocument.php b/src/ErrorsDocument.php index 546fbab1..ca15c6a5 100644 --- a/src/ErrorsDocument.php +++ b/src/ErrorsDocument.php @@ -13,10 +13,10 @@ class ErrorsDocument extends Document { /** @var ErrorObject[] */ protected array $errors = []; - /** @var array> */ - protected array $httpStatusCodes; - /** @var PHPStanTypeAlias_InternalOptions */ - protected static array $defaults = [ + /** @var array> */ + protected array $httpStatusCodes = []; + /** @var PHPStanTypeAlias_DefaultOptions_ErrorsDocument */ + 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_Options_ErrorsDocument $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_Options_ErrorsDocumentAndErrorObject $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)); @@ -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/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/ResourceDocument.php b/src/ResourceDocument.php index 17bfda7d..6b9f4b39 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_DefaultOptions_ResourceDocument */ + protected static array $resourceDocumentDefaults = [ /** * add resources inside relationships to /included when adding resources to the collection */ @@ -51,7 +51,8 @@ public function __construct(?string $type=null, string|int|null $id=null) { */ /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} {@see ResourceObject::$defaults} + * @param array $attributes + * @param PHPStanTypeAlias_Options_ResourceDocumentAndValidator $options {@see ResourceDocument::$resourceDocumentDefaults} {@see ResourceObject::$resourceObjectDefaults} */ public static function fromArray( array $attributes, @@ -66,7 +67,7 @@ public static function fromArray( } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_Options_ResourceDocumentAndValidator $options {@see ResourceDocument::$resourceDocumentDefaults} */ public static function fromObject( object $attributes, @@ -82,8 +83,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_Options_Validator $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function add(string $key, mixed $value, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -101,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_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} */ public function addRelationship( string $key, @@ -114,7 +115,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 +189,7 @@ public function setLocalId(string|int $localId): void { } /** - * @param PHPStanTypeAlias_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} */ public function setAttributesObject(AttributesObject $attributesObject, array $options=[]): void { if ($this->resource instanceof ResourceObject === false) { @@ -203,14 +204,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_Options_ResourceDocument $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 +225,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_Options_ResourceDocument $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 +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_InternalOptions $options {@see ResourceDocument::$defaults} + * @param PHPStanTypeAlias_Options_ResourceDocument $options {@see ResourceDocument::$resourceDocumentDefaults} * * @throws InputException if the $resource is a ResourceDocument itself */ @@ -260,7 +261,7 @@ public function setPrimaryResource(ResourceInterface $resource, array $options=[ /** @var ResourceIdentifierObject|ResourceObject $resource */ - $options = [...self::$defaults, ...$options]; + $options = [...self::$resourceDocumentDefaults, ...$options]; $this->resource = $resource; @@ -293,7 +294,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/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..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; @@ -13,6 +14,9 @@ * @internal */ class Converter { + /** + * @return array + */ public static function objectToArray(object $object): array { if ($object instanceof ObjectInterface) { return $object->toArray(); @@ -23,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 0863c306..5c2ac6ad 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_DefaultOptions_RequestParser */ + protected static array $requestParserDefaults = [ /** * reformat the include query parameter paths to nested arrays * this allows easier processing on each step of the chain @@ -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 */ @@ -44,20 +44,30 @@ 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; 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 === '') { + if ($document === '' || $document === false) { $document = []; } elseif ($documentIsJsonapi || $documentIsJson) { @@ -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 = []; } @@ -102,7 +114,7 @@ public function getSelfLink(): string { } public function hasIncludePaths(): bool { - return isset($this->queryParameters['include']); + return $this->hasQueryParameter('include'); } /** @@ -111,17 +123,17 @@ 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} - * @return string[]|array + * @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::$defaults, ...$options]; + $options = [...self::$requestParserDefaults, ...$options]; if ($options['useNestedIncludePaths'] === false) { return $includePaths; } @@ -143,22 +155,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,20 +181,20 @@ public function hasSortFields(): bool { * * @todo return some kind of SortFieldObject * - * @param PHPStanTypeAlias_InternalOptions $options {@see RequestParser::$defaults} + * @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::$defaults, ...$options]; + $options = [...self::$requestParserDefaults, ...$options]; if ($options['useAnnotatedSortFields'] === false) { return $fields; } @@ -206,7 +218,7 @@ public function getSortFields(array $options=[]): array { } public function hasPagination(): bool { - return isset($this->queryParameters['page']); + return $this->hasQueryParameter('page'); } /** @@ -216,18 +228,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 { @@ -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 { @@ -294,4 +306,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 d1010b61..981168c3 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_DefaultOptions_Validator */ + 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_Options_Validator $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..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_InternalOptions $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_InternalOptions $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 f42b426b..cfa16bf1 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_Options_Validator $options */ public function addAttribute(string $key, mixed $value, array $options=[]): void; } 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/ErrorObject.php b/src/objects/ErrorObject.php index cd47138c..23e07c75 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_DefaultOptions_ErrorObject */ + 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_Options_ErrorObject $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/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/src/objects/LinksObject.php b/src/objects/LinksObject.php index 3485f309..e8eb7595 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 = []; /** @@ -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/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php index e44ece9a..4c74e41c 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; } @@ -122,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); @@ -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/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/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/src/objects/ResourceObject.php b/src/objects/ResourceObject.php index 8f115f59..c42b4697 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_DefaultOptions_Validator */ + 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_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_InternalOptions $options {@see ResourceObject::$defaults} + * @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,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_Options_Validator $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_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_InternalOptions $options {@see ResourceObject::$defaults} + * @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_InternalOptions $options {@see ResourceObject::$defaults} + * @param PHPStanTypeAlias_Options_Validator $options {@see ResourceObject::$resourceObjectDefaults} * * @throws DuplicateException if the resource is contained as a resource in the relationship */ @@ -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); } /** diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 75825210..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; @@ -56,10 +57,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 +72,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): void { $this->setPaginationMeta($paginable, $exactTotal, $bestGuessTotal); } @@ -114,7 +115,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); } @@ -137,15 +138,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()); } @@ -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/CollectionDocumentTest.php b/tests/CollectionDocumentTest.php index b05004ab..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(); @@ -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,14 +119,15 @@ public function testSetPaginationLinks_IndividualLinks($key, $previous, $next, $ } } - public static function dataProviderSetPaginationLinks_IndividualLinks() { - 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], - ]; + /** + * @return \Iterator<(int | string), array{(string | null), (string | null), (string | null), (string | null), (string | 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 d2af386a..8eeb973f 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -10,9 +10,10 @@ use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\AttributesObject; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class ConverterTest extends TestCase { +final class ConverterTest extends TestCase { public function testObjectToArray_HappyPath(): void { $object = new \stdClass(); $object->foo = 'bar'; @@ -29,10 +30,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,50 +59,43 @@ 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 [ - ['value', 'value'], - ['camelValue', 'camel Value'], - ['TitleValue', 'Title Value'], - ['VALUE', 'VALUE'], - ['eclipseRCPExt', 'eclipse RCP Ext'], - ]; - } - /** - * @group Extensions - * @group Profiles + * @return \Iterator<(int | string), array{string, string}> */ + 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']; + } + + #[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 625088ef..c166f27c 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -12,12 +12,13 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\LinkObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class DocumentTest extends TestCase { - private object $document; +final 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 * @@ -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']); @@ -312,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/ErrorsDocumentTest.php b/tests/ErrorsDocumentTest.php index c7952517..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)); @@ -120,19 +120,20 @@ public function testDetermineHttpStatusCode_HappyPath(int $expectedAdvisedErrorC parent::assertSame($expectedAdvisedErrorCode, $advisedErrorCode); } - public static function dataProviderDetermineHttpStatusCode_HappyPath() { - 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]], - ]; + /** + * @return \Iterator<(int | string), array{int, non-empty-array}> + */ + 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 75a699f0..260efe26 100644 --- a/tests/ExampleOutputTest.php +++ b/tests/ExampleOutputTest.php @@ -4,23 +4,23 @@ namespace alsvanzelf\jsonapiTests; +use alsvanzelf\jsonapi\interfaces\DocumentInterface; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group OutputOnly - */ -class ExampleOutputTest extends TestCase { - private static $defaults = [ +#[Group('OutputOnly')] +final class ExampleOutputTest extends TestCase { + /** @var PHPStanTypeAlias_Options_Document */ + private static array $defaults = [ 'prettyPrint' => true, ]; #[DataProvider('dataProviderTestOutput')] - public function testOutput($generator, $expectedJson, array $options=[], $testName=null): void { - $options = [...self::$defaults, ...$options]; - - $document = $generator::createJsonapiDocument(); - $actualJson = $document->toJson($options); + public function testOutput(object $generator, ?string $expectedJson, ?string $testName): void { + /** @var DocumentInterface $document */ + $document = $generator::createJsonapiDocument(); // @phpstan-ignore staticMethod.notFound + $actualJson = $document->toJson(self::$defaults); // adhere to editorconfig $actualJson = str_replace(' ', "\t", $actualJson).PHP_EOL; @@ -35,8 +35,18 @@ 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); + if ($directories === false) { + throw new \Exception('failed to fetch example output'); + } $testCases = []; foreach ($directories as $directory) { @@ -47,16 +57,15 @@ 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); + if ($expectedJson === false) { + throw new \Exception('something went wrong fetching expected output'); + } } - $testCases[$testName] = [$generator, $expectedJson, $options, $testName]; + $testCases[$testName] = [$generator, $expectedJson, $testName]; } return $testCases; 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 144d574c..d903272e 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; @@ -14,9 +15,10 @@ use alsvanzelf\jsonapi\objects\RelationshipsObject; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class ResourceDocumentTest extends TestCase { +final class ResourceDocumentTest extends TestCase { public function testConstructor_NoResource(): void { $document = new ResourceDocument(); @@ -61,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'])); @@ -100,6 +100,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); @@ -110,18 +170,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..98820827 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -8,15 +8,14 @@ use alsvanzelf\jsonapi\enums\ContentTypeEnum; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group SeparateProcess - */ -class SeparateProcessTest extends TestCase { - private object $document; +#[Group('SeparateProcess')] +final 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 * @@ -26,9 +25,6 @@ public function setUp(): void { $this->document = new class extends Document {}; } - /** - * @runInSeparateProcess - */ public function testSendResponse_HappyPath(): void { ob_start(); $this->document->sendResponse(); @@ -37,9 +33,6 @@ public function testSendResponse_HappyPath(): void { parent::assertSame('{"jsonapi":{"version":"1.1"}}', $output); } - /** - * @runInSeparateProcess - */ public function testSendResponse_NoContent(): void { $this->document->setHttpStatusCode(204); @@ -51,9 +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'); @@ -83,10 +73,7 @@ public function testSendResponse_ContentTypeHeader(): void { parent::assertSame(['Content-Type: '.ContentTypeEnum::Jsonp->value], xdebug_get_headers()); } - /** - * @runInSeparateProcess - * @group Extensions - */ + #[Group('Extensions')] public function testSendResponse_ContentTypeHeaderWithExtensions(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -115,10 +102,7 @@ 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 - */ + #[Group('Profiles')] public function testSendResponse_ContentTypeHeaderWithProfiles(): void { if (extension_loaded('xdebug') === false) { parent::markTestSkipped('can not run without xdebug'); @@ -141,9 +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(); @@ -170,9 +151,6 @@ public function testSendResponse_StatusCodeHeader(): void { parent::assertSame(503, http_response_code()); } - /** - * @runInSeparateProcess - */ public function testSendResponse_CustomJson(): void { $options = ['json' => '{"foo":42}']; diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 6fa9235d..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(); @@ -147,52 +147,55 @@ 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 [ - ['foo'], - ['f_o'], - ['f-o'], - ['42foo'], - ['42'], - ]; + /** + * @return \Iterator<(int | string), array{string}> + */ + public static function dataProviderCheckMemberName_HappyPath(): \Iterator { + yield ['foo']; + yield ['f_o']; + yield ['f-o']; + yield ['42foo']; + yield ['42']; } #[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 [ - ['_'], - ['-'], - ['foo-'], - ['-foo'], - ]; + /** + * @return \Iterator<(int | string), array{string}> + */ + public static function dataProviderCheckMemberName_InvalidNames(): \Iterator { + yield ['_']; + yield ['-']; + yield ['foo-']; + yield ['-foo']; } #[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)); } - public static function dataProviderCheckHttpStatusCode_HappyPath() { - return [ - [false, 42], - [true, 100], - [true, 200], - [true, 300], - [true, 400], - [true, 500], - [false, 600], - [false, '42'], - [true, '100'], - ]; + /** + * @return \Iterator<(int | string), array{bool, (int | string)}> + */ + 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/example_output/ExampleTimestampsProfile.php b/tests/example_output/ExampleTimestampsProfile.php index 210af099..24fc00e5 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, + ): 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..6e607609 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() { + public function getCurrentLocation(): string { return 'Earth'; } } diff --git a/tests/example_output/ExampleVersionExtension.php b/tests/example_output/ExampleVersionExtension.php index dea5cde6..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, $version) { - 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); } 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 991fbaee..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; @@ -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/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/extensions/AtomicOperationsDocumentTest.php b/tests/extensions/AtomicOperationsDocumentTest.php index 56b9bb9c..af9bee67 100644 --- a/tests/extensions/AtomicOperationsDocumentTest.php +++ b/tests/extensions/AtomicOperationsDocumentTest.php @@ -7,12 +7,11 @@ 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 - */ -class AtomicOperationsDocumentTest extends TestCase { +#[Group('Extensions')] +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..e9bd1ae7 100644 --- a/tests/helpers/ExtensionMemberManagerTest.php +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -7,12 +7,11 @@ 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 - */ -class ExtensionMemberManagerTest extends TestCase { +#[Group('Extensions')] +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 47df373c..836099fc 100644 --- a/tests/helpers/LinksManagerTest.php +++ b/tests/helpers/LinksManagerTest.php @@ -8,10 +8,10 @@ use alsvanzelf\jsonapi\objects\LinkObject; use PHPUnit\Framework\TestCase; -class LinksManagerTest extends TestCase { +final 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; @@ -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) } }; } diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php index 2f03689c..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', @@ -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(); @@ -188,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/AttributesObjectTest.php b/tests/objects/AttributesObjectTest.php index ca604990..061c174b 100644 --- a/tests/objects/AttributesObjectTest.php +++ b/tests/objects/AttributesObjectTest.php @@ -6,9 +6,10 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\AttributesObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class AttributesObjectTest extends TestCase { +final class AttributesObjectTest extends TestCase { public function testFromObject_HappyPath(): void { $object = new \stdClass(); $object->foo = 'bar'; @@ -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 8bb42453..ba374c79 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -7,9 +7,10 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\ErrorObject; +use PHPUnit\Framework\Attributes\Group; 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); @@ -42,7 +43,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); @@ -155,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 29965b54..2ba8a776 100644 --- a/tests/objects/JsonapiObjectTest.php +++ b/tests/objects/JsonapiObjectTest.php @@ -7,9 +7,10 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\JsonapiObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class JsonapiObjectTest extends TestCase { +final class JsonapiObjectTest extends TestCase { public function testAddMeta_HappyPath(): void { $jsonapiObject = new JsonapiObject($version=null); @@ -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 fe6da28a..78b5eab2 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -6,9 +6,10 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\LinkObject; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -class LinkObjectTest extends TestCase { +final class LinkObjectTest extends TestCase { public function testSetDescribedBy_HappyPath(): void { $linkObject = new LinkObject(); @@ -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/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..050254f1 100644 --- a/tests/objects/MetaObjectTest.php +++ b/tests/objects/MetaObjectTest.php @@ -6,9 +6,10 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\objects\MetaObject; +use PHPUnit\Framework\Attributes\Group; 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']); @@ -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 e35a0a51..174e83f8 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -13,9 +13,10 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use PHPUnit\Framework\Attributes\Group; 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)); @@ -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); @@ -311,7 +310,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 +322,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']); 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 64e65ff2..e1035347 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -6,11 +6,14 @@ 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\Attributes\Group; use PHPUnit\Framework\TestCase; -class ResourceIdentifierObjectTest extends TestCase { +final class ResourceIdentifierObjectTest extends TestCase { public function testSetId_HappyPath(): void { $resourceIdentifierObject = new ResourceIdentifierObject(); $resourceIdentifierObject->setType('test'); @@ -55,6 +58,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(); + $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 +200,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(); @@ -178,9 +217,7 @@ public function testIsEmpty_WithAtMembers(): void { parent::assertFalse($resourceIdentifierObject->isEmpty()); } - /** - * @group Extensions - */ + #[Group('Extensions')] public function testIsEmpty_WithExtensionMembers(): void { $resourceIdentifierObject = new ResourceIdentifierObject(); @@ -190,4 +227,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/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 42250a6d..9df411a3 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -6,15 +6,15 @@ 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; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group Profiles - */ -class CursorPaginationProfileTest extends TestCase { +#[Group('Profiles')] +final class CursorPaginationProfileTest extends TestCase { public function testSetLinks_HappyPath(): void { $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); @@ -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 { @@ -343,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); + } }