Skip to content

Commit 7f4b109

Browse files
committed
feat: autoload relations: improve & add tests
1 parent 9fa16f7 commit 7f4b109

File tree

13 files changed

+483
-187
lines changed

13 files changed

+483
-187
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Ark4ne\JsonApi\Descriptors\Relations;
4+
5+
use Ark4ne\JsonApi\Resources\Relationship;
6+
use Closure;
7+
use Illuminate\Http\Resources\MissingValue;
8+
9+
final class RelationMissing extends Relation
10+
{
11+
private function __construct(string $related, Closure|string|null $relation)
12+
{
13+
parent::__construct($related, $relation);
14+
15+
$this->when(false);
16+
}
17+
18+
protected function value(Closure $value): Relationship
19+
{
20+
/** @var Relationship $relation */
21+
$relation = ($this->relation)();
22+
$relation->withValue(fn() => new MissingValue);
23+
return $relation;
24+
}
25+
26+
public static function fromRelationship(Relationship $relationship): self
27+
{
28+
return new self($relationship->getResource(), fn() => $relationship);
29+
}
30+
}

src/Resources/Concerns/ConditionallyLoadsAttributes.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Ark4ne\JsonApi\Resources\Concerns;
44

55
use Ark4ne\JsonApi\Descriptors\Relations\Relation;
6+
use Ark4ne\JsonApi\Descriptors\Relations\RelationMissing;
67
use Ark4ne\JsonApi\Resources\Relationship;
78
use Ark4ne\JsonApi\Support\Fields;
89
use Ark4ne\JsonApi\Support\Includes;
@@ -58,14 +59,7 @@ protected function applyWhen(bool $condition, iterable $data): MergeValue
5859

