Skip to content

Commit 9848151

Browse files
committed
[Bugfix] Fix include paths with depth
1 parent 6c17337 commit 9848151

File tree

5 files changed

+220
-9
lines changed

5 files changed

+220
-9
lines changed

src/Schema/RelationshipPath.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
/*
3+
* Copyright 2022 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace CloudCreativity\LaravelJsonApi\Schema;
21+
22+
use Countable;
23+
use Illuminate\Support\Collection;
24+
use InvalidArgumentException;
25+
use IteratorAggregate;
26+
use Traversable;
27+
use UnexpectedValueException;
28+
use function explode;
29+
use function implode;
30+
use function is_string;
31+
32+
class RelationshipPath implements IteratorAggregate, Countable
33+
{
34+
35+
/**
36+
* @var string[]
37+
*/
38+
private array $names;
39+
40+
/**
41+
* @param RelationshipPath|string $value
42+
* @return RelationshipPath
43+
*/
44+
public static function cast($value): self
45+
{
46+
if ($value instanceof self) {
47+
return $value;
48+
}
49+
50+
if (is_string($value)) {
51+
return self::fromString($value);
52+
}
53+
54+
throw new UnexpectedValueException('Unexpected relationship path value.');
55+
}
56+
57+
/**
58+
* @param string $path
59+
* @return RelationshipPath
60+
*/
61+
public static function fromString(string $path): self
62+
{
63+
if (!empty($path)) {
64+
return new self(...explode('.', $path));
65+
}
66+
67+
throw new UnexpectedValueException('Expecting a non-empty string.');
68+
}
69+
70+
/**
71+
* IncludePath constructor.
72+
*
73+
* @param string ...$paths
74+
*/
75+
public function __construct(string ...$paths)
76+
{
77+
if (empty($paths)) {
78+
throw new InvalidArgumentException('Expecting at least one relationship path.');
79+
}
80+
81+
$this->names = $paths;
82+
}
83+
84+
/**
85+
* @return string
86+
*/
87+
public function __toString()
88+
{
89+
return $this->toString();
90+
}
91+
92+
/**
93+
* Fluent to string method.
94+
*
95+
* @return string
96+
*/
97+
public function toString(): string
98+
{
99+
return implode('.', $this->names);
100+
}
101+
102+
/**
103+
* @return array
104+
*/
105+
public function names(): array
106+
{
107+
return $this->names;
108+
}
109+
110+
/**
111+
* @inheritDoc
112+
*/
113+
public function getIterator(): Traversable
114+
{
115+
yield from $this->names;
116+
}
117+
118+
/**
119+
* @inheritDoc
120+
*/
121+
public function count(): int
122+
{
123+
return count($this->names);
124+
}
125+
126+
/**
127+
* Get the first name.
128+
*
129+
* @return string
130+
*/
131+
public function first(): string
132+
{
133+
return $this->names[0];
134+
}
135+
136+
/**
137+
* @param int $num
138+
* @return $this
139+
*/
140+
public function take(int $num): self
141+
{
142+
return new self(
143+
...Collection::make($this->names)->take($num)
144+
);
145+
}
146+
147+
/**
148+
* @param int $num
149+
* @return $this|null
150+
*/
151+
public function skip(int $num): ?self
152+
{
153+
$names = Collection::make($this->names)->skip($num);
154+
155+
if ($names->isNotEmpty()) {
156+
return new self(...$names);
157+
}
158+
159+
return null;
160+
}
161+
}

