Skip to content

Commit e7500c8

Browse files
committed
Corrected build up queries with semi-circular dependencies
1 parent b153414 commit e7500c8

10 files changed

+178
-21
lines changed

src/Entity/Join.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
namespace WyriHaximus\React\SimpleORM\Entity;
44

5-
use WyriHaximus\React\SimpleORM\InspectedEntity;
5+
use WyriHaximus\React\SimpleORM\InspectedEntityInterface;
66

77
final class Join
88
{
9-
/** @var InspectedEntity */
9+
/** @var InspectedEntityInterface */
1010
private $entity;
1111

1212
/** @var string */
@@ -27,7 +27,7 @@ final class Join
2727
/** @var string */
2828
private $property;
2929

30-
public function __construct(InspectedEntity $entity, string $type, string $localKey, ?string $localCast, string $foreignKey, ?string $foreignCast, string $property)
30+
public function __construct(InspectedEntityInterface $entity, string $type, string $localKey, ?string $localCast, string $foreignKey, ?string $foreignCast, string $property)
3131
{
3232
$this->entity = $entity;
3333
$this->type = $type;
@@ -38,7 +38,7 @@ public function __construct(InspectedEntity $entity, string $type, string $local
3838
$this->property = $property;
3939
}
4040

41-
public function getEntity(): InspectedEntity
41+
public function getEntity(): InspectedEntityInterface
4242
{
4343
return $this->entity;
4444
}

src/EntityInspector.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ final class EntityInspector
1717
/** @var Reader */
1818
private $annotationReader;
1919

20-
/** @var InspectedEntity[] */
20+
/** @var InspectedEntityInterface[] */
2121
private $entities = [];
2222

2323
public function __construct(Reader $annotationReader)
2424
{
2525
$this->annotationReader = $annotationReader;
2626
}
2727

28-
public function getEntity(string $entity): InspectedEntity
28+
public function getEntity(string $entity): InspectedEntityInterface
2929
{
3030
if (!isset($this->entities[$entity])) {
3131
/** @psalm-suppress ArgumentTypeCoercion */
@@ -77,7 +77,7 @@ private function getJoins(ReflectionClass $class): iterable
7777
}
7878

7979
yield $annotation->getProperty() => new Join(
80-
$this->getEntity($annotation->getEntity()),
80+
new LazyInspectedEntity($this, $annotation->getEntity()),
8181
$annotation->getType(),
8282
$annotation->getLocalKey(),
8383
$annotation->getLocalCast(),

src/Hydrator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ final class Hydrator
1111
/** @var string[] */
1212
private $hydrators = [];
1313

14-
public function hydrate(InspectedEntity $inspectedEntity, array $data): EntityInterface
14+
public function hydrate(InspectedEntityInterface $inspectedEntity, array $data): EntityInterface
1515
{
1616
$class = $inspectedEntity->getClass();
1717
if (!isset($this->hydrators[$class])) {
@@ -49,7 +49,7 @@ public function hydrate(InspectedEntity $inspectedEntity, array $data): EntityIn
4949
return $this->hydrators[$class]->hydrate($data, new $class());
5050
}
5151

52-
public function extract(InspectedEntity $inspectedEntity, EntityInterface $entity): array
52+
public function extract(InspectedEntityInterface $inspectedEntity, EntityInterface $entity): array
5353
{
5454
$class = $inspectedEntity->getClass();
5555
/** @var HydratorInterface $hydrator */

src/InspectedEntity.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use WyriHaximus\React\SimpleORM\Entity\Field;
66
use WyriHaximus\React\SimpleORM\Entity\Join;
77

8-
final class InspectedEntity
8+
final class InspectedEntity implements InspectedEntityInterface
99
{
1010
/** @var string */
1111
private $class;

src/InspectedEntityInterface.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace WyriHaximus\React\SimpleORM;
4+
5+
use WyriHaximus\React\SimpleORM\Entity\Field;
6+
use WyriHaximus\React\SimpleORM\Entity\Join;
7+
8+
interface InspectedEntityInterface
9+
{
10+
public function getClass(): string;
11+
12+
public function getTable(): string;
13+
14+
/**
15+
* @return Field[]
16+
*/
17+
public function getFields(): array;
18+
19+
/**
20+
* @return Join[]
21+
*/
22+
public function getJoins(): array;
23+
}

src/LazyInspectedEntity.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace WyriHaximus\React\SimpleORM;
4+
5+
use WyriHaximus\React\SimpleORM\Entity\Field;
6+
use WyriHaximus\React\SimpleORM\Entity\Join;
7+
8+
final class LazyInspectedEntity implements InspectedEntityInterface
9+
{
10+
/** @var string */
11+
private $class;
12+
13+
/** @var string|null */
14+
private $table;
15+
16+
/** @var Field[] */
17+
private $fields = [];
18+
19+
/** @var Join[] */
20+
private $joins = [];
21+
22+
/** @var EntityInspector|null */
23+
private $entityInspector;
24+
25+
public function __construct(EntityInspector $entityInspector, string $class)
26+
{
27+
$this->class = $class;
28+
$this->entityInspector = $entityInspector;
29+
}
30+
31+
public function getClass(): string
32+
{
33+
return $this->class;
34+
}
35+
36+
/** @psalm-suppress InvalidNullableReturnType */
37+
public function getTable(): string
38+
{
39+
if ($this->table === null) {
40+
$this->loadEntity();
41+
}
42+
43+
/** @psalm-suppress NullableReturnStatement */
44+
return $this->table;
45+
}
46+
47+
/**
48+
* @return Field[]
49+
*/
50+
public function getFields(): array
51+
{
52+
if ($this->table === null) {
53+
$this->loadEntity();
54+
}
55+
56+
return $this->fields;
57+
}
58+
59+
/**
60+
* @return Join[]
61+
*/
62+
public function getJoins(): array
63+
{
64+
if ($this->table === null) {
65+
$this->loadEntity();
66+
}
67+
68+
return $this->joins;
69+
}
70+
71+
private function loadEntity(): void
72+
{
73+
if ($this->entityInspector === null) {
74+
return;
75+
}
76+
77+
$inspectedEntity = $this->entityInspector->getEntity($this->class);
78+
$this->entityInspector = null;
79+
80+
$this->table = $inspectedEntity->getTable();
81+
$this->fields = $inspectedEntity->getFields();
82+
$this->joins = $inspectedEntity->getJoins();
83+
}
84+
}

src/Repository.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ final class Repository implements RepositoryInterface
1212
{
1313
private const SINGLE = 1;
1414

15-
/** @var InspectedEntity */
15+
/** @var InspectedEntityInterface */
1616
private $entity;
1717

1818
/** @var ClientInterface */
@@ -34,7 +34,7 @@ final class Repository implements RepositoryInterface
3434
/** @var string[] */
3535
private $tableAliases = [];
3636

37-
public function __construct(InspectedEntity $entity, ClientInterface $client)
37+
public function __construct(InspectedEntityInterface $entity, ClientInterface $client)
3838
{
3939
$this->entity = $entity;
4040
$this->client = $client;
@@ -148,24 +148,27 @@ private function buildBaseQuery(): QueryBuilder
148148
return $query;
149149
}
150150

151-
private function buildJoins(QueryBuilder $query, InspectedEntity $entity, int &$i): QueryBuilder
151+
private function buildJoins(QueryBuilder $query, InspectedEntityInterface $entity, int &$i, string $rootProperty = 'root'): QueryBuilder
152152
{
153153
foreach ($entity->getJoins() as $join) {
154154
if ($join->getType() !== 'inner') {
155155
continue;
156156
}
157157

158158
$tableKey = \spl_object_hash($join->getEntity()) . '___' . $join->getProperty();
159-
$this->tableAliases[$tableKey] = 't' . $i++;
159+
if (!isset($this->tableAliases[$tableKey])) {
160+
$this->tableAliases[$tableKey] = 't' . $i++;
161+
}
160162

161163
$foreignTable = $join->getEntity()->getTable();
162164
$onLeftSide = $this->tableAliases[$tableKey] . '.' . $join->getForeignKey();
163165
if ($join->getForeignCast() !== null) {
164166
/** @psalm-suppress PossiblyNullOperand */
165167
$onLeftSide = 'CAST(' . $onLeftSide . ' AS ' . $join->getForeignCast() . ')';
166168
}
169+
167170
$onRightSide =
168-
$this->tableAliases[\spl_object_hash($entity) . '___root'] . '.' . $join->getLocalKey();
171+
$this->tableAliases[\spl_object_hash($entity) . '___' . $rootProperty] . '.' . $join->getLocalKey();
169172
if ($join->getLocalCast() !== null) {
170173
/** @psalm-suppress PossiblyNullOperand */
171174
$onRightSide = 'CAST(' . $onRightSide . ' AS ' . $join->getLocalCast() . ')';
@@ -185,7 +188,7 @@ private function buildJoins(QueryBuilder $query, InspectedEntity $entity, int &$
185188

186189
unset($this->fields[$entity->getTable() . '___' . $join->getProperty()]);
187190

188-
$query = $this->buildJoins($query, $join->getEntity(), $i);
191+
$query = $this->buildJoins($query, $join->getEntity(), $i, $join->getProperty());
189192
}
190193

191194
return $query;
@@ -216,7 +219,7 @@ private function inflate(array $row): array
216219
return $tables;
217220
}
218221

219-
private function buildTree(array $row, InspectedEntity $entity, string $tableKeySuffix = 'root'): array
222+
private function buildTree(array $row, InspectedEntityInterface $entity, string $tableKeySuffix = 'root'): array
220223
{
221224
$tableKey = \spl_object_hash($entity) . '___' . $tableKeySuffix;
222225
$tree = $row[$this->tableAliases[$tableKey]];

tests/EntityInspectorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public function inspectWithJoins(): void
8989

9090
self::assertArrayHasKey('author', $joins['comments']->getEntity()->getJoins());
9191
self::assertSame(UserStub::class, $joins['comments']->getEntity()->getJoins()['author']->getEntity()->getClass());
92+
self::assertCount(2, $joins['comments']->getEntity()->getJoins()['author']->getEntity()->getFields());
9293
self::assertSame('author_id', $joins['comments']->getEntity()->getJoins()['author']->getLocalKey());
9394
self::assertNull($joins['comments']->getEntity()->getJoins()['author']->getLocalCast());
9495
self::assertSame('id', $joins['comments']->getEntity()->getJoins()['author']->getForeignKey());

tests/RepositoryTest.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,31 +218,55 @@ public function testFetchWithJoinsLazyLoadComments(): void
218218
}))->shouldBeCalled()->willReturn(observableFromArray([
219219
[
220220
't0___id' => '99d00028-28d6-4194-b377-a0039b278c4d',
221-
't0___title' => 'blog_post_title',
221+
't0___blog_post_id' => '99d00028-28d6-4194-b377-a0039b278c4d',
222222
't0___contents' => 'comment contents',
223223
't1___id' => 'd45e8a1b-b962-4c1b-a7e7-c867fa06ffa7',
224224
't1___name' => 'author_name',
225+
't2___id' => '99d00028-28d6-4194-b377-a0039b278c4d',
226+
't2___title' => 'blog_post_title',
227+
't3___id' => '3fbf8eec-8a3f-4b01-ba9a-355f6650644b',
228+
't3___name' => 'author_name',
229+
't4___id' => 'd45e8a1b-b962-4c1b-a7e7-c867fa06ffa7',
230+
't4___name' => 'publisher_name',
225231
],
226232
[
227233
't0___id' => 'fa41900d-4f62-4037-9eb3-8cfb4b90eeef',
228-
't0___title' => 'blog_post_title',
234+
't0___blog_post_id' => '99d00028-28d6-4194-b377-a0039b278c4d',
229235
't0___contents' => 'comment contents',
230236
't1___id' => '0da49bee-ab27-4b24-a949-7b71a0b0449a',
231237
't1___name' => 'author_name',
238+
't2___id' => '99d00028-28d6-4194-b377-a0039b278c4d',
239+
't2___title' => 'blog_post_title',
240+
't3___id' => '3fbf8eec-8a3f-4b01-ba9a-355f6650644b',
241+
't3___name' => 'author_name',
242+
't4___id' => 'd45e8a1b-b962-4c1b-a7e7-c867fa06ffa7',
243+
't4___name' => 'publisher_name',
232244
],
233245
[
234246
't0___id' => '83f451cb-4b20-41b5-a8be-637af0bf1284',
235-
't0___title' => 'blog_post_title',
247+
't0___blog_post_id' => '99d00028-28d6-4194-b377-a0039b278c4d',
236248
't0___contents' => 'comment contents',
237249
't1___id' => '3fbf8eec-8a3f-4b01-ba9a-355f6650644b',
238250
't1___name' => 'author_name',
251+
't2___id' => '99d00028-28d6-4194-b377-a0039b278c4d',
252+
't2___title' => 'blog_post_title',
253+
't3___id' => '3fbf8eec-8a3f-4b01-ba9a-355f6650644b',
254+
't3___name' => 'author_name',
255+
't4___id' => 'd45e8a1b-b962-4c1b-a7e7-c867fa06ffa7',
256+
't4___name' => 'publisher_name',
239257
],
240258
[
241259
't0___id' => '590d4a9d-afb2-4860-a746-b0a086554064',
242-
't0___title' => 'blog_post_title',
260+
't0___blog_post_id' => '99d00028-28d6-4194-b377-a0039b278c4d',
243261
't0___contents' => 'comment contents',
244262
't1___id' => '0da49bee-ab27-4b24-a949-7b71a0b0449a',
245263
't1___name' => 'author_name',
264+
't2___id' => '99d00028-28d6-4194-b377-a0039b278c4d',
265+
't2___title' => 'blog_post_title',
266+
't3___id' => '3fbf8eec-8a3f-4b01-ba9a-355f6650644b',
267+
't3___name' => 'author_name',
268+
't4___id' => 'd45e8a1b-b962-4c1b-a7e7-c867fa06ffa7',
269+
't4___name' => 'publisher_name',
246270
],
247271
]));
248272

@@ -270,14 +294,22 @@ public function testFetchWithJoinsLazyLoadComments(): void
270294

271295
self::assertSame('99d00028-28d6-4194-b377-a0039b278c4d', $comments[0]->getId());
272296
self::assertSame('d45e8a1b-b962-4c1b-a7e7-c867fa06ffa7', $comments[0]->getAuthor()->getId());
297+
self::assertSame('99d00028-28d6-4194-b377-a0039b278c4d', $comments[0]->getBlogPost()->getId());
298+
self::assertSame('3fbf8eec-8a3f-4b01-ba9a-355f6650644b', $comments[0]->getBlogPost()->getAuthor()->getId());
273299

274300
self::assertSame('fa41900d-4f62-4037-9eb3-8cfb4b90eeef', $comments[1]->getId());
275301
self::assertSame('0da49bee-ab27-4b24-a949-7b71a0b0449a', $comments[1]->getAuthor()->getId());
302+
self::assertSame('99d00028-28d6-4194-b377-a0039b278c4d', $comments[1]->getBlogPost()->getId());
303+
self::assertSame('3fbf8eec-8a3f-4b01-ba9a-355f6650644b', $comments[1]->getBlogPost()->getAuthor()->getId());
276304

277305
self::assertSame('83f451cb-4b20-41b5-a8be-637af0bf1284', $comments[2]->getId());
278306
self::assertSame('3fbf8eec-8a3f-4b01-ba9a-355f6650644b', $comments[2]->getAuthor()->getId());
307+
self::assertSame('99d00028-28d6-4194-b377-a0039b278c4d', $comments[2]->getBlogPost()->getId());
308+
self::assertSame('3fbf8eec-8a3f-4b01-ba9a-355f6650644b', $comments[2]->getBlogPost()->getAuthor()->getId());
279309

280310
self::assertSame('590d4a9d-afb2-4860-a746-b0a086554064', $comments[3]->getId());
281311
self::assertSame('0da49bee-ab27-4b24-a949-7b71a0b0449a', $comments[3]->getAuthor()->getId());
312+
self::assertSame('99d00028-28d6-4194-b377-a0039b278c4d', $comments[3]->getBlogPost()->getId());
313+
self::assertSame('3fbf8eec-8a3f-4b01-ba9a-355f6650644b', $comments[3]->getBlogPost()->getAuthor()->getId());
282314
}
283315
}

tests/Stub/CommentStub.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
foreign_key="id",
1515
property="author"
1616
* )
17+
* @InnerJoin(
18+
entity=BlogPostStub::class,
19+
local_key="blog_post_id",
20+
foreign_key="id",
21+
property="blog_post"
22+
* )
1723
*/
1824
class CommentStub implements EntityInterface
1925
{
@@ -29,6 +35,9 @@ class CommentStub implements EntityInterface
2935
/** @var string */
3036
protected $blog_post_id;
3137

38+
/** @var BlogPostStub */
39+
protected $blog_post;
40+
3241
/** @var string */
3342
protected $contents;
3443

@@ -42,6 +51,11 @@ public function getContents(): string
4251
return $this->contents;
4352
}
4453

54+
public function getBlogPost(): BlogPostStub
55+
{
56+
return $this->blog_post;
57+
}
58+
4559
public function getAuthor(): UserStub
4660
{
4761
return $this->author;

0 commit comments

Comments
 (0)