5960
return new MergeValue(collect($data)->map(function ($raw) {
6061
if ($raw instanceof Relationship) {
61-
$relation = new class ($raw->getResource(), fn () => $raw) extends Relation {
62-
protected function value(\Closure $value): Relationship
63-
{
64-
return ($this->relation)();
65-
}
66-
};
67-
68-
return $relation->when(false);
62+
return RelationMissing::fromRelationship($raw);
6963
}
7064

7165
if ($raw instanceof Relation) {

src/Resources/Concerns/PrepareData.php

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Ark4ne\JsonApi\Resources\Concerns;
44

55
use Ark4ne\JsonApi\Descriptors\Resolver;
6+
use Ark4ne\JsonApi\Support\Values;
67
use Illuminate\Http\Request;
78
use Illuminate\Http\Resources\Json\JsonResource;
89
use Illuminate\Http\Resources\MergeValue;
@@ -24,82 +25,6 @@ trait PrepareData
2425
*/
2526
protected function prepareData(Request $request, ?iterable $data): iterable
2627
{
27-
return $data ? $this->resolveValues($request, $this->mergeValues($data)) : [];
28-
}
29-
30-
/**
31-
* @template TKey as array-key
32-
* @template TValue
33-
*
34-
* @param iterable<TKey, TValue> $data
35-
*
36-
* @return array<TKey, TValue>
37-
*/
38-
protected function mergeValues(iterable $data): array
39-
{
40-
$data = collect($data)->all();
41-
42-
$index = -1;
43-
44-
foreach ($data as $key => $value) {
45-
$index++;
46-
47-
if (is_iterable($value)) {
48-
$data[$key] = $this->mergeValues($value);
49-
50-
continue;
51-
}
52-
53-
if (is_numeric($key) && $value instanceof MergeValue) {
54-
$first = array_slice($data, 0, $index, true);
55-
$second = array_slice($data, $index + 1, null, true);
56-
57-
$first = $this->mergeIgnoreMissing($first, $this->mergeValues($value->data));
58-
59-
if (Arr::isList($value->data)) {
60-
return array_merge($first, $this->mergeValues(array_values($second)));
61-
}
62-
63-
return $this->mergeIgnoreMissing($first, $this->mergeValues($second));
64-
}
65-
}
66-
67-
return $data;
68-
}
69-
70-
/**
71-
* Merge two array without erase concrete value by missing value.
72-
*
73-
* @template TKey1 as array-key
74-
* @template TValue1
75-
* @template TKey2 as array-key
76-
* @template TValue2
77-
*
78-
* @param array<TKey1, TValue1> $array
79-
* @param array<TKey2, TValue2> $merge
80-
* @return array<TKey1&TKey2, TValue1&TValue2>
81-
*/
82-
private function mergeIgnoreMissing(array $array, array $merge): array
83-
{
84-
foreach ($merge as $key => $item) {
85-
if (!isset($array[$key]) || $this->isMissing($array[$key]) || !$this->isMissing($item)) {
86-
$array[$key] = $item;
87-
}
88-
}
89-
90-
return $array;
91-
}
92-
93-
/**
94-
* @param mixed|PotentiallyMissing|JsonResource $resource
95-
*
96-
* @return bool
97-
*/
98-
private function isMissing(mixed $resource): bool
99-
{
100-
return ($resource instanceof PotentiallyMissing && $resource->isMissing())
101-
|| ($resource instanceof JsonResource &&
102-
$resource->resource instanceof PotentiallyMissing &&
103-
$resource->resource->isMissing());
28+
return $data ? $this->resolveValues($request, Values::mergeValues($data)) : [];
10429
}
10530
}

src/Resources/Concerns/Relationships.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
use Ark4ne\JsonApi\Descriptors\Resolver;
66
use Ark4ne\JsonApi\Resources\Relationship;
7+
use Ark4ne\JsonApi\Support\Arr;
78
use Ark4ne\JsonApi\Support\Includes;
9+
use Ark4ne\JsonApi\Support\Values;
810
use Illuminate\Http\Request;
9-
use Illuminate\Support\Arr;
1011
use Illuminate\Support\Collection;
1112

1213
trait Relationships
@@ -55,7 +56,7 @@ public function requestedRelationshipsLoad(Request $request): array
5556
$loads[$value] = $include;
5657
} elseif (is_string($key)) {
5758
$loads[$key] = $value;
58-
foreach (Arr::dot(Arr::undot($include), "$key.") as $inc => $item) {
59+
foreach (Arr::flatDot($include, $key) as $inc => $item) {
5960
$loads[$inc] = $item;
6061
}
6162
}
@@ -66,7 +67,7 @@ public function requestedRelationshipsLoad(Request $request): array
6667
return $loads;
6768
};
6869

69-
return Arr::dot($walk($schema));
70+
return Arr::flatDot($walk($schema));
7071
}
7172

7273
/**
@@ -78,7 +79,7 @@ private function requestedRelationships(Request $request): array
7879
{
7980
$relations = [];
8081
$relationships = $this->toRelationships($request);
81-
$relationships = $this->mergeValues($relationships);
82+
$relationships = Values::mergeValues($relationships);
8283
$relationships = $this->resolveValues($request, $relationships);
8384
$relationships = $this->filter($relationships);
8485

src/Resources/Concerns/Schema.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
use Ark4ne\JsonApi\Descriptors\Relations\Relation;
77
use Ark4ne\JsonApi\Descriptors\Resolver;
88
use Ark4ne\JsonApi\Resources\Skeleton;
9-
use Ark4ne\JsonApi\Support\Arr;
109
use Ark4ne\JsonApi\Support\FakeModel;
10+
use Ark4ne\JsonApi\Support\Values;
1111
use Illuminate\Http\Request;
1212
use Illuminate\Support\Collection;
1313
use ReflectionClass;
@@ -36,12 +36,12 @@ public static function schema(Request $request = null): Skeleton
3636
$resource->toType($request)
3737
);
3838

39-
$schema->fields = (new Collection($resource->mergeValues($resource->toAttributes($request))))
39+
$schema->fields = (new Collection(Values::mergeValues($resource->toAttributes($request))))
4040
->map(fn($value, $key) => Describer::retrieveName($value, $key))
4141
->values()
4242
->all();
4343

44-
foreach ($resource->toRelationships($request) as $name => $relation) {
44+
foreach (Values::mergeValues($resource->toRelationships($request)) as $name => $relation) {
4545
if ($relation instanceof Relation) {
4646
$relationship = $relation->related();
4747
$name = Describer::retrieveName($relation, $name);
@@ -61,8 +61,8 @@ public static function schema(Request $request = null): Skeleton
6161
elseif (is_string($load)) {
6262
$schema->loads[$name] = $load;
6363
}
64-
elseif (is_array($load)) {
65-
foreach ($load as $key => $value) {
64+
else {
65+
foreach ((array)$load as $key => $value) {
6666
$schema->loads[$name][$key] = $value;
6767
}
6868
}

src/Resources/Relationship.php

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace Ark4ne\JsonApi\Resources;
44

5+
use Ark4ne\JsonApi\Support\Values;
56
use Ark4ne\JsonApi\Traits\HasRelationLoad;
67
use Closure;
78
use Illuminate\Http\Resources\Json\JsonResource;
89
use Illuminate\Http\Resources\Json\ResourceCollection;
910
use Illuminate\Http\Resources\MissingValue;
10-
use Illuminate\Http\Resources\PotentiallyMissing;
1111
use Illuminate\Support\Collection;
1212

1313
use function value;
@@ -39,6 +39,20 @@ public function __construct(
3939
) {
4040
}
4141

42+
/**
43+
* Re-set value for relation
44+
*
45+
* @param Closure $value
46+
*
47+
* @return $this
48+
*/
49+
public function withValue(Closure $value): self
50+
{
51+
$this->value = $value;
52+
53+
return $this;
54+
}
55+
4256
/**
4357
* Set callback for links for relation
4458
*
@@ -140,7 +154,7 @@ public function toArray(mixed $request, bool $included = true): array
140154
'meta' => value($this->meta, $resource),
141155
];
142156

143-
if ($this->isMissing($resource)) {
157+
if (Values::isMissing($resource)) {
144158
return [
145159
'data' => array_filter($data)
146160
];
@@ -184,17 +198,4 @@ public function toArray(mixed $request, bool $included = true): array
184198
: null
185199
]);
186200
}
187-
188-
/**
189-
* @param mixed|PotentiallyMissing|JsonResource $resource
190-
*
191-
* @return bool
192-
*/
193-
private function isMissing(mixed $resource): bool
194-
{
195-
return ($resource instanceof PotentiallyMissing && $resource->isMissing())
196-
|| ($resource instanceof JsonResource &&
197-
$resource->resource instanceof PotentiallyMissing &&
198-
$resource->resource->isMissing());
199-
}
200201
}

