diff --git a/composer.json b/composer.json index 2677eb3..7192f23 100644 --- a/composer.json +++ b/composer.json @@ -44,5 +44,5 @@ ] } }, - "minimum-stability": "dev" + "minimum-stability": "stable" } diff --git a/readme.md b/readme.md index 06e0b80..438ddb7 100644 --- a/readme.md +++ b/readme.md @@ -263,7 +263,7 @@ protected function toRelationships(Request $request): array `toRelationships` must returns an array, keyed by string, of `JsonApiResource` or `JsonApiCollection`. -#### Laravel conditional relationships +#### Laravel conditional relationships _**@see** [laravel: eloquent-conditional-relationships](https://laravel.com/docs/9.x/eloquent-resources#conditional-relationships)_ Support laravel conditional relationships. @@ -331,6 +331,7 @@ protected function toRelationships(Request $request): array } ``` + ### toLinks _**@see** [{json:api}: resource-linkage](https://jsonapi.org/format/#document-resource-object-links)_ diff --git a/src/Descriptors/Describer.php b/src/Descriptors/Describer.php index 4d06905..4f2100f 100644 --- a/src/Descriptors/Describer.php +++ b/src/Descriptors/Describer.php @@ -84,17 +84,17 @@ public function whenFilled(): static /** * @param \Illuminate\Http\Request $request * @param T $model - * @param string $field + * @param string $attribute * * @return mixed */ - public function valueFor(Request $request, mixed $model, string $field): mixed + public function valueFor(Request $request, mixed $model, string $attribute): mixed { - if (!$this->check($request, $model, $field)) { + if (!$this->check($request, $model, $attribute)) { return new MissingValue(); } - return $this->resolveFor($request, $model, $field); + return $this->resolveFor($request, $model, $attribute); } /** @@ -134,11 +134,11 @@ private function retrieveValue(mixed $model, string $attribute): mixed /** * @param \Illuminate\Http\Request $request * @param T $model - * @param string $field + * @param string $attribute * * @return mixed */ - abstract protected function resolveFor(Request $request, mixed $model, string $field): mixed; + abstract protected function resolveFor(Request $request, mixed $model, string $attribute): mixed; /** * @return string|Closure|null diff --git a/src/Descriptors/Relations/Relation.php b/src/Descriptors/Relations/Relation.php index 46a3f83..dc2f5b5 100644 --- a/src/Descriptors/Relations/Relation.php +++ b/src/Descriptors/Relations/Relation.php @@ -4,6 +4,7 @@ use Ark4ne\JsonApi\Descriptors\Describer; use Ark4ne\JsonApi\Resources\Relationship; +use Ark4ne\JsonApi\Support\Includes; use Ark4ne\JsonApi\Traits\HasRelationLoad; use Closure; use Illuminate\Database\Eloquent\Model; @@ -25,12 +26,13 @@ abstract class Relation extends Describer /** * @param class-string<\Ark4ne\JsonApi\Resources\JsonApiResource|\Ark4ne\JsonApi\Resources\JsonApiCollection> $related - * @param string|\Closure|null $relation + * @param string|\Closure|null $relation */ public function __construct( - protected string $related, + protected string $related, protected null|string|Closure $relation - ) { + ) + { } /** @@ -63,7 +65,7 @@ public function meta(Closure $meta): static /** * @param bool|null $whenIncluded - * @return $this + * @return static */ public function whenIncluded(null|bool $whenIncluded = null): static { @@ -73,39 +75,42 @@ public function whenIncluded(null|bool $whenIncluded = null): static $this->whenIncluded = $whenIncluded; } - return $this; + return $this->when(fn( + Request $request, + Model $model, + string $attribute + ): bool => !$this->whenIncluded || Includes::include($request, $attribute)); } /** - * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes::whenLoaded - * * @param string|null $relation * * @return static + * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes::whenLoaded */ - public function whenLoaded(null|string $relation = null): self + public function whenLoaded(null|string $relation = null): static { return $this->when(fn( Request $request, - Model $model, - string $attribute + Model $model, + string $attribute ): bool => $model->relationLoaded($relation ?? (is_string($this->relation) ? $this->relation : $attribute))); } /** - * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes::whenPivotLoadedAs - * - * @param string $table + * @param string $table * @param string|null $accessor * * @return static + * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes::whenPivotLoadedAs + * */ - public function whenPivotLoaded(string $table, null|string $accessor = null): self + public function whenPivotLoaded(string $table, null|string $accessor = null): static { return $this->when(fn( Request $request, - Model $model, - string $attribute + Model $model, + string $attribute ): bool => ($pivot = $model->{$accessor ?? (is_string($this->relation) ? $this->relation : $attribute)}) && ( $pivot instanceof $table || @@ -114,21 +119,21 @@ public function whenPivotLoaded(string $table, null|string $accessor = null): se ); } - public function resolveFor(Request $request, mixed $model, string $field): Relationship + public function resolveFor(Request $request, mixed $model, string $attribute): Relationship { $retriever = $this->retriever(); if ($retriever instanceof Closure) { - $value = static fn() => $retriever($model, $field); + $value = static fn() => $retriever($model, $attribute); } else { $value = static fn() => match (true) { - $model instanceof Model => $model->getRelationValue($retriever ?? $field), - Arr::accessible($model) => $model[$retriever ?? $field], - default => $model->{$retriever ?? $field} + $model instanceof Model => $model->getRelationValue($retriever ?? $attribute), + Arr::accessible($model) => $model[$retriever ?? $attribute], + default => $model->{$retriever ?? $attribute} }; } - $relation = $this->value(fn() => $this->check($request, $model, $field) ? $value() : new MissingValue()); + $relation = $this->value(fn() => $this->check($request, $model, $attribute) ? $value() : new MissingValue()); if ($this->whenIncluded !== null) { $relation->whenIncluded($this->whenIncluded); @@ -139,14 +144,14 @@ public function resolveFor(Request $request, mixed $model, string $field): Relat /** * @param \Illuminate\Http\Request $request - * @param T $model - * @param string $field + * @param T $model + * @param string $attribute * * @return mixed */ - public function valueFor(Request $request, mixed $model, string $field): mixed + public function valueFor(Request $request, mixed $model, string $attribute): mixed { - return $this->resolveFor($request, $model, $field); + return $this->resolveFor($request, $model, $attribute); } abstract protected function value(Closure $value): Relationship; diff --git a/src/Descriptors/Relations/RelationRaw.php b/src/Descriptors/Relations/RelationRaw.php index a0666fa..3586f33 100644 --- a/src/Descriptors/Relations/RelationRaw.php +++ b/src/Descriptors/Relations/RelationRaw.php @@ -22,6 +22,6 @@ protected function value(Closure $value): Relationship public static function fromRelationship(Relationship $relationship): self { - return new self($relationship->getResource(), fn() => $relationship); + return new self($relationship->getResource(), static fn() => $relationship); } } diff --git a/src/Resources/Concerns/ConditionallyLoadsAttributes.php b/src/Resources/Concerns/ConditionallyLoadsAttributes.php index feb1d1a..78f1cbf 100644 --- a/src/Resources/Concerns/ConditionallyLoadsAttributes.php +++ b/src/Resources/Concerns/ConditionallyLoadsAttributes.php @@ -66,7 +66,7 @@ protected function applyWhen(bool|Closure $condition, iterable $data): MergeValu $value = new ValueMixed(is_callable($raw) ? $raw : static fn () => $raw); } - return $value->when(fn () => value($condition)); + return $value->when(static fn () => value($condition)); })); } diff --git a/tests/Feature/Comment/CollectionTest.php b/tests/Feature/Comment/CollectionTest.php index d486ce8..8ec8a1e 100644 --- a/tests/Feature/Comment/CollectionTest.php +++ b/tests/Feature/Comment/CollectionTest.php @@ -3,6 +3,7 @@ namespace Test\Feature\Comment; use DateTimeInterface; +use Illuminate\Foundation\Application; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Test\app\Http\Resources\PostResource; @@ -82,6 +83,8 @@ private function getJsonResult(Collection $comments, ?array $attributes = null, )) ->reduce(fn(Collection $all, Collection $value) => $all->merge($value), collect()); + $isLaravel12 = ((int)explode('.', Application::VERSION, 2)[0]) >= 12; + return collect(array_filter([ 'data' => $data, 'included' => $include->uniqueStrict()->values()->all(), @@ -99,21 +102,25 @@ private function getJsonResult(Collection $comments, ?array $attributes = null, [ 'active' => false, 'label' => "« Previous", + ...($isLaravel12 ? ['page' => null] : []), 'url' => null, ], [ 'active' => true, 'label' => '1', + ...($isLaravel12 ? ['page' => 1] : []), 'url' => "http://localhost/comment?page=1", ], ...(array_map(static fn($value) => [ 'active' => false, 'label' => (string)$value, + ...($isLaravel12 ? ['page' => $value] : []), 'url' => "http://localhost/comment?page=$value", ], range(2, 10))), [ 'active' => false, 'label' => "Next »", + ...($isLaravel12 ? ['page' => 2] : []), 'url' => "http://localhost/comment?page=2", ], ], diff --git a/tests/Feature/SchemaTest.php b/tests/Feature/SchemaTest.php index 3a9a303..357fe4d 100644 --- a/tests/Feature/SchemaTest.php +++ b/tests/Feature/SchemaTest.php @@ -41,7 +41,7 @@ public function testSchema() $user->loads['main-post'] = 'post'; $user->loads['posts'] = 'posts'; $user->loads['comments'] = [ - 'comments' => fn(Builder $q) => $q->where('content', 'like', '%e%') + 'comments' => UserResource::schema()->loads['comments']['comments'] ]; $post->relationships['user'] = $user; diff --git a/tests/Unit/Descriptors/ValueTest.php b/tests/Unit/Descriptors/ValueTest.php index 260843d..1ea4e02 100644 --- a/tests/Unit/Descriptors/ValueTest.php +++ b/tests/Unit/Descriptors/ValueTest.php @@ -91,7 +91,7 @@ public static function modelsValues() * @dataProvider values */ #[DataProvider('values')] - public function testConvertValue($class, $value, $excepted) + public function testConvertValue($class, $value, $excepted, $_ignored) { /** @var \Ark4ne\JsonApi\Descriptors\Values\Value $v */ $v = new $class(null); @@ -102,7 +102,7 @@ public function testConvertValue($class, $value, $excepted) * @dataProvider modelsValues */ #[DataProvider('modelsValues')] - public function testValueFor($model, $class, $value, $excepted) + public function testValueFor($model, $class, $value, $excepted, $_ignored) { data_set($model, 'attr', $value); @@ -118,7 +118,7 @@ public function testValueFor($model, $class, $value, $excepted) * @dataProvider modelsValues */ #[DataProvider('modelsValues')] - public function testValueForWithNull($model, $class, $value, $excepted) + public function testValueForWithNull($model, $class, $value, $excepted, $_ignored) { data_set($model, 'attr', null); diff --git a/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php b/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php index 617aeaa..6961f00 100644 --- a/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php +++ b/tests/Unit/Resources/Concerns/ConditionallyLoadsAttributesTest.php @@ -78,46 +78,49 @@ public function testApplyWhen() 'missing.1' => 'abc', 'missing.2' => 123, ]); - $this->assertEquals(new MergeValue([ - 'missing.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => false), - 'missing.2' => (new ValueMixed(fn() => 123))->when(fn() => false), - ]), $actual); + $this->assertInstanceOf(MergeValue::class, $actual); + $this->assertInstanceOf(ValueMixed::class, $actual->data['missing.1']); + $this->assertInstanceOf(ValueMixed::class, $actual->data['missing.2']); + $this->assertEquals('abc', $actual->data['missing.1']->retriever()()); + $this->assertEquals(123, $actual->data['missing.2']->retriever()()); $actual = Reflect::invoke($stub, 'applyWhen', true, [ 'present.1' => 'abc', 'present.2' => 123, ]); - $this->assertEquals(new MergeValue([ - 'present.1' => (new ValueMixed(fn() => 'abc'))->when(fn() => true), - 'present.2' => (new ValueMixed(fn() => 123))->when(fn() => true), - ]), $actual); + $this->assertInstanceOf(ValueMixed::class, $actual->data['present.1']); + $this->assertInstanceOf(ValueMixed::class, $actual->data['present.2']); + $this->assertEquals('abc', $actual->data['present.1']->retriever()()); + $this->assertEquals(123, $actual->data['present.2']->retriever()()); + $actual = Reflect::invoke($stub, 'applyWhen', true, [ - 'present.1' => (new ValueMixed(fn() => 'abc')), - 'present.2' => (new ValueMixed(fn() => 123)), - 'present.3' => (new RelationOne('present', fn() => 'abc')), - 'present.4' => (new RelationOne('present', fn() => 123)), - 'present.5' => (new Relationship(UserResource::class, fn() => null)), + 'present.1' => $p1 = (new ValueMixed(fn() => 'abc')), + 'present.2' => $p2 = (new ValueMixed(fn() => 123)), + 'present.3' => $p3 = (new RelationOne('present', fn() => 'abc')), + 'present.4' => $p4 = (new RelationOne('present', fn() => 123)), + 'present.5' => $p5 = (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), - ]), $actual); + $this->assertInstanceOf(MergeValue::class, $actual); + $this->assertEquals($p1, $actual->data['present.1']); + $this->assertEquals($p2, $actual->data['present.2']); + $this->assertEquals($p3, $actual->data['present.3']); + $this->assertEquals($p4, $actual->data['present.4']); + $this->assertInstanceOf(RelationRaw::class, $actual->data['present.5']); + $this->assertInstanceOf(Relationship::class, $actual->data['present.5']->retriever()()); + $actual = Reflect::invoke($stub, 'applyWhen', false, [ - 'missing.1' => (new ValueMixed(fn() => 'abc')), - 'missing.2' => (new ValueMixed(fn() => 123)), - 'missing.3' => (new RelationOne('present', fn() => 'abc')), - 'missing.4' => (new RelationOne('present', fn() => 123)), + 'missing.1' => $p1 = (new ValueMixed(fn() => 'abc')), + 'missing.2' => $p2 = (new ValueMixed(fn() => 123)), + 'missing.3' => $p3 = (new RelationOne('present', fn() => 'abc')), + 'missing.4' => $p4 = (new RelationOne('present', fn() => 123)), '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), - ]), $actual); + $this->assertInstanceOf(MergeValue::class, $actual); + $this->assertEquals($p1, $actual->data['missing.1']); + $this->assertEquals($p2, $actual->data['missing.2']); + $this->assertEquals($p3, $actual->data['missing.3']); + $this->assertEquals($p4, $actual->data['missing.4']); + $this->assertInstanceOf(RelationRaw::class, $actual->data['missing.5']); + $this->assertInstanceOf(Relationship::class, $actual->data['missing.5']->retriever()()); } public function testWhenHas() diff --git a/tests/Unit/Support/ValuesTest.php b/tests/Unit/Support/ValuesTest.php index 8eb53e9..4963843 100644 --- a/tests/Unit/Support/ValuesTest.php +++ b/tests/Unit/Support/ValuesTest.php @@ -136,7 +136,7 @@ public static function dataAttribute() * @dataProvider dataAttribute */ #[DataProvider('dataAttribute')] - public function testHasAttribute($data, $attribute, $expected) + public function testHasAttribute($data, $attribute, $expected, $_ignored) { $this->assertEquals($expected, Values::hasAttribute($data, $attribute)); }