diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index c68ff75..406a498 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - php_version: ['8.1', '8.2', '8.3'] + php_version: ['8.1', '8.2', '8.3', '8.4'] laravel_version: ['^9.0', '^10.0', '^11.0'] exclude: - php_version: '8.1' diff --git a/CHANGELOG-1.4.md b/CHANGELOG-1.4.md index bcc63a2..4ffc537 100644 --- a/CHANGELOG-1.4.md +++ b/CHANGELOG-1.4.md @@ -1,10 +1,20 @@ Release note ============ -# v1.4.4 +# v1.4.6 +### Change +- Support PHP 8.4 +### Fixed +- Fix `applyWhen` behavior + +# v1.4.5 ### Change - `applyWhen` condition can be a closure - CI: test all supported laravel version +# v1.4.4 +### Fixes +- typo on ToResponse : change Content-type to Content-Type + # v1.4.3 ### Added - Add support for Laravel 11 diff --git a/phpunit.php8.4.xml.dist b/phpunit.php8.4.xml.dist new file mode 100644 index 0000000..8ef7a48 --- /dev/null +++ b/phpunit.php8.4.xml.dist @@ -0,0 +1,26 @@ + + + + + tests/Unit + + + tests/Feature + + + + + + + + + src + + + diff --git a/src/Asserts/AssertJsonApiResource.php b/src/Asserts/AssertJsonApiResource.php new file mode 100644 index 0000000..6bc1182 --- /dev/null +++ b/src/Asserts/AssertJsonApiResource.php @@ -0,0 +1,55 @@ + $class + */ + public function __construct(protected string $class) + { + } + + public function assert(): void + { + $this->itCanGenerateSchema(); + $this->allAttributesAreLazySet(); + } + + private function itCanGenerateSchema(): void + { + try { + $this->class::schema(); + } catch (\Throwable $throwable) { + throw new FailGenerateSchema($this->class, $throwable); + } + } + + private function allAttributesAreLazySet(): void + { + $reflection = new ReflectionClass($this->class); + $instance = $reflection->newInstanceWithoutConstructor(); + $instance->resource = new FakeModel; + + $method = $reflection->getMethod('toAttributes'); + $method->setAccessible(true); + /** @var iterable $attributes */ + $attributes = $method->invoke($instance, new Request()); + $attributes = Values::mergeValues($attributes); + + foreach ($attributes as $key => $attribute) { + if (!($attribute instanceof \Closure || $attribute instanceof MissingValue || $attribute instanceof Value)) { + throw new EagerSetAttribute($this->class, $key); + } + } + } +} diff --git a/src/Asserts/EagerSetAttribute.php b/src/Asserts/EagerSetAttribute.php new file mode 100644 index 0000000..b3b854d --- /dev/null +++ b/src/Asserts/EagerSetAttribute.php @@ -0,0 +1,13 @@ +whenIncluded ??= true; @@ -83,7 +83,7 @@ public function whenIncluded(bool $whenIncluded = null): static * * @return static */ - public function whenLoaded(string $relation = null): self + public function whenLoaded(null|string $relation = null): self { return $this->when(fn( Request $request, @@ -100,7 +100,7 @@ public function whenLoaded(string $relation = null): self * * @return static */ - public function whenPivotLoaded(string $table, string $accessor = null): self + public function whenPivotLoaded(string $table, null|string $accessor = null): self { return $this->when(fn( Request $request, diff --git a/src/Descriptors/Values/ValueRaw.php b/src/Descriptors/Values/ValueRaw.php deleted file mode 100644 index db0aa72..0000000 --- a/src/Descriptors/Values/ValueRaw.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class ValueRaw extends Value -{ - public function __construct( - null|string|Closure $attribute, - protected mixed $raw - ) { - parent::__construct($attribute); - } - - public function value(mixed $of): mixed - { - return $this->raw; - } -} diff --git a/src/Resources/Concerns/ConditionallyLoadsAttributes.php b/src/Resources/Concerns/ConditionallyLoadsAttributes.php index a6cd0d7..feb1d1a 100644 --- a/src/Resources/Concerns/ConditionallyLoadsAttributes.php +++ b/src/Resources/Concerns/ConditionallyLoadsAttributes.php @@ -4,7 +4,7 @@ use Ark4ne\JsonApi\Descriptors\Describer; use Ark4ne\JsonApi\Descriptors\Relations\RelationRaw; -use Ark4ne\JsonApi\Descriptors\Values\ValueRaw; +use Ark4ne\JsonApi\Descriptors\Values\ValueMixed; use Ark4ne\JsonApi\Resources\Relationship; use Ark4ne\JsonApi\Support\Fields; use Ark4ne\JsonApi\Support\Includes; @@ -57,13 +57,13 @@ protected function whenIncluded(Request $request, string $type, mixed $value) */ protected function applyWhen(bool|Closure $condition, iterable $data): MergeValue { - return new MergeValue(collect($data)->map(function ($raw, $key) use ($condition) { + return new MergeValue(collect($data)->map(function ($raw) use ($condition) { if ($raw instanceof Describer) { $value = $raw; } elseif ($raw instanceof Relationship) { $value = RelationRaw::fromRelationship($raw); } else { - $value = new ValueRaw($key, $raw); + $value = new ValueMixed(is_callable($raw) ? $raw : static fn () => $raw); } return $value->when(fn () => value($condition)); diff --git a/src/Resources/Concerns/Relationships.php b/src/Resources/Concerns/Relationships.php index abd97f1..131f9c0 100644 --- a/src/Resources/Concerns/Relationships.php +++ b/src/Resources/Concerns/Relationships.php @@ -60,7 +60,7 @@ private function requestedRelationshipsLoadFromSchema(Request $request, Skeleton if ($load && Includes::include($request, $name)) { $include = Includes::through($name, fn() => $this->requestedRelationshipsLoadFromSchema($request, $schema->relationships[$name])); - $apply = static function ($load, string $pre = null) use (&$loads, &$apply) { + $apply = static function ($load, null|string $pre = null) use (&$loads, &$apply) { foreach ((array)$load as $key => $value) { if (is_string($value)) { $loads["$pre.$value"] = []; diff --git a/src/Resources/Concerns/Schema.php b/src/Resources/Concerns/Schema.php index 4e593e4..8499bd8 100644 --- a/src/Resources/Concerns/Schema.php +++ b/src/Resources/Concerns/Schema.php @@ -21,7 +21,7 @@ trait Schema */ private static array $schemas = []; - public static function schema(Request $request = null): Skeleton + public static function schema(null|Request $request = null): Skeleton { if (isset(self::$schemas[static::class])) { return self::$schemas[static::class]; diff --git a/src/Resources/Concerns/SchemaCollection.php b/src/Resources/Concerns/SchemaCollection.php index dedfbb3..39f314d 100644 --- a/src/Resources/Concerns/SchemaCollection.php +++ b/src/Resources/Concerns/SchemaCollection.php @@ -8,7 +8,7 @@ trait SchemaCollection { - public static function schema(Request $request = null): Skeleton + public static function schema(null|Request $request = null): Skeleton { return self::new()->collects::schema($request); } diff --git a/src/Resources/Relationship.php b/src/Resources/Relationship.php index 6083e38..c8a64c9 100644 --- a/src/Resources/Relationship.php +++ b/src/Resources/Relationship.php @@ -100,7 +100,7 @@ public function asCollection(): self * @param bool|null $whenIncluded * @return $this */ - public function whenIncluded(bool $whenIncluded = null): static + public function whenIncluded(null|bool $whenIncluded = null): static { if ($whenIncluded === null) { $this->whenIncluded ??= true; diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 3aae71d..f83b64e 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -107,7 +107,7 @@ private static function flattenDot(array $array, string $prepend, string $saveKe * @param string|null $saveKey * @return array */ - public static function undot(array $array, string $saveKey = null): array + public static function undot(array $array, null|string $saveKey = null): array { $results = []; @@ -129,7 +129,7 @@ public static function undot(array $array, string $saveKey = null): array * @param string|null $saveKey * @return mixed */ - public static function apply(array &$array, string $path, mixed $value, string $saveKey = null): mixed + public static function apply(array &$array, string $path, mixed $value, null|string $saveKey = null): mixed { $keys = explode('.', $path); diff --git a/src/Support/Fields.php b/src/Support/Fields.php index 0020b35..c17cf59 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -37,7 +37,7 @@ public static function through(string $type, callable $callable): mixed * * @return string[]|null */ - public static function get(Request $request, string $type = null): ?array + public static function get(Request $request, null|string $type = null): ?array { $type ??= self::$current; @@ -57,7 +57,7 @@ public static function get(Request $request, string $type = null): ?array * * @return bool */ - public static function has(Request $request, string $field, string $type =null): bool { + public static function has(Request $request, string $field, null|string $type =null): bool { $type ??= self::$current; if ($type === null) { diff --git a/tests/Feature/SchemaTest.php b/tests/Feature/SchemaTest.php index 8a3b147..4978272 100644 --- a/tests/Feature/SchemaTest.php +++ b/tests/Feature/SchemaTest.php @@ -13,7 +13,14 @@ class SchemaTest extends FeatureTestCase { public function testSchema() { - $user = new Skeleton(UserResource::class, 'user', ['name', 'email', 'only-with-fields']); + $user = new Skeleton(UserResource::class, 'user', [ + 'name', + 'email', + 'only-with-fields', + 'with-apply-conditional-raw', + 'with-apply-conditional-closure', + 'with-apply-conditional-value', + ]); $post = new Skeleton(PostResource::class, 'post', ['state', 'title', 'content']); $comment = new Skeleton(CommentResource::class, 'comment', ['content']); diff --git a/tests/Feature/User/ResourceTest.php b/tests/Feature/User/ResourceTest.php index 20c67c3..c1ab1c5 100644 --- a/tests/Feature/User/ResourceTest.php +++ b/tests/Feature/User/ResourceTest.php @@ -164,7 +164,16 @@ private function getJsonResult(User $user, ?array $attributes = null, ?array $re 'attributes' => array_filter(array_intersect_key([ 'name' => $user->name, 'email' => $user->email, - ], array_fill_keys($attributes ?? ['name', 'email'], true))), + "with-apply-conditional-closure" => "huge-data-set", + "with-apply-conditional-raw" => "huge-data-set", + "with-apply-conditional-value" => "huge-data-set" + ], array_fill_keys($attributes ?? [ + 'name', + 'email', + "with-apply-conditional-closure", + "with-apply-conditional-raw", + "with-apply-conditional-value", + ], true))), 'relationships' => [ 'main-post' => [], 'posts' => array_filter([ diff --git a/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php b/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php index d9b4dcb..4af60e2 100644 --- a/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php +++ b/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php @@ -7,6 +7,7 @@ use Ark4ne\JsonApi\Descriptors\Values\ValueMixed; use Ark4ne\JsonApi\Descriptors\Values\ValueRaw; use Ark4ne\JsonApi\Resources\Concerns\ConditionallyLoadsAttributes; +use Ark4ne\JsonApi\Resources\JsonApiResource; use Ark4ne\JsonApi\Resources\Relationship; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -75,16 +76,16 @@ public function testApplyWhen() 'missing.2' => 123, ]); $this->assertEquals(new MergeValue([ - 'missing.1' => (new ValueRaw('missing.1', 'abc'))->when(fn () => false), - 'missing.2' => (new ValueRaw('missing.2', 123))->when(fn () => false), + 'missing.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => false), + 'missing.2' => (new ValueMixed(fn() => 123))->when(fn() => false), ]), $actual); $actual = Reflect::invoke($stub, 'applyWhen', true, [ 'present.1' => 'abc', 'present.2' => 123, ]); $this->assertEquals(new MergeValue([ - 'present.1' => (new ValueRaw('present.1', 'abc'))->when(fn () => true), - 'present.2' => (new ValueRaw('present.2', 123))->when(fn () => true), + 'present.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => true), + 'present.2' => (new ValueMixed(fn() => 123))->when(fn() => true), ]), $actual); $actual = Reflect::invoke($stub, 'applyWhen', true, [ 'present.1' => (new ValueMixed(fn() => 'abc')), @@ -94,11 +95,11 @@ public function testApplyWhen() 'present.5' => (new Relationship(UserResource::class, fn() => null)), ]); $this->assertEquals(new MergeValue([ - 'present.1' => (new ValueMixed(fn() => 'abc'))->when(fn () => true), - 'present.2' => (new ValueMixed(fn() => 123))->when(fn () => true), - 'present.3' => (new RelationOne('present', fn() => 'abc'))->when(fn () => true), - 'present.4' => (new RelationOne('present', fn() => 123))->when(fn () => true), - 'present.5' => RelationRaw::fromRelationship(new Relationship(UserResource::class, fn() => null))->when(fn () => true), + 'present.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => true), + 'present.2' => (new ValueMixed(fn() => 123))->when(fn() => true), + 'present.3' => (new RelationOne('present', fn() => 'abc'))->when(fn() => true), + 'present.4' => (new RelationOne('present', fn() => 123))->when(fn() => true), + 'present.5' => RelationRaw::fromRelationship(new Relationship(UserResource::class, fn() => null))->when(fn() => true), ]), $actual); $actual = Reflect::invoke($stub, 'applyWhen', false, [ 'missing.1' => (new ValueMixed(fn() => 'abc')), @@ -108,11 +109,11 @@ public function testApplyWhen() 'missing.5' => (new Relationship(UserResource::class, fn() => null)), ]); $this->assertEquals(new MergeValue([ - 'missing.1' => (new ValueMixed(fn() => 'abc'))->when(fn () => false), - 'missing.2' => (new ValueMixed(fn() => 123))->when(fn () => false), - 'missing.3' => (new RelationOne('present', fn() => 'abc'))->when(fn () => false), - 'missing.4' => (new RelationOne('present', fn() => 123))->when(fn () => false), - 'missing.5' => RelationRaw::fromRelationship(new Relationship(UserResource::class, fn() => null))->when(fn () => false), + 'missing.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => false), + 'missing.2' => (new ValueMixed(fn() => 123))->when(fn() => false), + 'missing.3' => (new RelationOne('present', fn() => 'abc'))->when(fn() => false), + 'missing.4' => (new RelationOne('present', fn() => 123))->when(fn() => false), + 'missing.5' => RelationRaw::fromRelationship(new Relationship(UserResource::class, fn() => null))->when(fn() => false), ]), $actual); } diff --git a/tests/app/Http/Resources/UserResource.php b/tests/app/Http/Resources/UserResource.php index 47e0d66..47ae055 100644 --- a/tests/app/Http/Resources/UserResource.php +++ b/tests/app/Http/Resources/UserResource.php @@ -25,6 +25,11 @@ protected function toAttributes(Request $request): iterable 'name' => $this->resource->name, 'email' => $this->resource->email, 'only-with-fields' => $this->string(fn() => 'huge-data-set')->whenInFields(), + $this->applyWhen(fn () => true, [ + 'with-apply-conditional-raw' => 'huge-data-set', + 'with-apply-conditional-closure' => fn () => 'huge-data-set', + 'with-apply-conditional-value' => $this->string(fn () => 'huge-data-set'), + ]), ]; }