src/Support/Arr.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,108 @@ public static function wash(iterable $with): array
6565
return array_filter(self::uniqueRecursive(self::toArray($with)));
6666
}
6767

68+
/**
69+
* @param array<mixed> $array
70+
* @param string $prepend
71+
* @return array<mixed>
72+
*/
73+
public static function flatDot(array $array, string $prepend = ''): array
74+
{
75+
return self::flattenDot($array, $prepend, uniqid('self-', false));
76+
}
77+
78+
/**
79+
* @param array<mixed> $array
80+
* @param string $prepend
81+
* @param string $saveKey
82+
* @return array<mixed>
83+
*/
84+
private static function flattenDot(array $array, string $prepend, string $saveKey): array
85+
{
86+
$array = self::undot($array, $saveKey);
87+
88+
$results = [];
89+
90+
foreach ($array as $key => $value) {
91+
if (is_array($value) && !empty($value)) {
92+
foreach (self::flattenDot($value, $prepend ? "$prepend.$key" : $key, $saveKey) as $itemKey => $item) {
93+
$results[str_replace(".$saveKey", '', $itemKey)] = $item;
94+
}
95+
} elseif ($key === $saveKey) {
96+
$results[$prepend] = $value;
97+
} else {
98+
$results[$prepend ? "$prepend.$key" : $key] = $value;
99+
}
100+
}
101+
102+
return $results;
103+
}
104+
105+
/**
106+
* @param array<mixed> $array
107+
* @param string|null $saveKey
108+
* @return array<mixed>
109+
*/
110+
public static function undot(array $array, string $saveKey = null): array
111+
{
112+
$results = [];
113+
114+
ksort($array);
115+
116+
foreach ($array as $key => $value) {
117+
$value = is_array($value) ? self::undot($value, $saveKey) : $value;
118+
119+
self::apply($results, $key, $value, $saveKey);
120+
}
121+
122+
return $results;
123+
}
124+
125+
/**
126+
* @param array<mixed> $array
127+
* @param string $path
128+
* @param mixed $value
129+
* @param string|null $saveKey
130+
* @return mixed
131+
*/
132+
public static function apply(array &$array, string $path, mixed $value, string $saveKey = null): mixed
133+
{
134+
if (is_null($path)) {
135+
return $array = $value;
136+
}
137+
138+
$keys = explode('.', $path);
139+
140+
$keysCount = count($keys);
141+
142+
foreach ($keys as $i => $key) {
143+
if ($keysCount === 1) {
144+
break;
145+
}
146+
147+
$keysCount--;
148+
unset($keys[$i]);
149+
150+
// If the key doesn't exist at this depth, we will just create an empty array
151+
// to hold the next value, allowing us to create the arrays to hold final
152+
// values at the correct depth. Then we'll keep digging into the array.
153+
if (!isset($array[$key])) {
154+
$array[$key] = [];
155+
}
156+
if (!is_array($array[$key])) {
157+
$array[$key] = $saveKey
158+
? [$saveKey => $array[$key]]
159+
: [];
160+
}
161+
162+
$array = &$array[$key];
163+
}
164+
165+
$array[array_shift($keys)] = $value;
166+
167+
return $array;
168+
}
169+
68170
/**
69171
* @template TKey as array-key
70172
* @template TValue

0 commit comments

Comments
 (0)