diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..66a983e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: "https://www.paypal.me/f3ath" + diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..eae93ab --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,36 @@ +name: PHP Composer + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: composer run-script test diff --git a/.gitignore b/.gitignore index f22a32d..c195581 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ composer.phar /vendor/ /composer.lock /.php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..acd1414 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +exclude('somedir') + ->notPath('src/Symfony/Component/Translation/Tests/fixtures/resources.php') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'braces' => [ + 'allow_single_line_closure' => true, + 'position_after_functions_and_oop_constructs' => 'same'], +]) + ->setFinder($finder); \ No newline at end of file diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index 06b82a3..0000000 --- a/.php_cs.dist +++ /dev/null @@ -1,53 +0,0 @@ -files() - ->name('*.php') - ->in(__DIR__ . '/examples') - ->in(__DIR__ . '/src') - ->in(__DIR__ . '/test'); -return PhpCsFixer\Config::create() - ->setUsingCache(true) - ->setRiskyAllowed(true) - ->setRules([ - '@PSR2' => true, - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => true, - 'cast_spaces' => true, - 'concat_space' => true, - 'declare_strict_types' => true, - 'include' => true, - 'is_null' => true, - 'lowercase_cast' => true, - 'mb_str_functions' => true, - 'method_separation' => true, - 'native_function_casing' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_unused_imports' => true, - 'no_whitespace_in_blank_line' => true, - 'object_operator_without_whitespace' => true, - 'ordered_imports' => true, - 'phpdoc_align' => true, - 'phpdoc_indent' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_package' => true, - 'phpdoc_order' => true, - 'phpdoc_scalar' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'psr0' => true, - 'short_scalar_cast' => true, - 'single_blank_line_before_namespace' => true, - 'single_quote' => true, - 'standardize_not_equals' => true, - 'strict_comparison' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline_array' => true, - 'whitespace_after_comma_in_array' => true, - ]) - ->setFinder($finder); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bef7869..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: php -php: - - '7.2' - -before_script: - - composer install - - mkdir build/logs -p - -script: - - vendor/bin/php-cs-fixer fix -v --dry-run - - phpunit --coverage-clover build/logs/clover.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index bebaa62..22fd9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,42 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -## [2.0.0] - 2018-02-26 -V2 initial release +## [3.0.0] - 2022-07-26 +### Changed +- The package is migrated to PHP 8.1 -[Unreleased]: https://github.com/json-api-php/json-api/compare/2.0.0...HEAD +### Removed +- Support for PHP 7 and older versions +- Compound document validation logic is dropped + +## [2.2.0] - 2020-10-12 +### Added +- `NewResourceObject` to allow omitting `id` in resources to-be-created (#108) + +## [2.1.2] - 2020-03-16 +### Fixed +- Related links must be allowed inside relationship documents (#104) + +## [2.1.1] - 2019-12-19 +### Fixed +- `ResourceIdentifier` does not allow multiple meta members (#99) + +## [2.1.0] - 2019-02-25 +### Fixed +- Relationship without data property (#92) + +## [2.0.1] - 2018-12-31 +### Changed +- Downgraded min required php version to 7.1 + +## 2.0.0 - 2018-02-26 +### Added +- v2 initial release + +[Unreleased]: https://github.com/json-api-php/json-api/compare/3.0.0...HEAD +[3.0.0]: https://github.com/json-api-php/json-api/compare/2.2.2...3.0.0 +[2.2.0]: https://github.com/json-api-php/json-api/compare/2.1.2...2.2.0 +[2.1.2]: https://github.com/json-api-php/json-api/compare/2.1.1...2.1.2 +[2.1.1]: https://github.com/json-api-php/json-api/compare/2.1.0...2.1.1 +[2.1.0]: https://github.com/json-api-php/json-api/compare/2.0.1...2.1.0 +[2.0.1]: https://github.com/json-api-php/json-api/compare/2.0.0...2.0.1 diff --git a/README.md b/README.md index 1677ff1..2272090 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # [JSON API](http://jsonapi.org) spec implemented in PHP 7. Immutable -**This is v2 of the implementation. For v1 click [here](https://github.com/json-api-php/json-api/tree/v1).** - The goal of this library is to ensure strict validity of JSON API documents being produced. JSON: @@ -65,13 +63,14 @@ First, take a look at the examples. All of them are runnable. - [Simple Document](./examples/simple_doc.php) (the same as above) - [Extensive Compound Document](./examples/compound_doc.php) -The library API and use-cases are expressed in comprehensive suite of tests. +The library API and use-cases are expressed in a comprehensive suite of tests. - Data Documents (containing primary data) - [with a single Resource Object](./test/DataDocument/SingleResourceObjectTest.php) - [with a single Resource Identifier](./test/DataDocument/SingleResourceIdentifierTest.php) - [with null data](./test/DataDocument/NullDataTest.php) - [with multiple Resource Objects](./test/DataDocument/ManyResourceObjectsTest.php) - [with multiple Resource Identifiers](./test/DataDocument/ManyResourceIdentifiersTest.php) + - [with a new Resource (no id)](./test/NewResourceObjectTest.php) - [Compound Documents](./test/CompoundDocumentTest.php) - [Error Documents](./test/ErrorDocumentTest.php) - [Meta Documents (containing neither data nor errors)](./test/MetaDocumentTest.php) diff --git a/composer.json b/composer.json index 1cdeb90..5a7e2b9 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "json-api-php/json-api", - "description": "An attempt to express JSON API specs (jsonapi.org) in object-oriented way as a set of PHP 7 classes", + "description": "JSON API specs (jsonapi.org) as a set of PHP classes", "type": "library", "prefer-stable": true, "license": "MIT", @@ -11,11 +11,12 @@ } ], "require": { - "php": ">=7.2" + "php": ">=8.1", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^7.0", - "friendsofphp/php-cs-fixer": "^2.2" + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "^3.9" }, "autoload": { "psr-4": { diff --git a/examples/compound_doc.php b/examples/compound_doc.php index ddb6fd9..1d0cbbb 100644 --- a/examples/compound_doc.php +++ b/examples/compound_doc.php @@ -1,4 +1,6 @@ - - - - - ./test - - - - - ./src/ - - + + + + + ./src/ + + + + + ./test + + diff --git a/src/Attribute.php b/src/Attribute.php index 69c6621..8276746 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -1,4 +1,6 @@ -validateFieldName($name); $this->name = $name; $this->val = $val; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { child($o, 'attributes')->{$this->name} = $this->val; } } diff --git a/src/CompoundDocument.php b/src/CompoundDocument.php index 7e84887..7a15492 100644 --- a/src/CompoundDocument.php +++ b/src/CompoundDocument.php @@ -1,26 +1,25 @@ -validateLinkage($data); + public function __construct(PrimaryData $data, Included $included, DataDocumentMember ...$members) { $this->doc = combine($data, $included, ...$members); } - public function jsonSerialize() - { + public function jsonSerialize(): object { return $this->doc; } } diff --git a/src/DataDocument.php b/src/DataDocument.php index d8b986d..20d0e7c 100644 --- a/src/DataDocument.php +++ b/src/DataDocument.php @@ -1,25 +1,25 @@ -value = combine($data, ...$members); } - public function jsonSerialize() - { + public function jsonSerialize(): object { return $this->value; } } diff --git a/src/EmptyRelationship.php b/src/EmptyRelationship.php new file mode 100644 index 0000000..2c123bb --- /dev/null +++ b/src/EmptyRelationship.php @@ -0,0 +1,34 @@ +obj = combine($member, ...$members); + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + child($o, 'relationships')->{$this->name} = $this->obj; + } +} diff --git a/src/Error.php b/src/Error.php index 65a012d..8a222fe 100644 --- a/src/Error.php +++ b/src/Error.php @@ -1,4 +1,6 @@ -error = (object) []; + public function __construct(ErrorMember ...$members) { + $this->error = (object)[]; foreach ($members as $member) { $member->attachTo($this->error); } } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->errors[] = $this->error; } } diff --git a/src/Error/Code.php b/src/Error/Code.php index eafbc72..40455dd 100644 --- a/src/Error/Code.php +++ b/src/Error/Code.php @@ -1,26 +1,23 @@ -code = $code; + public function __construct(private readonly string $code) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->code = $this->code; } } diff --git a/src/Error/Detail.php b/src/Error/Detail.php index 5950b46..217c43d 100644 --- a/src/Error/Detail.php +++ b/src/Error/Detail.php @@ -1,26 +1,23 @@ -detail = $detail; + public function __construct(private readonly string $detail) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->detail = $this->detail; } } diff --git a/src/Error/Id.php b/src/Error/Id.php index 3553f02..371884f 100644 --- a/src/Error/Id.php +++ b/src/Error/Id.php @@ -1,26 +1,23 @@ -id = $id; + public function __construct(private readonly string $id) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->id = $this->id; } } diff --git a/src/Error/SourceParameter.php b/src/Error/SourceParameter.php index 613967a..d344d64 100644 --- a/src/Error/SourceParameter.php +++ b/src/Error/SourceParameter.php @@ -1,27 +1,25 @@ -parameter = $parameter; + public function __construct(private readonly string $parameter) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { child($o, 'source')->parameter = $this->parameter; } } diff --git a/src/Error/SourcePointer.php b/src/Error/SourcePointer.php index bfc6394..c33d0b1 100644 --- a/src/Error/SourcePointer.php +++ b/src/Error/SourcePointer.php @@ -1,24 +1,25 @@ -pointer = $pointer; + public function __construct(private readonly string $pointer) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { child($o, 'source')->pointer = $this->pointer; } } diff --git a/src/Error/Status.php b/src/Error/Status.php index a61031f..2083373 100644 --- a/src/Error/Status.php +++ b/src/Error/Status.php @@ -1,26 +1,23 @@ -status = $status; + public function __construct(private readonly string $status) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->status = $this->status; } } diff --git a/src/Error/Title.php b/src/Error/Title.php index d61a706..1352eb5 100644 --- a/src/Error/Title.php +++ b/src/Error/Title.php @@ -1,27 +1,24 @@ -title = $title; + public function __construct(private readonly string $title) { } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->title = $this->title; } } diff --git a/src/ErrorDocument.php b/src/ErrorDocument.php index e4a52e5..4cb70b5 100644 --- a/src/ErrorDocument.php +++ b/src/ErrorDocument.php @@ -1,28 +1,27 @@ -obj = (object) []; - $error->attachTo($this->obj); + public function __construct(ErrorDocumentMember ...$members) { + $this->obj = (object)[]; foreach ($members as $member) { $member->attachTo($this->obj); } } - public function jsonSerialize() - { + public function jsonSerialize(): object { return $this->obj; } } diff --git a/src/Included.php b/src/Included.php index 04c8d1c..96a1edb 100644 --- a/src/Included.php +++ b/src/Included.php @@ -1,45 +1,37 @@ -key(); if (isset($this->resources[$key])) { - throw new \LogicException("Resource $resource is already included"); + throw new LogicException("Resource $resource is already included"); } $this->resources[$key] = $resource; - $resource->registerIn($this->identifiers); } } - public function validateLinkage(PrimaryData $data): void - { - $registry = []; - $data->registerIn($registry); - foreach ($this->resources as $resource) { - if (isset($registry[$resource->key()]) || isset($this->identifiers[$resource->key()])) { - continue; - } - throw new \LogicException('Full linkage required for '.$resource); - } - } - - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { foreach ($this->resources as $resource) { $resource->attachAsIncludedTo($o); } diff --git a/src/Internal/Attachable.php b/src/Internal/Attachable.php index cccd352..01de494 100644 --- a/src/Internal/Attachable.php +++ b/src/Internal/Attachable.php @@ -1,11 +1,17 @@ -obj = (object)['type' => $type]; + + $this->addMembers(...$members); + } + + /** + * @param ResourceMember ...$members + * @internal + */ + protected function addMembers(ResourceMember ...$members): void { + $fields = []; + foreach ($members as $member) { + if ($member instanceof ResourceField) { + $name = $member->name(); + if (isset($fields[$name])) { + throw new LogicException("Field '$name' already exists'"); + } + $fields[$name] = true; + } + $member->attachTo($this->obj); + } + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = $this->obj; + } +} diff --git a/src/Internal/Collection.php b/src/Internal/Collection.php index e0e73d0..cd8b8d6 100644 --- a/src/Internal/Collection.php +++ b/src/Internal/Collection.php @@ -1,7 +1,8 @@ -link = (object) ['href' => $url]; + $this->link = (object)['href' => $url]; foreach ($metas as $meta) { $meta->attachTo($this->link); } diff --git a/src/Internal/MetaDocumentMember.php b/src/Internal/MetaDocumentMember.php index daff35c..e204a70 100644 --- a/src/Internal/MetaDocumentMember.php +++ b/src/Internal/MetaDocumentMember.php @@ -1,10 +1,11 @@ -name; } } diff --git a/src/Internal/ResourceMember.php b/src/Internal/ResourceMember.php index b20e3b0..ba4918d 100644 --- a/src/Internal/ResourceMember.php +++ b/src/Internal/ResourceMember.php @@ -1,10 +1,11 @@ -obj = (object) [ + public function __construct(string $version = '1.0', Meta $meta = null) { + $this->obj = (object)[ 'version' => $version, ]; - if ($meta) { - $meta->attachTo($this->obj); - } + $meta?->attachTo($this->obj); } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->jsonapi = $this->obj; } } diff --git a/src/Link/AboutLink.php b/src/Link/AboutLink.php index 31fe133..fdafefd 100644 --- a/src/Link/AboutLink.php +++ b/src/Link/AboutLink.php @@ -1,17 +1,22 @@ -about = $this->link; } } diff --git a/src/Link/FirstLink.php b/src/Link/FirstLink.php index f41ab3c..dfc5b6a 100644 --- a/src/Link/FirstLink.php +++ b/src/Link/FirstLink.php @@ -1,17 +1,22 @@ -first = $this->link; } } diff --git a/src/Link/LastLink.php b/src/Link/LastLink.php index b174cb2..a2215b8 100644 --- a/src/Link/LastLink.php +++ b/src/Link/LastLink.php @@ -1,17 +1,22 @@ -last = $this->link; } } diff --git a/src/Link/NextLink.php b/src/Link/NextLink.php index e1cce27..082fbcc 100644 --- a/src/Link/NextLink.php +++ b/src/Link/NextLink.php @@ -1,17 +1,22 @@ -next = $this->link; } } diff --git a/src/Link/PrevLink.php b/src/Link/PrevLink.php index 99a61c3..d38d025 100644 --- a/src/Link/PrevLink.php +++ b/src/Link/PrevLink.php @@ -1,17 +1,22 @@ -prev = $this->link; } } diff --git a/src/Link/RelatedLink.php b/src/Link/RelatedLink.php index c0005ff..501c2ec 100644 --- a/src/Link/RelatedLink.php +++ b/src/Link/RelatedLink.php @@ -1,17 +1,23 @@ -related = $this->link; } } diff --git a/src/Link/SelfLink.php b/src/Link/SelfLink.php index 17d96cd..0168cb1 100644 --- a/src/Link/SelfLink.php +++ b/src/Link/SelfLink.php @@ -1,19 +1,24 @@ -self = $this->link; } } diff --git a/src/Meta.php b/src/Meta.php index e186648..ebec4dc 100644 --- a/src/Meta.php +++ b/src/Meta.php @@ -1,33 +1,38 @@ -key = $key; - $this->value = $value; - } - public function attachTo(object $o): void - { - child($o, 'meta')->{$this->key} = $this->value; + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + child($o, 'meta')->{$this->key} = $this->value; + } } -} diff --git a/src/MetaDocument.php b/src/MetaDocument.php index 72ca320..14f0334 100644 --- a/src/MetaDocument.php +++ b/src/MetaDocument.php @@ -1,20 +1,20 @@ -doc = combine($meta, ...$members); } - public function jsonSerialize() - { + public function jsonSerialize(): object { return $this->doc; } } diff --git a/src/NewResourceObject.php b/src/NewResourceObject.php new file mode 100644 index 0000000..44364f1 --- /dev/null +++ b/src/NewResourceObject.php @@ -0,0 +1,20 @@ +data = null; } - - public function registerIn(array &$registry): void - { - } } diff --git a/src/PaginatedCollection.php b/src/PaginatedCollection.php index 8b8e0bd..ffc9050 100644 --- a/src/PaginatedCollection.php +++ b/src/PaginatedCollection.php @@ -1,39 +1,22 @@ -pagination = $pagination; - $this->collection = $collection; - } - - public function attachTo(object $o): void - { - $this->collection->attachTo($o); - $this->pagination->attachTo($o); +final class PaginatedCollection implements PrimaryData { + public function __construct(private readonly Pagination $pagination, private readonly Collection $collection) { } /** + * @param object $o * @internal - * @param array $registry */ - public function registerIn(array &$registry): void - { - $this->collection->registerIn($registry); + public function attachTo(object $o): void { + $this->collection->attachTo($o); + $this->pagination->attachTo($o); } } diff --git a/src/Pagination.php b/src/Pagination.php index aa4f9c3..dc2c642 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -1,4 +1,5 @@ links = $links; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { foreach ($this->links as $link) { $link->attachTo($o); } diff --git a/src/ResourceCollection.php b/src/ResourceCollection.php index 77ac09f..592dd21 100644 --- a/src/ResourceCollection.php +++ b/src/ResourceCollection.php @@ -1,34 +1,30 @@ -resources = $resources; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->data = []; foreach ($this->resources as $resource) { $resource->attachToCollection($o); } } - - public function registerIn(array &$registry): void - { - foreach ($this->resources as $resource) { - $resource->registerIn($registry); - } - } } diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php index 221e096..cf1cf88 100644 --- a/src/ResourceIdentifier.php +++ b/src/ResourceIdentifier.php @@ -1,50 +1,41 @@ -obj = (object) [ + $this->obj = (object)[ 'type' => $type, 'id' => $id, ]; - if ($meta) { + foreach ($metas as $meta) { $meta->attachTo($this->obj); } - $this->type = $type; - $this->id = $id; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->data = $this->obj; } - public function attachToCollection(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachToCollection(object $o): void { $o->data[] = $this->obj; } - - public function registerIn(array &$registry): void - { - $registry[compositeKey($this->type, $this->id)] = true; - } } diff --git a/src/ResourceIdentifierCollection.php b/src/ResourceIdentifierCollection.php index d13eb11..794789e 100644 --- a/src/ResourceIdentifierCollection.php +++ b/src/ResourceIdentifierCollection.php @@ -1,34 +1,30 @@ -identifiers = $identifiers; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->data = []; foreach ($this->identifiers as $identifier) { $identifier->attachToCollection($o); } } - - public function registerIn(array &$registry): void - { - foreach ($this->identifiers as $identifier) { - $identifier->registerIn($registry); - } - } } diff --git a/src/ResourceObject.php b/src/ResourceObject.php index de0a478..f65d575 100644 --- a/src/ResourceObject.php +++ b/src/ResourceObject.php @@ -1,81 +1,52 @@ -obj = (object) ['type' => $type, 'id' => $id]; - $fields = []; - foreach ($members as $member) { - if ($member instanceof Identifier) { - $member->registerIn($this->registry); - } - if ($member instanceof ResourceField) { - $name = $member->name(); - if (isset($fields[$name])) { - throw new \LogicException("Field '$name' already exists'"); - } - $fields[$name] = true; - } - $member->attachTo($this->obj); - } +final class ResourceObject extends BaseResource implements PrimaryData { + public function __construct(string $type, private readonly string $id, ResourceMember ...$members) { + parent::__construct($type, ...$members); + $this->obj->id = $id; $this->type = $type; - $this->id = $id; } - public function identifier(): ResourceIdentifier - { + public function identifier(): ResourceIdentifier { return new ResourceIdentifier($this->type, $this->id); } - public function key(): string - { + public function key(): string { return compositeKey($this->type, $this->id); } - public function registerIn(array &$registry): void - { - $registry = array_merge($registry, $this->registry); - } - - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $o->data = $this->obj; } - public function attachAsIncludedTo(object $o): void - { + /** + * @param object $o + */ + public function attachAsIncludedTo(object $o): void { $o->included[] = $this->obj; } - public function attachToCollection(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachToCollection(object $o): void { $o->data[] = $this->obj; } - public function __toString(): string - { + public function __toString(): string { return $this->key(); } } diff --git a/src/ToMany.php b/src/ToMany.php index 94b873b..afe4987 100644 --- a/src/ToMany.php +++ b/src/ToMany.php @@ -1,34 +1,37 @@ -validateFieldName($name); $this->name = $name; $this->members = $members; - $this->collection = $collection; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $rel = child(child($o, 'relationships'), $this->name); $rel->data = []; $this->collection->attachTo($rel); @@ -36,9 +39,4 @@ public function attachTo(object $o): void $member->attachTo($rel); } } - - public function registerIn(array &$registry): void - { - $this->collection->registerIn($registry); - } } diff --git a/src/ToNull.php b/src/ToNull.php index 0efaef0..cbaec93 100644 --- a/src/ToNull.php +++ b/src/ToNull.php @@ -1,4 +1,6 @@ -validateFieldName($name); $this->name = $name; $this->members = $members; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { $obj = combine(...$this->members); $obj->data = null; child($o, 'relationships')->{$this->name} = $obj; diff --git a/src/ToOne.php b/src/ToOne.php index c6d32da..eb86853 100644 --- a/src/ToOne.php +++ b/src/ToOne.php @@ -1,4 +1,6 @@ -validateFieldName($name); $this->name = $name; $this->obj = combine($identifier, ...$members); - $this->identifier = $identifier; } - public function attachTo(object $o): void - { + /** + * @param object $o + * @internal + */ + public function attachTo($o): void { child($o, 'relationships')->{$this->name} = $this->obj; } - - public function registerIn(array &$registry): void - { - $this->identifier->registerIn($registry); - } } diff --git a/src/functions.php b/src/functions.php index d7c89a4..5a08291 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,32 +1,30 @@ -attachTo($obj); } return $obj; } -function child(object $o, string $name): object -{ +function child($o, string $name): mixed { if (!isset($o->{$name})) { - $o->{$name} = (object) []; + $o->{$name} = (object)[]; } return $o->{$name}; } -function isValidName(string $name): bool -{ +function isValidName(string $name): bool { return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1; } -function compositeKey(string $type, string $id): string -{ +function compositeKey(string $type, string $id): string { return "{$type}:{$id}"; } diff --git a/test/BaseTestCase.php b/test/BaseTestCase.php index 2485e47..e649b2c 100644 --- a/test/BaseTestCase.php +++ b/test/BaseTestCase.php @@ -1,13 +1,13 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('Full linkage required for apples:1'); - $create_doc(); - } - - public function documentsWithoutFullLinkage(): array - { - $included = new Included(new ResourceObject('apples', '1')); - return [ - [ - function () use ($included) { - return new CompoundDocument(new NullData(), $included); - }, - ], - [ - function () use ($included) { - return new CompoundDocument(new ResourceCollection(), $included); - }, - ], - [ - function () use ($included) { - return new CompoundDocument(new ResourceIdentifier('oranges', '1'), $included); - }, - ], - [ - function () use ($included) { - return new CompoundDocument( - new ResourceIdentifierCollection( - new ResourceIdentifier('oranges', '1'), - new ResourceIdentifier('oranges', '1') - ), - $included - ); - }, - ], - [ - function () use ($included) { - return new CompoundDocument( - new ResourceCollection(new ResourceObject('oranges', '1'), new ResourceObject('oranges', '1')), - $included - ); - }, - ], - ]; - } - - public function testIncludedResourceMayBeIdentifiedByLinkageInPrimaryData() - { + public function testIncludedResourceMayBeIdentifiedByLinkageInPrimaryData() { $author = new ResourceObject('people', '9'); $article = new ResourceObject( 'articles', @@ -233,8 +175,7 @@ public function testIncludedResourceMayBeIdentifiedByLinkageInPrimaryData() $this->assertNotEmpty($doc); } - public function testIncludedResourceMayBeIdentifiedByAnotherLinkedResource() - { + public function testIncludedResourceMayBeIdentifiedByAnotherLinkedResource() { $writer = new ResourceObject('writers', '3', new Attribute('name', 'Eric Evans')); $book = new ResourceObject( 'books', @@ -253,11 +194,10 @@ public function testIncludedResourceMayBeIdentifiedByAnotherLinkedResource() /** * A compound document MUST NOT include more than one resource object for each type and id pair. - * @expectedException \LogicException - * @expectedExceptionMessage Resource apples:1 is already included */ - public function testCanNotBeManyIncludedResourcesWithEqualIdentifiers() - { + public function testCanNotBeManyIncludedResourcesWithEqualIdentifiers() { + $this->expectException('LogicException'); + $this->expectExceptionMessage('Resource apples:1 is already included'); $apple = new ResourceObject('apples', '1'); new CompoundDocument($apple->identifier(), new Included($apple, $apple)); } diff --git a/test/DataDocument/ManyResourceIdentifiersTest.php b/test/DataDocument/ManyResourceIdentifiersTest.php index eb12973..2d4728d 100644 --- a/test/DataDocument/ManyResourceIdentifiersTest.php +++ b/test/DataDocument/ManyResourceIdentifiersTest.php @@ -1,4 +1,6 @@ -assertEncodesTo( ' { @@ -26,8 +26,7 @@ public function testMinimalDocument() ); } - public function testExtendedDocument() - { + public function testExtendedDocument() { $this->assertEncodesTo( ' { diff --git a/test/DataDocument/ManyResourceObjectsTest.php b/test/DataDocument/ManyResourceObjectsTest.php index 3a2be82..a555e0c 100644 --- a/test/DataDocument/ManyResourceObjectsTest.php +++ b/test/DataDocument/ManyResourceObjectsTest.php @@ -1,20 +1,21 @@ -assertEncodesTo( ' { @@ -27,30 +28,28 @@ public function testMinimalDocument() ); } - public function testExtendedDocument() - { + public function testExtendedDocument() { $this->assertEncodesTo( ' { "data": [{ - "type": "apples", + "type": "people", "id": "1", "attributes": { - "color": "red", - "sort": "Fuji" + "name": "Martin Fowler" }, "meta": {"apple_meta": "foo"} },{ - "type": "apples", + "type": "people", "id": "2", "attributes": { - "color": "yellow", - "sort": "Gala" + "name": "Kent Beck" }, "meta": {"apple_meta": "foo"} }], "links": { - "self": "/apples" + "self": "/books/123/relationships/authors", + "related": "/books/123/authors" }, "jsonapi": { "version": "1.0" @@ -61,21 +60,20 @@ public function testExtendedDocument() new DataDocument( new ResourceCollection( new ResourceObject( - 'apples', + 'people', '1', - new Attribute('color', 'red'), - new Attribute('sort', 'Fuji'), + new Attribute('name', 'Martin Fowler'), new Meta('apple_meta', 'foo') ), new ResourceObject( - 'apples', + 'people', '2', - new Attribute('color', 'yellow'), - new Attribute('sort', 'Gala'), + new Attribute('name', 'Kent Beck'), new Meta('apple_meta', 'foo') ) ), - new SelfLink('/apples'), + new SelfLink('/books/123/relationships/authors'), + new RelatedLink('/books/123/authors'), new JsonApi(), new Meta('document_meta', 'bar') ) diff --git a/test/DataDocument/NullDataTest.php b/test/DataDocument/NullDataTest.php index 05746cd..4bb759d 100644 --- a/test/DataDocument/NullDataTest.php +++ b/test/DataDocument/NullDataTest.php @@ -1,4 +1,5 @@ assertEncodesTo( ' { @@ -26,8 +25,7 @@ public function testMinimalDocument() ); } - public function testExtendedDocument() - { + public function testExtendedDocument() { $this->assertEncodesTo( ' { diff --git a/test/DataDocument/SingleResourceIdentifierTest.php b/test/DataDocument/SingleResourceIdentifierTest.php index 92a91d5..3eea202 100644 --- a/test/DataDocument/SingleResourceIdentifierTest.php +++ b/test/DataDocument/SingleResourceIdentifierTest.php @@ -1,45 +1,49 @@ -assertEncodesTo( ' { "data": { - "type": "apples", + "type": "companies", "id": "1" } } ', new DataDocument( - new ResourceIdentifier('apples', '1') + new ResourceIdentifier('companies', '1') ) ); } - public function testExtendedDocument() - { + public function testExtendedDocument() { $this->assertEncodesTo( ' { "data": { - "type": "apples", + "type": "companies", "id": "1", - "meta": {"apple_meta": "foo"} + "meta": { + "apple_meta": "foo", + "bar": [42] + } }, "links": { - "self": "/apples/1" + "self": "/books/123/relationships/publisher", + "related": "/books/123/publisher" }, "jsonapi": { "version": "1.0" @@ -49,11 +53,13 @@ public function testExtendedDocument() ', new DataDocument( new ResourceIdentifier( - 'apples', + 'companies', '1', - new Meta('apple_meta', 'foo') + new Meta('apple_meta', 'foo'), + new Meta('bar', [42]) ), - new SelfLink('/apples/1'), + new SelfLink('/books/123/relationships/publisher'), + new RelatedLink('/books/123/publisher'), new JsonApi(), new Meta('document_meta', 'bar') ) diff --git a/test/DataDocument/SingleResourceObjectTest.php b/test/DataDocument/SingleResourceObjectTest.php index dfa25a1..0d19dde 100644 --- a/test/DataDocument/SingleResourceObjectTest.php +++ b/test/DataDocument/SingleResourceObjectTest.php @@ -1,4 +1,6 @@ -assertEncodesTo( ' { @@ -29,8 +29,7 @@ public function testMinimalDocument() ); } - public function testExtendedDocument() - { + public function testExtendedDocument() { $this->assertEncodesTo( ' { diff --git a/test/ErrorDocumentTest.php b/test/ErrorDocumentTest.php index 9a9131d..cb0bc4c 100644 --- a/test/ErrorDocumentTest.php +++ b/test/ErrorDocumentTest.php @@ -1,4 +1,5 @@ assertEncodesTo( ' { @@ -36,8 +35,7 @@ public function testMinimalExample() ); } - public function testExtensiveExample() - { + public function testExtensiveExample() { $this->assertEncodesTo( ' { @@ -82,8 +80,7 @@ public function testExtensiveExample() ); } - public function testMultipleErrors() - { + public function testMultipleErrors() { $this->assertEncodesTo( ' { diff --git a/test/ExamplesTest.php b/test/ExamplesTest.php index ba7a1cf..7dc5da4 100644 --- a/test/ExamplesTest.php +++ b/test/ExamplesTest.php @@ -1,23 +1,21 @@ assertJson(`php $file`); } - public function examples() - { + public function examples() { return [ [__DIR__.'/../examples/compound_doc.php'], [__DIR__.'/../examples/simple_doc.php'], diff --git a/test/JsonApiTest.php b/test/JsonApiTest.php index 49c5982..8a5ce91 100644 --- a/test/JsonApiTest.php +++ b/test/JsonApiTest.php @@ -1,4 +1,6 @@ -assertEncodesTo( ' { diff --git a/test/LinkObjectTest.php b/test/LinkObjectTest.php index a69443c..00a9a78 100644 --- a/test/LinkObjectTest.php +++ b/test/LinkObjectTest.php @@ -1,4 +1,6 @@ -assertEncodesTo( '{ "data": {"type": "apples", "id": "1"}, diff --git a/test/MetaDocumentTest.php b/test/MetaDocumentTest.php index 7893dae..1837f3e 100644 --- a/test/MetaDocumentTest.php +++ b/test/MetaDocumentTest.php @@ -1,4 +1,5 @@ assertEncodesTo( ' { @@ -29,8 +28,7 @@ public function testMetaDocument() /** * A meta document may contain jsonapi member */ - public function testMetaDocumentWithExtraMembers() - { + public function testMetaDocumentWithExtraMembers() { $this->assertEncodesTo( ' { diff --git a/test/MetaTest.php b/test/MetaTest.php index 67d3097..c8d8d93 100644 --- a/test/MetaTest.php +++ b/test/MetaTest.php @@ -1,14 +1,14 @@ -expectException(\DomainException::class); $this->expectExceptionMessage("Invalid character in a member name 'invalid:name'"); new Meta('invalid:name', '1'); diff --git a/test/NewResourceObjectTest.php b/test/NewResourceObjectTest.php new file mode 100644 index 0000000..a2fc94e --- /dev/null +++ b/test/NewResourceObjectTest.php @@ -0,0 +1,201 @@ +assertEncodesTo( + ' + { + "data": { + "type": "apples", + "attributes": { + "title": "Rails is Omakase" + }, + "meta": {"foo": "bar"}, + "relationships": { + "author": { + "meta": {"foo": "bar"}, + "data": null + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'apples', + new Meta('foo', 'bar'), + new Attribute('title', 'Rails is Omakase'), + new ToNull( + 'author', + new Meta('foo', 'bar') + ) + ) + ) + ); + } + + public function testRelationshipWithSingleIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": {"type": "apples", "id": "1"} + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToOne('content', new ResourceIdentifier('apples', '1')) + ) + ) + ); + } + + public function testRelationshipWithMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": [{ + "type": "apples", + "id": "1" + },{ + "type": "pears", + "id": "2" + }] + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToMany( + 'content', + new ResourceIdentifierCollection( + new ResourceIdentifier('apples', '1'), + new ResourceIdentifier('pears', '2') + ) + ) + ) + ) + ); + } + + public function testRelationshipWithEmptyMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": [] + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToMany('content', new ResourceIdentifierCollection()) + ) + ) + ); + } + + public function testRelationshipWithNoData() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "empty": { + "links": { + "related": "/foo" + } + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new EmptyRelationship('empty', new RelatedLink('/foo')) + ) + ) + ); + + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "empty": { + "links": { + "related": "/foo", + "self": "/bar" + }, + "meta": { + "foo": "bar" + } + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new EmptyRelationship('empty', new RelatedLink('/foo'), new SelfLink('/bar'), new Meta('foo', 'bar')) + ) + ) + ); + } + + public function testResourceFieldsMustBeUnique() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage("Field 'foo' already exists"); + new NewResourceObject( + 'apples', + new Attribute('foo', 'bar'), + new ToOne('foo', new ResourceIdentifier('apples', '1')) + ); + } + + public function testNameValidation() { + $this->expectException(\DomainException::class); + new NewResourceObject('invalid:id'); + } +} diff --git a/test/PaginationTest.php b/test/PaginationTest.php index 17ee668..a9307d8 100644 --- a/test/PaginationTest.php +++ b/test/PaginationTest.php @@ -1,4 +1,6 @@ -assertEncodesTo( ' { @@ -33,7 +34,8 @@ public function testPaginatedResourceCollection() "first": "http://example.com/fruits?page=first", "last": "http://example.com/fruits?page=last", "prev": "http://example.com/fruits?page=3", - "next": "http://example.com/fruits?page=5" + "next": "http://example.com/fruits?page=5", + "self": "http://example.com/fruits?page=4" } } ', @@ -49,13 +51,13 @@ public function testPaginatedResourceCollection() new ResourceObject('apples', '1'), new ResourceObject('apples', '2') ) - ) + ), + new SelfLink('http://example.com/fruits?page=4') ) ); } - public function testPaginatedResourceIdentifierCollection() - { + public function testPaginatedResourceIdentifierCollection() { $this->assertEncodesTo( ' { diff --git a/test/ResourceIdentifierTest.php b/test/ResourceIdentifierTest.php index 5dcd959..eb479ee 100644 --- a/test/ResourceIdentifierTest.php +++ b/test/ResourceIdentifierTest.php @@ -1,14 +1,13 @@ expectException(\DomainException::class); new ResourceIdentifier('invalid:id', 'foo'); } diff --git a/test/ResourceObjectTest.php b/test/ResourceObjectTest.php index 37e845f..b565d7d 100644 --- a/test/ResourceObjectTest.php +++ b/test/ResourceObjectTest.php @@ -1,9 +1,12 @@ -assertEncodesTo( ' { @@ -62,8 +63,7 @@ public function testFullFledgedResourceObject() ); } - public function testRelationshipWithSingleIdLinkage() - { + public function testRelationshipWithSingleIdLinkage() { $this->assertEncodesTo( ' { @@ -88,8 +88,7 @@ public function testRelationshipWithSingleIdLinkage() ); } - public function testRelationshipWithMultiIdLinkage() - { + public function testRelationshipWithMultiIdLinkage() { $this->assertEncodesTo( ' { @@ -126,8 +125,7 @@ public function testRelationshipWithMultiIdLinkage() ); } - public function testRelationshipWithEmptyMultiIdLinkage() - { + public function testRelationshipWithEmptyMultiIdLinkage() { $this->assertEncodesTo( ' { @@ -152,29 +150,81 @@ public function testRelationshipWithEmptyMultiIdLinkage() ); } - public function testCanNotCreateIdAttribute() - { + public function testRelationshipWithNoData() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "empty": { + "links": { + "related": "/foo" + } + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new EmptyRelationship('empty', new RelatedLink('/foo')) + ) + ) + ); + + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "empty": { + "links": { + "related": "/foo", + "self": "/bar" + }, + "meta": { + "foo": "bar" + } + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new EmptyRelationship('empty', new RelatedLink('/foo'), new SelfLink('/bar'), new Meta('foo', 'bar')) + ) + ) + ); + } + + public function testCanNotCreateIdAttribute() { $this->expectException(\DomainException::class); $this->expectExceptionMessage("Can not use 'id' as a resource field"); new Attribute('id', 'foo'); } - public function testCanNotCreateTypeAttribute() - { + public function testCanNotCreateTypeAttribute() { $this->expectException(\DomainException::class); $this->expectExceptionMessage("Can not use 'type' as a resource field"); new Attribute('type', 'foo'); } - public function testCanNotCreateIdRelationship() - { + public function testCanNotCreateIdRelationship() { $this->expectException(\DomainException::class); $this->expectExceptionMessage("Can not use 'id' as a resource field"); new ToOne('id', new ResourceIdentifier('apples', '1')); } - public function testCanNotCreateTypeRelationship() - { + public function testCanNotCreateTypeRelationship() { $this->expectException(\DomainException::class); $this->expectExceptionMessage("Can not use 'type' as a resource field"); new ToOne('type', new ResourceIdentifier('apples', '1')); @@ -184,8 +234,7 @@ public function testCanNotCreateTypeRelationship() * @dataProvider invalidCharacters * @param string $invalid_char */ - public function testAttributeMustOnlyHaveAllowedCharacters(string $invalid_char) - { + public function testAttributeMustOnlyHaveAllowedCharacters(string $invalid_char) { $this->expectException(\DomainException::class); $this->expectExceptionMessage('Invalid character in a member name'); new Attribute("foo{$invalid_char}bar", 'plus can not be used'); @@ -195,15 +244,13 @@ public function testAttributeMustOnlyHaveAllowedCharacters(string $invalid_char) * @dataProvider invalidCharacters * @param string $invalid_char */ - public function testRelationshipMustOnlyHaveAllowedCharacters(string $invalid_char) - { + public function testRelationshipMustOnlyHaveAllowedCharacters(string $invalid_char) { $this->expectException(\DomainException::class); $this->expectExceptionMessage('Invalid character in a member name'); new ToNull("foo{$invalid_char}bar"); } - public function invalidCharacters() - { + public function invalidCharacters() { return [ ['+'], ['!'], @@ -213,8 +260,7 @@ public function invalidCharacters() ]; } - public function testResourceFieldsMustBeUnique() - { + public function testResourceFieldsMustBeUnique() { $this->expectException(\LogicException::class); $this->expectExceptionMessage("Field 'foo' already exists"); new ResourceObject( @@ -225,8 +271,7 @@ public function testResourceFieldsMustBeUnique() ); } - public function testNameValidation() - { + public function testNameValidation() { $this->expectException(\DomainException::class); new ResourceObject('invalid:id', 'foo'); } diff --git a/test/benchmarks/compound10k.php b/test/benchmarks/compound10k.php index c1a0ae2..44664fd 100644 --- a/test/benchmarks/compound10k.php +++ b/test/benchmarks/compound10k.php @@ -1,4 +1,6 @@ -