src/Schema/SchemaFields.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,12 @@ public function __construct(iterable $paths = null, iterable $fieldSets = null)
8787
{
8888
if (null !== $paths) {
8989
foreach ($paths as $path) {
90-
$separatorPos = \strrpos($path, static::PATH_SEPARATOR);
91-
if ($separatorPos === false) {
92-
$curPath = '';
93-
$relationship = $path;
94-
} else {
95-
$curPath = \substr($path, 0, $separatorPos);
96-
$relationship = \substr($path, $separatorPos + 1);
90+
$path = RelationshipPath::cast($path);
91+
foreach ($path as $key => $relationship) {
92+
$curPath = (0 === $key) ? '' : $path->take($key)->toString();
93+
$this->fastRelationships[$curPath][$relationship] = true;
94+
$this->fastRelationshipLists[$curPath][$relationship] = $relationship;
9795
}
98-
$this->fastRelationships[$curPath][$relationship] = true;
99-
$this->fastRelationshipLists[$curPath][$relationship] = $relationship;
10096
}
10197
}
10298

tests/dummy/app/JsonApi/Posts/Validators.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Validators extends AbstractValidators
5252
*/
5353
protected $allowedIncludePaths = [
5454
'author',
55+
'author.phone',
5556
'comments',
5657
'comments.createdBy',
5758
'image',

tests/lib/Integration/Eloquent/ResourceTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use CloudCreativity\LaravelJsonApi\Tests\Integration\TestCase;
2323
use DummyApp\Comment;
2424
use DummyApp\JsonApi\Posts\Schema;
25+
use DummyApp\Phone;
2526
use DummyApp\Post;
2627
use DummyApp\Tag;
2728
use Illuminate\Support\Facades\Event;
@@ -409,6 +410,43 @@ public function testReadWithInclude()
409410
->assertIsIncluded('tags', $tag);
410411
}
411412

413+
public function testReadWithIncludeAtDepth(): void
414+
{
415+
$model = $this->createPost();
416+
417+
$phone = factory(Phone::class)->create(['user_id' => $model->author]);
418+
419+
$comments = factory(Comment::class, 2)->create([
420+
'commentable_type' => Post::class,
421+
'commentable_id' => $model,
422+
]);
423+
424+
$expected = $this->serialize($model);
425+
426+
$expected['relationships']['author']['data'] = $userId = [
427+
'type' => 'users',
428+
'id' => (string) $model->getRouteKey(),
429+
];
430+
431+
$expected['relationships']['comments']['data'] = $commentIds = $comments->map(
432+
fn(Comment $comment) => ['type' => 'comments', 'id' => (string) $comment->getRouteKey()],
433+
);
434+
435+
$response = $this
436+
->jsonApi()
437+
->includePaths('author.phone', 'comments.createdBy')
438+
->get(url('/api/v1/posts', $model));
439+
440+
$response->assertFetchedOne($expected)->assertIncluded([
441+
$userId,
442+
['type' => 'phones', 'id' => (string) $phone->getRouteKey()],
443+
$commentIds[0],
444+
['type' => 'users', 'id' => (string) $comments[0]->user->getRouteKey()],
445+
$commentIds[1],
446+
['type' => 'users', 'id' => (string) $comments[1]->user->getRouteKey()],
447+
]);
448+
}
449+
412450
/**
413451
* @see https://github.com/cloudcreativity/laravel-json-api/issues/518
414452
*/

tests/lib/Unit/Schema/SchemaFieldsTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function test(): SchemaFields
4747
$this->assertTrue($fields->isRelationshipRequested('', 'a2'));
4848
$this->assertFalse($fields->isRelationshipRequested('', 'blah'));
4949

50+
$this->assertSame(['b2' => 'b2'], $fields->getRequestedRelationships('a2'));
5051
$this->assertSame(['c2' => 'c2'], $fields->getRequestedRelationships('a2.b2'));
5152
$this->assertTrue($fields->isRelationshipRequested('a2.b2', 'c2'));
5253
$this->assertFalse($fields->isRelationshipRequested('a2.b2', 'blah'));
@@ -65,6 +66,20 @@ public function test(): SchemaFields
6566
return $fields;
6667
}
6768

69+
public function test2(): void
70+
{
71+
$fields = new SchemaFields([
72+
'author.phone',
73+
'comments.createdBy',
74+
]);
75+
76+
$this->assertSame(['author' => 'author', 'comments' => 'comments'], $fields->getRequestedRelationships(''));
77+
$this->assertSame(['phone' => 'phone'], $fields->getRequestedRelationships('author'));
78+
$this->assertEmpty($fields->getRequestedRelationships('author.phone'));
79+
$this->assertSame(['createdBy' => 'createdBy'], $fields->getRequestedRelationships('comments'));
80+
$this->assertEmpty($fields->getRequestedRelationships('comments.createdBy'));
81+
}
82+
6883
/**
6984
* @param SchemaFields $expected
7085
* @return void

0 commit comments

Comments
 (0)