Skip to content

fix: Resolve nested relations #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions src/Resolver/StoryResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,34 @@ public function resolve(array $target, array $relations): array
$relationMap[$relation['uuid']] = $relation;
}

foreach ($target as &$value) {
if (\is_string($value) && \array_key_exists($value, $relationMap)) {
$value = $relationMap[$value];
return $this->doResolve($target, $relationMap, []);
Copy link

@frastel frastel Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are two things done:

  • deep resolving of relations
  • not to replace the uuids themselves

This would fix my both opened issues. But I think you could simplify this approach a bit.
We have implemented our own StoryResolver in the meantime (I had not enough time yet to create a PR with our version) and we did it like this:

// resolve relations which may be included in the resolved relations themselves
$this->doResolve($relationMap, $relationMap);

// resolve relations in the main target
$this->doResolve($target, $relationMap);

return $target;
        
private function doResolve(array &$target, array $relationMap): void
//...
// uuid check here like it is done in this PR

We are passing the $target as reference because our API responses are quite huge and we want to lower the memory usage on our servers.

In this case you do not need the $seen parameter and can assume that the resolved relations are already resolved when the main stories are looped.

}

private function doResolve(array $target, array $relationMap, array $seen): array
{
foreach ($target as $key => $value) {
if ('uuid' === $key) {
continue;
}

if (\is_array($value) && \array_key_exists('id', $value) && \array_key_exists($value['id'], $relationMap)) {
$value = $relationMap[$value['id']];
if (\is_string($value) && isset($relationMap[$value])) {
if (\in_array($value, $seen, true)) {
continue;
}

continue;
}
$seen[] = $value;
$target[$key] = $this->doResolve($relationMap[$value], $relationMap, $seen);
} elseif (\is_array($value) && isset($value['id'], $relationMap[$value['id']])) {
$id = $value['id'];

if (\in_array($id, $seen, true)) {
continue;
}

if (\is_array($value)) {
$value = $this->resolve($value, $relations);
$seen[] = $id;
$target[$key] = $this->doResolve($relationMap[$id], $relationMap, $seen);
} elseif (\is_array($value)) {
$target[$key] = $this->doResolve($value, $relationMap, $seen);
}
}

Expand Down
117 changes: 117 additions & 0 deletions tests/Unit/Resolver/StoryResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,121 @@ public function resolveReplacesMultiLinkWithLinkPayload(): void

self::assertSame($expected, $resolver->resolve($story, $references));
}

#[Test]
public function resolveRecursive(): void
{
$resolver = new StoryResolver();

$faker = self::faker();

$story = [
'name' => $faker->word(),
'content' => [
'uuid' => $faker->uuid(),
'reference' => $cUuid = $faker->uuid(),
'some_field' => $faker->word(),
],
];

$references = [
$b = [
'uuid' => $bUuid = $faker->uuid(),
'name' => $faker->word(),
'another_field' => $faker->sentence(),
],
$c = [
'uuid' => $cUuid,
'name' => $faker->word(),
'some_field' => [
'test' => $bUuid,
],
],
];

$expected = $story;
$expected['content']['reference'] = $c;
$expected['content']['reference']['some_field']['test'] = $b;

self::assertSame($expected, $resolver->resolve($story, $references));
}

#[Test]
public function resolveWithManyNestedRels(): void
{
$resolver = new StoryResolver();

$faker = self::faker();

$story = [
'name' => $faker->word(),
'content' => [
'uuid' => $faker->uuid(),
'reference' => $cUuid = $faker->uuid(),
'some_field' => $faker->word(),
],
];

$references = [
$a = [
'uuid' => $aUuid = $faker->uuid,
'name' => $faker->word(),
],
$b = [
'uuid' => $bUuid = $faker->uuid(),
'name' => $faker->word(),
'another_field' => $faker->sentence(),
'field' => $aUuid,
],
$c = [
'uuid' => $cUuid,
'name' => $faker->word(),
'some_field' => [
'test' => $bUuid,
],
],
];

$expected = $story;
$expected['content']['reference'] = $c;
$expected['content']['reference']['some_field']['test'] = $b;
$expected['content']['reference']['some_field']['test']['field'] = $a;

self::assertSame($expected, $resolver->resolve($story, $references));
}

#[Test]
public function resolveMustNeverResolveUuidOfStory(): void
{
$resolver = new StoryResolver();

$faker = self::faker();

$story = [
'name' => $faker->word(),
'content' => [
'uuid' => $uuid = $faker->uuid(),
'reference' => $referenceUuid = $faker->uuid(),
'some_field' => $faker->word(),
],
];

$references = [
$a = [
'uuid' => $referenceUuid,
'name' => $faker->word(),
'another_field' => $faker->sentence(),
],
$b = [
'uuid' => $uuid,
'name' => $faker->word(),
'another_field' => $faker->sentence(),
],
];

$expected = $story;
$expected['content']['reference'] = $a;

self::assertSame($expected, $resolver->resolve($story, $references));
}
}